Обработка исключений в Java: полное руководство с лучшими и наихудшими практиками

Обзор Обработка исключений в Java - одна из самых основных и фундаментальных вещей, которую разработчик должен знать наизусть. К сожалению, это часто упускается из виду, и важность обработки исключений недооценивается - она так же важна, как и остальной код. В этой статье давайте рассмотрим все, что вам нужно знать об обработке исключений в Java, а также о хороших и плохих практиках. Что такое обработка исключений? В реальной жизни мы ежедневно окружены обработкой исключений. Когда ord

Обзор

Обработка исключений в Java - одна из самых основных и фундаментальных вещей, которую разработчик должен знать наизусть. К сожалению, это часто упускается из виду, и важность обработки исключений недооценивается - она так же важна, как и остальной код.

В этой статье давайте рассмотрим все, что вам нужно знать об обработке исключений в Java, а также о хороших и плохих практиках.

Что такое обработка исключений?

В реальной жизни мы ежедневно окружены обработкой исключений.

При заказе товара в интернет-магазине - товар может отсутствовать на складе или может произойти сбой в доставке. Таким исключительным условиям можно противодействовать, производя другой продукт или отправляя новый после неудачной доставки.

При создании приложений они могут столкнуться со всевозможными исключительными условиями. К счастью, будучи опытным в обработке исключений, таким условиям можно противодействовать, изменяя поток кода.

Зачем использовать обработку исключений?

При создании приложений мы обычно работаем в идеальной среде - файловая система может предоставить нам все файлы, которые мы запрашиваем, наше интернет-соединение стабильно, а JVM всегда может предоставить достаточно памяти для наших нужд.

К сожалению, на самом деле среда далека от идеала - файл не может быть найден, подключение к Интернету время от времени прерывается, JVM не может предоставить достаточно памяти, и мы остаемся с пугающей StackOverflowError .

Если мы не справимся с такими условиями, все приложение окажется в руинах, а весь остальной код устареет. Следовательно, мы должны уметь писать код, который может адаптироваться к таким ситуациям.

Представьте, что компания не может решить простую проблему, возникшую после заказа продукта - вы не хотите, чтобы ваше приложение работало таким образом.

Иерархия исключений

Все это вызывает вопрос - что это за исключения в глазах 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

Более распространенным подходом было бы использование 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.<init>(FileInputStream.java:138) 
 at java.util.Scanner.<init>(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. 

наконец блоки

Представляя новый тип блока, блок finally выполняется независимо от того, что происходит в блоке 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 также были введены блоки перехвата объединения . Они позволяют нам обрабатывать несколько исключений одинаково и обрабатывать их исключения в одном блоке:

 public void parseFile(String filePath) { 
 try { 
 // some code 
 } catch (IOException | NumberFormatException ex) { 
 // handle 
 } 
 } 

Как выбросить исключения

Иногда мы не хотим обрабатывать исключения. В таких случаях мы должны заботиться о том, чтобы генерировать их только тогда, когда это необходимо, и позволять кому-то другому, вызывая наш метод, обрабатывать их соответствующим образом.

Выброс проверенного исключения

Когда что - то идет не так, как количество пользователей в настоящее время подключения к нашему сервису превышает сумму максимальной для сервера , чтобы справиться легко, мы хотим , чтобы throw исключение , чтобы указать на исключительную ситуацию:

 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."); 
 } 
 } 

Этот код увеличит numberOfUsers до тех пор, пока не превысит максимальное рекомендуемое количество, после чего вызовет исключение. Поскольку это проверенное исключение, мы должны добавить предложение 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!"); 
 } 
 } 

Поскольку мы генерируем исключение времени выполнения, нет необходимости включать его в сигнатуру метода, как в приведенном выше примере, но это часто считается хорошей практикой, по крайней мере, для документации.

Опять же, определить настраиваемое исключение времени выполнения, подобное этому, так же просто, как:

 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); 
 } 
 } 

Повторное отображение Throwable или _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; 
 } 
 } 

Теперь мы должны объявить, что метод выбрасывает Throwable . Почему это может быть полезно - это обширная тема, выходящая за рамки этого блога, но в этом конкретном случае есть способы использования.

Наследование исключений

Подклассы, наследующие метод, могут генерировать меньше проверенных исключений, чем их суперкласс:

 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-with-resources

Как уже упоминалось выше, всегда лучше использовать более новый, более лаконичный и понятный подход при работе с ресурсами.

Закройте ресурсы в try-catch-finally

Если вы по какой-либо причине не используете предыдущий совет, по крайней мере, не забудьте закрыть ресурсы вручную в блоке finally.

Я не буду включать пример кода для этого, поскольку оба уже были предоставлены для краткости.

Наихудшие методы обработки исключений

Проглатывание исключений

Если ваше намерение состоит в том, чтобы просто удовлетворить компилятор, вы можете легко сделать это, проглотив исключение :

 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, то выполняется finally , а затем есть выбор.

Итак, в терминологии документации, если finally завершается нормально, то try завершается внезапно по причине R.

Если finally завершается внезапно по причине S, то try завершается внезапно по причине S (и причина 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 в finally приведет к удалению исключения из блока 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; 
 } 
 } 

Это избыточно и просто приведет к появлению кучи сообщений журнала, которые на самом деле не нужны. Объем текста уменьшит видимость журналов.

Перехват исключения или бросок

Почему бы нам просто не поймать Exception или Throwable, если он улавливает все подклассы?

Обычно не рекомендуется делать это, если нет веской конкретной причины поймать любой из этих двух.

Catching Exception будет перехватывать как проверенные исключения, так и исключения времени выполнения. Исключения времени выполнения представляют собой проблемы, которые являются прямым результатом проблемы программирования, и поэтому их не следует обнаруживать, поскольку нельзя разумно ожидать их восстановления или обработки.

Catching Throwable поймает все . Это включает в себя все ошибки, которые на самом деле никоим образом не предназначены для обнаружения.

Заключение

В этой статье мы рассмотрели исключения и их обработку с нуля. После этого мы рассмотрели лучшие и худшие практики обработки исключений в Java.

Надеюсь, вы нашли этот блог информативным и образовательным, удачного кодирования!

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus