GolovachCourses
Компания
27,76
рейтинг
8 июня 2014 в 17:04

Разработка → Исключения в Java, Часть II (checked/unchecked) tutorial

Это вторая часть статьи (первая часть — try-catch-finally), посвященной такому языковому механизму Java как исключения. Она имеет вводный характер и рассчитана на начинающих разработчиков или тех, кто только приступает к изучению языка.

Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).

1. Магия checked/unchecked
2. Пессимистичный механизм
3. throws с непроверяемым (unckecked) исключением
4. Множественные исключения
5. Или catch, или throws
6. Поведение компилятора/JVM
7. Overriding и throws
8. Передача свойства по наследству


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 = проверяемое исключение, проверяемое компилятором.
Что именно проверяет компилятор мы разберем на этой лекции.

Напомним иерархию исключений
                    Object
                      |
                  Throwable
                  /      \
              Error     Exception
                            |
                    RuntimeException


Расставим значение свойства checked/unchecked
                    Object
                      |
               Throwable(CHECKED)
               /            \
     Error(UNCHECKED)    Exception(CHECKED)
                            |
                  RuntimeException(UNCHECKED)



2. Пессимистичный механизм



Я называю связь между проверяемыми исключениями и throws — «пессимистичной», польку мы можем «напугать» о большем, чем может произойти на самом деле, но не наоборот

Мы не можем бросать, но не предупредить
public class App {
    public static void main(String[] args) {
        throw new Exception(); // тут ошибка компиляции
    }
}

>> COMPILATION ERROR: unhandled exception: java.lang.Exception


Мы не можем бросать, но предупредить о «меньшем»
import java.io.IOException;

public class App {
    public static void main(String[] args) throws IOException {
        throw new Exception(); // тут ошибка компиляции
    }
}

>> COMPILATION ERROR: unhandled exception: java.lang.Exception


Мы можем предупредить точно о том, что бросаем
public class App {
    public static void main(String[] args) throws Exception { // предупреждаем о Exception
        throw new Exception(); // и кидаем Exception
    }
}


Мы можем предупредить о большем, чем мы бросаем
public class App {
    public static void main(String[] args) throws Throwable { // предупреждаем "целом" Throwable
        throw new Exception(); // а кидаем только Exception
    }
}


Можем даже предупредить о том, чего вообще нет
public class App {
    public static void main(String[] args) throws Exception { // пугаем
        // но ничего не бросаем
    }
}


Даже если предупреждаем о том, чего нет — все обязаны бояться
public class App {
    public static void main(String[] args) {
        f(); // тут ошибка компиляции
    }

    public static void f() throws Exception {
    }    
}

>> COMPILATION ERROR: unhandled exception: java.lang.Exception


Хотя они (испугавшиеся) могут перепугать остальных еще больше
public class App {
    // они пугают целым Throwable
    public static void main(String[] args) throws Throwable { 
        f();
    }
    // хотя мы пугали всего-лишь Exception
    public static void f() throws Exception {
    }    
}


В чем цель «пессимистичности»? Все достаточно просто.
Вы в режиме протипирования «набросали», скажем, класс-утилиту для скачивания из интернета
public class InternetDownloader {
    public static byte[] (String url) throws IOException { 
        return "<html><body>Nothing! It's stub!</body></html>".getBytes();
    }
}

и хотели бы «принудить» пользователей вашего класса УЖЕ ОБРАБАТЫВАТЬ возможное исключение IOException, хотя из реализации-пустышки вы ПОКА НЕ ГЕНЕРИРУЕТЕ такое исключение. Но в будущем — собираетесь.


3. throws с непроверяемым (unckecked) исключением


Вызов метода, который «пугает» unchecked исключением не накладывает на нас никаких обязанностей.
public class App {
    public static void main(String[] args) { 
        f();
    }
    public static void f() throws RuntimeException {
    }    
}

Эта конструкция служит цели «указать» программисту-читателю кода на то, что ваш метод может выбросить некоторое непроверяемое (unchecked) исключение.

Пример (java.lang.NumberFormatException — непроверяемое исключение):
package java.lang;

public final class Integer extends Number implements Comparable<Integer> {
    ...
    /**
     * ...
     *
     * @param s    a {@code String} containing the {@code int}
     *             representation to be parsed
     * @return     the integer value represented by the argument in decimal.
     * @exception  NumberFormatException  if the string does not contain a
     *               parsable integer.
     */
    public static int parseInt(String s) throws NumberFormatException {
        return parseInt(s,10);
    }
    ...
}

Integer.parseInt() может бросить unchecked NumberFormatException на неподходящем аргументе (int k = Integer.parseInt(«123abc»)), это отразили
— в сигнатуре метода: throws NumberFormatException
— в документации (javadoc): @ exception
но это ни к чему нас не обязывает.


4. Множественные исключения


Рассмотрим ситуацию с кодом, который может бросать проверяемые исключения разных типов.
Далее учитывайте, что EOFException и FileNotFoundException — потомки IOException.

Мы можем точно указать, что выбрасываем
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    // пугаем ОБОИМИ исключениями
    public static void main(String[] args) throws EOFException, FileNotFoundException {
        if (System.currentTimeMillis() % 2 == 0) {
            throw new EOFException();
        } else {
            throw new FileNotFoundException();
        }
    }
}

или вот так
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    // пугаем ОБОИМИ исключениями
    public static void main(String[] args) throws EOFException, FileNotFoundException {
        f0();
        f1();
    }
    public static void f0() throws EOFException {...}
    public static void f1() throws FileNotFoundException {...}
}


А можем «испугать» сильнее (предком обоих исключений)
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;

public class App {
    // пугаем ПРЕДКОМ исключений
    public static void main(String[] args) throws IOException {
        if (System.currentTimeMillis() % 2 == 0) {
            throw new EOFException();
        } else {
            throw new FileNotFoundException();
        }
    }
}

или вот так
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    // пугаем ПРЕДКОМ исключений
    public static void main(String[] args) throws IOException {
        f0();
        f1();
    }
    public static void f0() throws EOFException {...}
    public static void f1() throws FileNotFoundException {...}
}


Можно и вот так: EOFException и FileNotFoundException «обобщаем до» IOException, а InterruptedException «пропускаем нетронутым» (InterruptedException — НЕ потомок IOException)
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    public static void main(String[] args) throws IOException, InterruptedException {
        f0();
        f1();
        f2();
    }
    public static void f0() throws EOFException {...}
    public static void f1() throws FileNotFoundException {...}
    public static void f2() throws InterruptedException {...}
}



5. Или catch, или throws


Не надо пугать тем, что вы перехватили
так
public class App {
    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (Exception e) {
            // ...
        }
    }
}


или так (ставим catch по предку и точно перехватываем)
public class App {
    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (Throwable e) {
            // ...
        }
    }
}


Но если перехватили только потомка, то не выйдет
public class App {
    public static void main(String[] args) {
        try {
            throw new Throwable();
        } catch (Exception e) {
            // ...
        }
    }
}

>> COMPILATION ERROR: unhandled exception: java.lang.Throwable


Не годится, естественно, и перехватывание «брата»
public class App {
    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (Error e) {
            // ...
        }
    }
}

>> COMPILATION ERROR: unhandled exception: java.lang.Exception


Если вы часть перехватили, то можете этим не пугать
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    // EOFException перехватили catch-ом, им не пугаем
    public static void main(String[] args) throws FileNotFoundException {
        try {
            if (System.currentTimeMillis() % 2 == 0) {
                throw new EOFException();
            } else {
                throw new FileNotFoundException();
            }
        } catch (EOFException e) {
            // ...
        }
    }
}



6. Поведение компилятора/JVM


Необходимо понимать, что
проверка на cheched исключения происходит в момент компиляции (compile-time checking)
перехват исключений (catch) происходит в момент выполнения (runtime checking)
public class App {
    // пугаем Exception
    public static void main(String[] args) throws Exception { 
        Throwable t = new Exception(); // и лететь будет Exception
        throw t; // но тут ошибка компиляции 
    }  
}

>> COMPILATION ERROR: unhandled exception: java.lang.Throwable


Полная аналогия с
public class App {
    public static void main(String[] args) throws Exception { 
        Object ref = "Hello!";  // ref указывает на строку 
        char c = ref.charAt(0); // но тут ошибка компиляции 
    }  
}

>> COMPILATION ERROR: Cannot resolve method 'charAt(int)'

Хотя ССЫЛКА ref УКАЗЫВАЕТ на объект типа java.lang.String (у которого имеется метод charAt(int)), но ТИП ССЫЛКИ — java.lang.Object (ссылка типа java.lang.Object на объект типа java.lang.String). Компилятор ориентируется на «левый тип» (тип ссылки, а не тип ссылаемого) и не пропускает такой код.

Хотя В ДАННОЙ СИТУАЦИИ компилятор мог бы разобрать, что t ссылается на Exception, а ref — на String, но этого уже невозможно сделать при раздельно компиляции. Представьте, что мы МОГЛИ БЫ скомпилировать ОТДЕЛЬНО такой класс, упаковать в jar и распространять
// НЕ КОМПИЛИРУЕТСЯ! ИССЛЕДУЕМ ГИПОТЕТИЧЕСКУЮ СИТУАЦИЮ!
public class App {
    public static void f0(Throwable t) throws Exception { 
        throw t;
    }  
    public static void f1(Object ref){ 
        char c = ref.charAt(0);
    }  
}


А кто-то берет этот класс, добавляет в classpath и вызывает App.f0(new Throwable()) или App.f1(new Integer(42)). В таком случае JVM столкнулась бы с ситуацией, когда от нее требует бросить проверяемое исключение, которое не отследил компилятор (в случае с f0) или вызвать метод, которого нет (в случае с f1)!

Java — язык со статической типизацией (т.е. отслеживание корректности использования типов (наличие используемых полей, наличие вызываемых методов, проверка на checked исключения, ...) проводится компилятором), запрещает такое поведение. В некоторых языках (языки с динамической типизацией — отслеживание корректности использования типов проводится средой исполнения (оно разрешено, например в JavaScript).

Компилятор не пропустит этот код, хотя метод main ГАРАНТИРОВАННО НЕ ВЫБРОСИТ ИСКЛЮЧЕНИЯ
public class App {
    // пугаем Exception
    public static void main(String[] args) throws Exception { 
        try {
            Throwable t = new Exception(); // и лететь будет Exception
            throw t; // но тут ошибка компиляции 
        } catch (Exception e) {
            System.out.println("Перехвачено!");
        }
    }  
}

>> COMPILATION ERROR: unhandled exception: java.lang.Throwable


public class App {
    // ТЕПЕРЬ пугаем Throwable
    public static void main(String[] args) throws Throwable { 
        try {
            Throwable t = new Exception(); // а лететь будет Exception
            throw t;
        } catch (Exception e) { // и мы перехватим Exception
            System.out.println("Перехвачено!");
        }
    }  
}

>> Перехвачено!



7. Overriding и throws


При переопределении (overriding) список исключений потомка не обязан совпадать с таковым у предка.
Но он должен быть «не сильнее» списка предка:
import java.io.FileNotFoundException;
import java.io.IOException;

public class Parent {
    // предок пугает IOException и InterruptedException
    public void f() throws IOException, InterruptedException {}
}

class Child extends Parent {
    // а потомок пугает только потомком IOException
    @Override
    public void f() throws FileNotFoundException {}
}


Однако тут мы попытались «расширить тип» бросаемых исключений
import java.io.IOException;

public class Parent {
    public void f() throws IOException, InterruptedException {}
}

class ChildB extends Parent {
    @Override
    public void f() throws Exception {}
}

>> COMPILATION ERROR: overridden method does not throw 'java.lang.Exception'


Почему можно сужать тип, но не расширять?
Рассмотрим следующую ситуацию:
public class Parent {
    // предок пугает Exception
    public void f() throws Exception {}
}


// тут ошибка компиляции в Java, но, ДОПУСТИМ, ее нет
public class Child extends Parent {
    // потомок расширил Exception до Throwable
    public void f() throws Throwable {}
}


public class Demo {
    public static void test(Parent ref) {
        // тут все компилируется, Parent.f() пугает Exception и мы его ловим catch
        try {
            ref.f();
        } catch(Exception e) {}
    }
}


public class App {
    public static void main(String[] args) {
        // тут все компилируется, Demo.test хотел Parent и мы дали ему подтип - Child
        Demo.test(new Child());
    }
}


Внимательно посмотрите на этот пример — если бы потомок мог расширять тип бросаемого исключения предка, то те места которые «ждут» предка, а получают экземпляр «расширенного» потомка могли бы неконтролируемо выбрасывать проверяемые исключения


8. Передача свойства по наследству


Напомним иерархию исключений с расставленными флагами свойства checked/unchecked
                    Object
                      |
               Throwable(CHECKED)
               /            \
     Error(UNCHECKED)    Exception(CHECKED)
                            |
                  RuntimeException(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 можно увидеть на схеме
                    Object
                      |
               Throwable(CHECKED)
               /    |       \
 Error(UNCHECKED)   |   Exception(CHECKED)
    |       |       |      |            |
    A(UNC)  D(UNC)  |      F(C)        RuntimeException(UNCHECKED)
  /   \             |     /   \             |       |
B(UNC) C(UNC)       |   G(C)  H(C)          I(UNC)  J(UNC)  
                   E(C)                   /   \ 
                                       K(UNC) L(UNC)


Контакты



Я занимаюсь онлайн обучением Java (курсы программирования) и публикую часть учебных материалов в рамках переработки курса Java Core. Видеозаписи лекций в аудитории Вы можете увидеть на youtube-канале, возможно видео канала лучше систематизировано в этой статье.
Мой метод обучения состоит в том, что я
  1. показываю различные варианты применения
  2. строю усложняющуюся последовательность примеров по каждому варианту
  3. объясняю логику двигавшую авторами (по мере возможности)
  4. даю большое количество тестов (50-100) всесторонне проверяющее понимание и демонстрирующих различные комбинации
  5. даю лабораторные для самостоятельной работы

Данная статье следует пунктам #1 (различные варианты) и #2(последовательность примеров по каждому варианту).

skype: GolovachCourses
email: GolovachCourses@gmail.com
Автор: @IvanGolovach
GolovachCourses
рейтинг 27,76
Компания прекратила активность на сайте

Комментарии (12)

  • 0
    Спасибо! Отлично дополняет Ваш канал на Ютубе. Однако, Ваши выпускники, Иван, не должны повторять «РанЕйбл», Вы же аутсорсников готовите :)
    • 0
      Если Вы имеете в виду, что у меня просто чудовищное произношение, то да.
      Есть такая беда, не могу исправить:(
      • 0
        Да нет же! Я посмотрел десятки часов Ваших курсов, заметил 3-4 случая, не больше. (Buy как Пока, например). На вводных Вы говорите, что английский в профессии обязателен. Полностью согласен. Просто в новых видеокурсах поправьте. По мере нахождения буду отписывать в личку.
        Благодарный заказчик.
  • 0
    А еще стоит добавить, что бросание RuntimeException сильно замедляет код, т.к. при каждом бросании копирует стек вызовов весь и чтобы это победить, нужно переопределить метод fillStackTrace().
    • +1
      Господи, да что же все так привязались к медленно работе исключений?
      Сколько же раз у вас бросается исключений в секунду? 1.000, 10.000, 100.000 в секунду?
      Переписывайте такой код!
      В нормальном коде не более 10-100 бросков в секунду.
      • 0
        Например, в первой версии playframework через runtime исключения был сделан механизм render, чтобы не писать постоянно return и там это конкретно замедляло код (поэтому авторы и сделали то, что я описал выше). И можно еще придумать множество специфичных решений через unchecked exceptions, которые требуют быстрого выполнения.
        • +4
          Ваш пример говорит о криворукости программистов, а не о «медленности runtime исключений». Исключения только для исключений, строить логику на них — это проблема архитектуры.
        • 0
          Ислючения — механизм для «особенных ситуаций». Не стоит строить логику работы на исключениях.
          PlayFramework — из мира Scala. Этот язык не везде идеально ложится на JVM, отмеченный Вами момент так же (кажется) есть у встроенных в Scala акторов (реализация выхода из замыкания через бросок исключения, если я правильно выражаюсь).
          — Я полностью согласен с тем, что исключения не самый быстрый механизм работы. И что есть способы его ускорить, но такие указания для «неокрепших умов» могут толкнуть на нетипичный дизайн кода (есть отличие в том как писать методички и как писать справочники, это — скорее методичка).
          — На замечание по поводу PlayFramework — спасибо, не знал. Вы могли бы кинуть кусок кода или ссылку, где можно больше прочитать про API рендеринга?
          • 0
            — Так вы же пишете учебные статьи, поэтому и появляются дополнения. Мы знаем, что не стоит строить логику на исключениях, а разработчик, только столкнувшийся с Java, прочитав вашу статью, может и не узнать об этом.

            dim_s, вероятно, говорил про первую версию play framework, где, например, метод ok() контроллера бросал Exception.
            • 0
              >> — Так вы же пишете учебные статьи, поэтому и появляются дополнения. Мы знаем, что не стоит строить логику на исключениях, а разработчик, только столкнувшийся с Java, прочитав вашу статью, может и не узнать об этом.
              — В этом смысле — все правильно. Но я стараюсь написать то, или структурировать материал так, чего/как нет в «классических» книгах: Хорстманн, Эккель, Гослинг, Шилдт.
              Т.е. я сам свой материал рассматриваю как некоторое дополнение, а не единственный источник знаний.
    • 0
      Ну там всё относительно. Есть же пост Шипилева: habrahabr.ru/post/133399 / shipilev.net/blog/2014/exceptional-performance
  • 0
    Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).

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

Самое читаемое Разработка