Исключения в Java: catch под лупой. Часть 3
Исследуем, в каком порядке обрабатываются исключения, как поймать несколько исключений сразу и заставить программу выполнить задачу при любом из них.
Иллюстрация: Dana Moskvina / Skillbox Media
Если вы уже слышали про конструкцию try-catch, но ещё не знаете, как работает блок catch, эта статья для вас. Если оба термина звучат незнакомо, прочитайте предыдущие статьи из цикла об исключениях в Java.
Один try — сколько угодно catch
Конструкция try-catch в языке Java помогает обрабатывать исключения. В блоке catch мы указываем класс исключений, которые «ловим». Взгляните на код ниже — если в try случится ArithmeticException, управление перейдёт к блоку catch и программа выдаст ошибку: Что ты делаешь, говорили же не делить на ноль!.
Но что будет, если в коде случится другое исключение, которое мы не предусматривали?
Зайдём издалека: в Java ссылочная переменная может ссылаться на любой объект, созданный из её дочернего класса. Например, если класс Child наследуется от класса Parent, то ссылка на переменную типа Child может храниться в переменной типа Parent: Parent man = new Child();.
Это правило работает везде — catch (ArithmeticException e) обработает любое исключение, если оно наследуется от ArithmeticException. Но если в блоке случится событие, которое не относится ни к ArithmeticException, ни к его классам-наследникам, try-catch для него не сработает.
Кроме ArithmeticException есть и другие исключения, одно из них — NullPointerException. Оно возникает, когда ссылка на объект хранит null и по ней пытаются получить значение поля объекта или вызвать его метод.
Пример:
На практике NullPointerException по сравнению с другими встроенными в Java исключениями возникает очень часто, поэтому у него есть сокращённое название: NPE.
Желательно писать код так, чтобы NPE не возникали. Для этого нужно, чтобы null никогда не передавался в качестве аргумента и не возвращался в результате метода null. Тогда и обрабатывать NPE не придётся.
Но если мы хотим перехватывать ArithmeticException и NullPointerException одновременно, после try можно написать сколько угодно catch и в каждом из них обработать нужное исключение:
Когда в try возникает исключение, Java по порядку проверяет блоки catch и находит первый подходящий.
В нашем примере это произойдёт так: если в try произошло исключение, Java проверит — это ArithmeticException? Если да, то запустится код из первого catch. Если нет, Java пойдёт дальше. Возникло NPE? Тогда сработает второй catch. Если ничего не совпало, то ни один из catch не подошёл — программа «выбросит» исключение, как будто его никто и не «ловил».
Важный момент: управление программы либо попадёт в один из блоков catch, либо не попадёт никуда.
Почему важен порядок блоков catch
Взглянем на этот пример:
Если в блоке try случится ArithmeticException или NullPointerException, сработает первый или второй catch. Если это будет другое исключение, Java проверит, не Exception ли это, и выполнит третий catch. Всё потому, что любое исключение наследуется от Exception. Мы писали об этом в первой статье.
ArithmeticException и NullPointerException — подклассы Exception, но такие исключения не попадут в третий catch, потому что их обработали в коде выше.
Если поменять порядок catch местами, код не соберётся. Компилятор поймёт, что второй и третий catch никогда не сработают, потому что они наследники исключения Exception, которое обрабатывается выше.
В одной конструкции try-catch обработка подклассов-исключений не может располагаться ниже обработки их суперклассов.
Multi-catch — как поймать несколько исключений одним кодом
Как сделать так, чтобы для разных исключений выполнялся один код? Первая мысль — написать один блок и скопировать его в разные части программы. Это сработает, но дублировать код — дурной тон. Второй вариант — вынести дублирующийся код в метод и вызывать его в каждом из catch, раньше так и делали.
Но в 2011 году появился третий способ. Тогда вышел Java 7, где несколько catch можно объединить в один, который будет выполняться для разных классов исключений.
Это называется multi-catch — многократный перехват. В нём можно перечислить несколько исключений, разделив их знаком |:
Finally — выполнить при любых условиях
У конструкции try-catch есть ещё один блок, он называется finally. Исполняемая программа попадает в него всегда — неважно, произошло ли исключение в try и сработал ли какой-то catch.
Блок finally сработает, даже если в try или в catch код наткнётся на return. Сначала выполнится finally, а потом программа выйдет из метода.
Единственный случай, когда finally не срабатывает, — критическая ошибка, когда программа вылетела или у неё закончилась выделенная память.
Приведём пример ситуации, в которой полезен finally. Представим, что нужно открыть файл, получить данные и закрыть его. Java работает с файлами через потоки ввода-вывода. Не страшно, если вы с ними не знакомы. Важно то, что для работы с потоком его нужно открыть, а в конце работы закрыть, чтобы в операционной системе не было утечки ресурсов.
Звучит несложно: открыть поток, поработать с ним и закрыть его. Но что будет, если в процессе произойдёт исключение и поток не закроется? Спасает finally: в блок try нужно поместить весь код для работы с потоком, в catch — для обработки возможных ошибок, а в finally — для закрытия потоков.
Похоже, с такой целью finally использовали так часто, что в Java 7 придумали расширенную форму try и назвали её try-with-resources.
Теперь, когда вы знаете про finally, делимся занимательным фактом: в конструкции try-catch может не быть блока catch. Но для этого там должен быть finally.
Итог: как работает блок try-catch
Пары статей не хватит, чтобы полностью разобраться с try-catch, но теперь вы уже знаете:
- В блоке try-catch может быть больше одного catch.
- Если происходит исключение, выполняется код первого подходящего блока catch.
- Всегда или выполняется один catch, или не выполняется ни одного.
- Компилятор умный и не допустит, чтобы обработки подклассов-исключений были ниже, чем catch для суперклассов.
- Когда несколько разных исключений нужно обрабатывать одинаково, поможет multi-catch.
- Независимо от того, что произошло в блоках try и catch, исполнение программы всегда попадает в finally.