Автор оригинала: David Landup.
Обзор
Обработка исключений в Java-одна из самых основных и фундаментальных вещей, которые разработчик должен знать наизусть. К сожалению, это часто упускается из виду, а важность обработки исключений недооценивается – это так же важно, как и остальная часть кода.
В этой статье давайте рассмотрим все, что вам нужно знать об обработке исключений в Java, а также о хороших и плохих методах.
Что такое Обработка исключений?
Мы ежедневно сталкиваемся с обработкой исключений в реальной жизни.
При заказе товара в интернет – магазине-товара может не быть в наличии на складе или может произойти сбой в доставке. Таким исключительным условиям можно противостоять, изготовив другой продукт или отправив новый после сбоя в доставке.
При создании приложений – они могут столкнуться со всевозможными исключительными условиями. К счастью, будучи опытным в обработке исключений, таким условиям можно противостоять, изменив поток кода.
Зачем использовать обработку исключений?
При создании приложений мы обычно работаем в идеальной среде – файловая система может предоставить нам все файлы, которые мы запрашиваем, наше подключение к Интернету стабильно, а JVM всегда может предоставить достаточно памяти для наших нужд.
К сожалению, на самом деле окружающая среда далека от идеала – файл не может быть найден, время от времени прерывается подключение к Интернету, а JVM не может предоставить достаточно памяти, и мы остаемся с пугающей Ошибкой StackOverflow
.
Если мы не справимся с такими условиями, все приложение окажется в руинах, а весь остальной код устареет. Поэтому мы должны уметь писать код, который может адаптироваться к таким ситуациям.
Представьте, что компания не может решить простую проблему, возникшую после заказа продукта, – вы не хотите, чтобы ваше приложение работало таким образом.
Иерархия исключений
Все это просто напрашивается вопрос – каковы эти исключения в глазах Java и JVM?
В конце концов, исключения-это просто объекты Java, которые расширяют интерфейс Throwable
:
---> Throwable <--- | (checked) | | | | | ---> Exception Error | (checked) (unchecked) | RuntimeException (unchecked)
Когда мы говорим об исключительных условиях, мы обычно имеем в виду одно из трех:
- Проверенные Исключения
- Непроверенные Исключения/Исключения во Время Выполнения
- Ошибки
Примечание : Термины “Время выполнения” и “Непроверенный” часто используются взаимозаменяемо и относятся к одному и тому же типу исключений.
Проверенные Исключения
Проверенные исключения-это исключения, которые мы обычно можем предвидеть и планировать заранее в нашем приложении. Это также исключения, которые компилятор Java требует, чтобы мы либо обрабатывали, либо объявляли при написании кода.
Правило обработки или объявления относится к нашей ответственности за то, чтобы либо объявить, что метод создает исключение в стеке вызовов, не предпринимая особых усилий для его предотвращения, либо обработать исключение с помощью нашего собственного кода, что обычно приводит к восстановлению программы из исключительного состояния.
По этой причине они называются проверенными исключениями . Компилятор может обнаружить их до выполнения, и вы знаете об их потенциальном существовании во время написания кода.
Непроверенные Исключения
Непроверенные исключения-это исключения, которые обычно возникают из-за человеческой, а не экологической ошибки. Эти исключения проверяются не во время компиляции, а во время выполнения, поэтому их также называют Исключениями во время выполнения .
Им часто можно противостоять, реализуя простые проверки перед сегментом кода, который потенциально может быть использован таким образом, чтобы сформировать исключение во время выполнения, но об этом позже.
Ошибки
Ошибки-это самые серьезные исключительные условия, с которыми вы можете столкнуться. От них часто невозможно избавиться, и нет реального способа справиться с ними. Единственное, что мы, как разработчики, можем сделать, – это оптимизировать код в надежде, что ошибки никогда не возникнут.
Ошибки могут возникать из-за ошибок человека и окружающей среды. Создание бесконечно повторяющегося метода может привести к ошибке StackOverflowError
, или утечка памяти может привести к ошибке OutOfMemoryError
.
Как обрабатывать исключения
бросок и броски
Самый простой способ устранить ошибку компилятора при работе с проверенным исключением – просто выбросить ее.
public File getFile(String url) throws FileNotFoundException { // some code throw new FileNotFoundException(); }
Мы обязаны пометить подпись нашего метода предложением throws
. Метод может добавлять столько исключений, сколько необходимо в его предложении throws
, и может добавлять их позже в коде, но это не обязательно. Этот метод не требует оператора return
, даже если он определяет тип возвращаемого значения. Это связано с тем, что по умолчанию он создает исключение, которое резко прекращает поток метода. Поэтому оператор return
будет недоступен и вызовет ошибку компиляции.
Имейте в виду, что любой, кто вызывает этот метод, также должен следовать правилу обработки или объявления.
При создании исключения мы можем либо создать новое исключение, как в предыдущем примере, либо пойманное исключение.
попробуйте-поймайте блоки
Более распространенным подходом было бы использовать try
– catch
блок для перехвата и обработки возникающего исключения:
public String readFirstLine(String url) throws FileNotFoundException { try { Scanner scanner = new Scanner(new File(url)); return scanner.nextLine(); } catch(FileNotFoundException ex) { throw ex; } }
В этом примере мы “отметили” опасный сегмент кода, заключив его в блок try
. Это говорит компилятору, что мы знаем о потенциальном исключении и намерены обработать его, если оно возникнет.
Этот код пытается прочитать содержимое файла, и если файл не найден, исключение FileNotFoundException
| поймано и повторно обработано . Подробнее на эту тему позже.
Запуск этого фрагмента кода без допустимого URL-адреса приведет к появлению исключения:
Exception in thread "main" java.io.FileNotFoundException: some_file (The system cannot find the file specified) <-- some_file doesn't exist at java.io.FileInputStream.open0(Native Method) at java.io.FileInputStream.open(FileInputStream.java:195) at java.io.FileInputStream.(FileInputStream.java:138) at java.util.Scanner. (Scanner.java:611) at Exceptions.ExceptionHandling.readFirstLine(ExceptionHandling.java:15) <-- Exception arises on the the readFirstLine() method, on line 15 at Exceptions.ExceptionHandling.main(ExceptionHandling.java:10) <-- readFirstLine() is called by main() on line 10 ...
В качестве альтернативы, мы можем попытаться оправиться от этого состояния вместо того, чтобы бросать:
public static String readFirstLine(String url) { try { Scanner scanner = new Scanner(new File(url)); return scanner.nextLine(); } catch(FileNotFoundException ex) { System.out.println("File not found."); return null; } }
Запуск этого фрагмента кода без действительного URL-адреса приведет к:
File not found.
наконец, Блоки
Вводя новый тип блока, блок наконец
выполняется независимо от того, что происходит в блоке try. Даже если он внезапно завершится созданием исключения, блок finally
будет выполнен.
Это часто использовалось для закрытия ресурсов, открытых в блоке try
, поскольку возникающее исключение пропускало бы код, закрывающий их:
public String readFirstLine(String path) throws IOException { BufferedReader br = new BufferedReader(new FileReader(path)); try { return br.readLine(); } finally { if(br != null) br.close(); } }
Однако этот подход был осужден после выпуска Java 7, которая представила лучший и более чистый способ закрытия ресурсов, и в настоящее время рассматривается как плохая практика.
заявление о попытке использования ресурсов
Ранее сложный и подробный блок может быть заменен:
static String readFirstLineFromFile(String path) throws IOException { try(BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } }
Это намного чище и, очевидно, упрощено, если включить объявление в круглые скобки блока try
.
Кроме того, вы можете включить в этот блок несколько ресурсов, один за другим:
static String multipleResources(String path) throws IOException { try(BufferedReader br = new BufferedReader(new FileReader(path)); BufferedWriter writer = new BufferedWriter(path, charset)) { // some code } }
Таким образом, вам не нужно беспокоиться о закрытии ресурсов самостоятельно, так как блок try-with-resources гарантирует, что ресурсы будут закрыты в конце инструкции.
Несколько блоков захвата
Когда код, который мы пишем, может выдавать более одного исключения, мы можем использовать несколько блоков catch для обработки их по отдельности:
public void parseFile(String filePath) { try { // some code } catch (IOException ex) { // handle } catch (NumberFormatException ex) { // handle } }
Когда блок try
вызывает исключение, JVM проверяет, является ли первое пойманное исключение подходящим, и если нет, продолжает, пока не найдет его.
Примечание : При перехвате общего исключения будут перехвачены все его подклассы, поэтому не требуется перехват их отдельно.
Перехват FileNotFound
исключения в этом примере не требуется, поскольку он распространяется из Исключение IOException
, но если возникнет необходимость, мы сможем поймать его до Исключения IOException
:
public void parseFile(String filePath) { try { // some code } catch(FileNotFoundException ex) { // handle } catch (IOException ex) { // handle } catch (NumberFormatException ex) { // handle } }
Таким образом, мы можем обрабатывать более конкретное исключение иным способом, чем более общее.
Примечание : При перехвате нескольких исключений компилятор Java требует, чтобы мы помещали более конкретные исключения перед более общими, иначе они были бы недоступны и привели бы к ошибке компилятора.
Блоки захвата соединения
Чтобы уменьшить шаблонный код, в Java 7 также были введены блоки union catch/|. Они позволяют нам одинаково обрабатывать несколько исключений и обрабатывать их исключения в одном блоке:
public void parseFile(String filePath) { try { // some code } catch (IOException | NumberFormatException ex) { // handle } }
Как создавать исключения
Иногда мы не хотим обрабатывать исключения. В таких случаях мы должны заботиться только о том, чтобы генерировать их, когда это необходимо, и позволять кому-то другому, вызывающему наш метод, обрабатывать их соответствующим образом.
Создание проверенного исключения
Когда что-то идет не так, например, количество пользователей, подключающихся в настоящее время к нашему сервису, превышает максимальную сумму, которую сервер может легко обработать, мы хотим создать
исключение, чтобы указать на исключительную ситуацию:
public void countUsers() throws TooManyUsersException { int numberOfUsers = 0; while(numberOfUsers < 500) { // some code numberOfUsers++; } throw new TooManyUsersException("The number of users exceeds our maximum recommended amount."); } }
Git Essentials
Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!
Этот код будет увеличивать количество Пользователей
до тех пор, пока не превысит максимально рекомендуемую сумму, после чего он выдаст исключение. Поскольку это проверяемое исключение, мы должны добавить предложение throws
в подпись метода.
Определить подобное исключение так же просто, как написать следующее:
public class TooManyUsersException extends Exception { public TooManyUsersException(String message) { super(message); } }
Создание непроверенного исключения
Создание исключений во время выполнения обычно сводится к проверке входных данных, поскольку они чаще всего возникают из – за неправильного ввода-либо в виде исключения IllegalArgumentException
, NumberFormatException
, ArrayIndexOutOfBoundsException
, либо NullPointerException
:
public void authenticateUser(String username) throws UserNotAuthenticatedException { if(!isAuthenticated(username)) { throw new UserNotAuthenticatedException("User is not authenticated!"); } }
Поскольку мы создаем исключение runtimeexception, нет необходимости включать его в подпись метода, как в приведенном выше примере, но это часто считается хорошей практикой, по крайней мере, для документации.
Опять же, определить пользовательское исключение во время выполнения, подобное этому, так же просто, как:
public class UserNotAuthenticatedException extends RuntimeException { public UserNotAuthenticatedException(String message) { super(message); } }
Бросание
Создание исключения упоминалось ранее, поэтому вот краткий раздел для уточнения:
public String readFirstLine(String url) throws FileNotFoundException { try { Scanner scanner = new Scanner(new File(url)); return scanner.nextLine(); } catch(FileNotFoundException ex) { throw ex; } }
Повторное выбрасывание относится к процессу выбрасывания уже пойманного исключения, а не к выбрасыванию нового.
Обертывание
Обертывание, с другой стороны, относится к процессу обертывания уже пойманного исключения в другое исключение:
public String readFirstLine(String url) throws FileNotFoundException { try { Scanner scanner = new Scanner(new File(url)); return scanner.nextLine(); } catch(FileNotFoundException ex) { throw new SomeOtherException(ex); } }
Бросание бросаемого или _Exception*?
Эти классы верхнего уровня могут быть пойманы и переосмыслены, но как это сделать, может варьироваться:
public void parseFile(String filePath) { try { throw new NumberFormatException(); } catch (Throwable t) { throw t; } }
В этом случае метод создает исключение NumberFormatException
, которое является исключением во время выполнения. Из-за этого нам не нужно отмечать подпись метода с помощью NumberFormatException
или Throwable
.
Однако, если мы создадим проверенное исключение в методе:
public void parseFile(String filePath) throws Throwable { try { throw new IOException(); } catch (Throwable t) { throw t; } }
Теперь мы должны объявить, что метод выбрасывает Бросаемый
. Почему это может быть полезно-это широкая тема, которая выходит за рамки данного блога, но для этого конкретного случая есть способы использования.
Наследование исключений
Подклассы, наследующие метод, могут создавать только меньшее количество проверенных исключений, чем их суперкласс:
public class SomeClass { public void doSomething() throws SomeException { // some code } }
При таком определении следующий метод вызовет ошибку компилятора:
public class OtherClass extends SomeClass { @Override public void doSomething() throws OtherException { // some code } }
Лучшие и худшие методы обработки исключений
Учитывая все это, вы должны быть хорошо знакомы с тем, как работают исключения и как их использовать. Теперь давайте рассмотрим лучшие и худшие методы, когда дело доходит до обработки исключений, которые, мы надеемся, теперь полностью понятны.
Лучшие Методы Обработки Исключений
Избегайте Исключительных Условий
Иногда, используя простые проверки, мы можем вообще избежать формирования исключений:
public Employee getEmployee(int i) { Employee[] employeeArray = {new Employee("David"), new Employee("Rhett"), new Employee("Scott")}; if(i >= employeeArray.length) { System.out.println("Index is too high!"); return null; } else { System.out.println("Employee found: " + employeeArray[i].name); return employeeArray[i]; } } }
Вызов этого метода с допустимым индексом приведет к:
Employee found: Scott
Но вызов этого метода с индексом, который выходит за рамки, приведет к:
Index is too high!
В любом случае, даже если индекс слишком высок, нарушающая строка кода не будет выполнена, и никаких исключений не возникнет.
Используйте попытку с ресурсами
Как уже упоминалось выше, всегда лучше использовать более новый, более лаконичный и чистый подход при работе с ресурсами.
Закройте ресурсы в try-catch-наконец-то
Если вы по какой-либо причине не используете предыдущий совет, по крайней мере, не забудьте закрыть ресурсы вручную в блоке “Наконец”.
Я не буду включать пример кода для этого, так как оба они уже были предоставлены для краткости.
Наихудшие Методы Обработки Исключений
Глотание Исключений
Если вы намерены просто удовлетворить компилятор, вы можете легко сделать это, проглотив исключение :
public void parseFile(String filePath) { try { // some code that forms an exception } catch (Exception ex) {} }
Проглатывание исключения относится к акту перехвата исключения и не устранения проблемы.
Таким образом, компилятор удовлетворен, так как исключение поймано, но вся соответствующая полезная информация, которую мы могли извлечь из исключения для отладки, потеряна, и мы ничего не сделали для восстановления после этого исключительного условия.
Еще одна очень распространенная практика-просто распечатать трассировку стека исключения:
public void parseFile(String filePath) { try { // some code that forms an exception } catch(Exception ex) { ex.printStackTrace(); } }
Такой подход создает иллюзию управляемости. Да, хотя это лучше, чем просто игнорировать исключение, распечатав соответствующую информацию, это не справляется с исключительным условием больше, чем игнорирование.
Вернитесь в последний блок
В соответствии с JLS ( Спецификация языка Java ):
Если выполнение блока try завершается внезапно по какой-либо другой причине R, то наконец
блок выполняется, и тогда есть выбор.
Итак, в терминологии документации, если блок наконец
завершается нормально, то оператор try
завершается внезапно по причине R.
Если блок finally
завершается внезапно по причинам, то оператор try
завершается внезапно по причинам (и причина R отбрасывается).
По сути, при резком возврате из блока finally
JVM удалит исключение из блока try
, и все ценные данные из него будут потеряны:
public String doSomething() { String name = "David"; try { throw new IOException(); } finally { return name; } }
В этом случае, даже если блок try
создает новый Исключение IOException
, мы используем return
в блоке finally
, резко завершая его. Это приводит к внезапному завершению блока try
из-за оператора return, а не из-за исключения IOException
, что, по сути, приводит к удалению исключения в процессе.
Бросаю в последний блок
Очень похоже на предыдущий пример, использование throw
в наконец
блоке приведет к удалению исключения из try-catch блока:
public static String doSomething() { try { // some code that forms an exception } catch(IOException io) { throw io; } finally { throw new MyException(); } }
В этом примере исключение MyException
, созданное внутри блока finally
, затмит исключение, созданное блоком catch
, и вся ценная информация будет удалена.
Имитация оператора goto
Критическое мышление и творческие способы поиска решения проблемы-хорошая черта, но некоторые решения, какими бы творческими они ни были, неэффективны и избыточны.
Java не имеет оператора goto , как некоторые другие языки, а скорее использует метки для перехода по коду:
public void jumpForward() { label: { someMethod(); if (condition) break label; otherMethod(); } }
И все же некоторые люди используют исключения для их имитации:
public void jumpForward() { try { // some code 1 throw new MyException(); // some code 2 } catch(MyException ex) { // some code 3 } }
Использование исключений для этой цели неэффективно и медленно. Исключения предназначены для исключительного кода и должны использоваться для исключительного кода.
Лесозаготовки и метания
При попытке отладить фрагмент кода и выяснить, что происходит, не регистрируйте и не создавайте исключение:
public static String readFirstLine(String url) throws FileNotFoundException { try { Scanner scanner = new Scanner(new File(url)); return scanner.nextLine(); } catch(FileNotFoundException ex) { LOGGER.error("FileNotFoundException: ", ex); throw ex; } }
Выполнение этого является избыточным и просто приведет к появлению множества сообщений журнала, которые на самом деле не нужны. Объем текста уменьшит видимость журналов.
Перехват исключения или Выбрасывание
Почему бы нам просто не перехватить исключение или Выбрасываемое, если оно улавливает все подклассы?
Если нет веской, конкретной причины поймать кого-либо из этих двоих, обычно не рекомендуется этого делать.
Перехват Исключения
будет перехватывать как проверенные, так и исключения во время выполнения. Исключения во время выполнения представляют собой проблемы, которые являются прямым результатом проблемы программирования, и поэтому их не следует перехватывать, поскольку нельзя разумно ожидать, что они будут устранены или обработаны.
Ловить Бросаемый
поймает все . Это включает в себя все ошибки, которые на самом деле никоим образом не должны быть пойманы.
Вывод
В этой статье мы рассмотрели исключения и обработку исключений с нуля. После этого мы рассмотрели лучшие и худшие методы обработки исключений в Java.
Надеюсь, вы нашли этот блог информативным и образовательным, счастливого кодирования!