Исключения в Java, Часть II (checked/unchecked)
Это вторая часть статьи (первая часть — try-catch-finally), посвященной такому языковому механизму Java как исключения. Она имеет вводный характер и рассчитана на начинающих разработчиков или тех, кто только приступает к изучению языка.
Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).
1. «Магия» checked/uncheckedМеханизм исключительных ситуация в Java связан с двумя элементами «магии», т.е. поведения, которое никак не отражено в исходном коде: 1. «Магию» java.lang.Throwable — в throw, catch и throws могут стоять исключительно Throwable или его наследники (мы уже разбирали в предыдущей лекции). Это «право» находиться в throw, catch и throws никак не отражено в исходном коде. 2. Все исключительные ситуации делятся на «проверяемые» (checked) и «непроверяемые» (unchecked). Это свойство присуще «корневищу» (Throwable, Error, Exception, RuntimeException) и передается по наследству. Никак не видимо в исходном коде класса исключения. В дальнейших примера просто учтите, что — Throwable и Exception и все их наследники (за исключением наследников Error-а и RuntimeException-а) — checked — Error и RuntimeException и все их наследники — unchecked
checked exception = проверяемое исключение, проверяемое компилятором. Что именно проверяет компилятор мы разберем на этой лекции.
Напомним иерархию исключений
Расставим значение свойства checked/unchecked
2. Пессимистичный механизмЯ называю связь между проверяемыми исключениями и throws — «пессимистичной», польку мы можем «напугать» о большем, чем может произойти на самом деле, но не наоборот
Мы не можем бросать, но не предупредить
Мы не можем бросать, но предупредить о «меньшем»
Мы можем предупредить точно о том, что бросаем
Мы можем предупредить о большем, чем мы бросаем
Можем даже предупредить о том, чего вообще нет
Даже если предупреждаем о том, чего нет — все обязаны бояться
Хотя они (испугавшиеся) могут перепугать остальных еще больше
В чем цель «пессимистичности»? Все достаточно просто. Вы в режиме протипирования «набросали», скажем, класс-утилиту для скачивания из интернета
и хотели бы «принудить» пользователей вашего класса УЖЕ ОБРАБАТЫВАТЬ возможное исключение IOException, хотя из реализации-пустышки вы ПОКА НЕ ГЕНЕРИРУЕТЕ такое исключение. Но в будущем — собираетесь.
3. throws с непроверяемым (unckecked) исключениемВызов метода, который «пугает» unchecked исключением не накладывает на нас никаких обязанностей.
Эта конструкция служит цели «указать» программисту-читателю кода на то, что ваш метод может выбросить некоторое непроверяемое (unchecked) исключение.
Пример (java.lang.NumberFormatException — непроверяемое исключение):
Integer.parseInt() может бросить unchecked NumberFormatException на неподходящем аргументе (int k = Integer.parseInt(«123abc»)), это отразили — в сигнатуре метода: throws NumberFormatException — в документации (javadoc): @ exception но это ни к чему нас не обязывает.
4. Множественные исключенияРассмотрим ситуацию с кодом, который может бросать проверяемые исключения разных типов. Далее учитывайте, что EOFException и FileNotFoundException — потомки IOException.
Мы можем точно указать, что выбрасываем
А можем «испугать» сильнее (предком обоих исключений)
Можно и вот так: EOFException и FileNotFoundException «обобщаем до» IOException, а InterruptedException «пропускаем нетронутым» (InterruptedException — НЕ потомок IOException)
5. Или catch, или throwsНе надо пугать тем, что вы перехватили так
или так (ставим catch по предку и точно перехватываем)
Но если перехватили только потомка, то не выйдет
Не годится, естественно, и перехватывание «брата»
Если вы часть перехватили, то можете этим не пугать
6. Поведение компилятора/JVMНеобходимо понимать, что — проверка на cheched исключения происходит в момент компиляции (compile-time checking) — перехват исключений (catch) происходит в момент выполнения (runtime checking)
Полная аналогия с
Хотя ССЫЛКА ref УКАЗЫВАЕТ на объект типа java.lang.String (у которого имеется метод charAt(int)), но ТИП ССЫЛКИ — java.lang.Object (ссылка типа java.lang.Object на объект типа java.lang.String). Компилятор ориентируется на «левый тип» (тип ссылки, а не тип ссылаемого) и не пропускает такой код.
Хотя В ДАННОЙ СИТУАЦИИ компилятор мог бы разобрать, что t ссылается на Exception, а ref — на String, но этого уже невозможно сделать при раздельно компиляции. Представьте, что мы МОГЛИ БЫ скомпилировать ОТДЕЛЬНО такой класс, упаковать в jar и распространять
А кто-то берет этот класс, добавляет в classpath и вызывает App.f0(new Throwable()) или App.f1(new Integer(42)). В таком случае JVM столкнулась бы с ситуацией, когда от нее требует бросить проверяемое исключение, которое не отследил компилятор (в случае с f0) или вызвать метод, которого нет (в случае с f1)!
Java — язык со статической типизацией (т.е. отслеживание корректности использования типов (наличие используемых полей, наличие вызываемых методов, проверка на checked исключения, . ) проводится компилятором), запрещает такое поведение. В некоторых языках (языки с динамической типизацией — отслеживание корректности использования типов проводится средой исполнения (оно разрешено, например в JavaScript).
Компилятор не пропустит этот код, хотя метод main ГАРАНТИРОВАННО НЕ ВЫБРОСИТ ИСКЛЮЧЕНИЯ
7. Overriding и throwsПри переопределении (overriding) список исключений потомка не обязан совпадать с таковым у предка. Но он должен быть «не сильнее» списка предка:
Однако тут мы попытались «расширить тип» бросаемых исключений
Почему можно сужать тип, но не расширять? Рассмотрим следующую ситуацию:
Внимательно посмотрите на этот пример — если бы потомок мог расширять тип бросаемого исключения предка, то те места которые «ждут» предка, а получают экземпляр «расширенного» потомка могли бы неконтролируемо выбрасывать проверяемые исключения
8. Передача свойства по наследствуНапомним иерархию исключений с расставленными флагами свойства checked/unchecked
Логика расположения свойства НЕ СВЯЗАНА С НАСЛЕДОВАНИЕМ. Эту логику мы рассмотрим позже (в следующих статьях). Однако свойство checked/unchecked пользовательских классов исключений строится ИСКЛЮЧИТЕЛЬНО НА ОСНОВЕ НАСЛЕДОВАНИЯ. Правило крайне простое: 1. Если исключение из списка Throwable, Error, Exception, RuntimeException — то твое свойство надо просто запомнить. 2. Если ты не из списка, то твое свойство равно свойству предка. Нарушить наследование тут нельзя.
Если мы породим потомков A, B, C, D, E, F, G, H, I, J, K, L которые следующим образом наследуются от «корневища» (Throwable, Error, Exception, RuntimeException), то значение их свойства checked/unchecked можно увидеть на схеме