Если мы хотим выкинуть ошибку нам достаточно java

Вопрос от пользователя:

Если мы хотим выкинуть ошибку в Java, нам достаточно:
создать объект класса исключения
написать throw и затем тип желаемой ошибки
написать catch и тип желаемой ошибки
создать объект класса исключения и указать его в throw

Ваш вопрос в очереди на обработку, пожалуйста подождите.

Подождите!

Не закрывайте страницу. Нейросеть пишет ответ. Он появится тут.

Этот ответ написан нейросетью.

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

Ответ нейросети:

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

Пример:

if (x < 0) {

    throw new IllegalArgumentException("x должен быть положительным");

}

В данном примере мы проверяем, что значение переменной x является положительным. Если это не так, мы выбрасываем исключение IllegalArgumentException, передавая в его конструктор строку с сообщением об ошибке. Данное исключение является одним из стандартных исключений в Java.

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

Пример:

try {

    // some code that might throw an exception

} catch (IllegalArgumentException e) {

    System.err.println("Ошибка: " + e.getMessage());

}

В данном примере мы используем блок try-catch для обработки исключения IllegalArgumentException, которое может возникнуть внутри блока try. Если исключение выбрасывается, программа продолжает работу внутри блока catch. Мы выводим сообщение об ошибке, которое передали в конструктор исключения.

Таким образом, для выбрасывания ошибки в Java нужно создать объект класса исключения и вызвать метод throw, передав в него этот объект. Для обработки ошибок нужно написать блок catch, указав тип ожидаемого исключения и действия, которые нужно выполнить при его возникновении.

⚠️Зарепортить пост!

Подождите!

Не закрывайте страницу. Нейросеть пишет ответ. Он появится тут.

Этот ответ написан нейросетью.

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

Ответ нейросети:

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

Пример:

if (x < 0) {

    throw new IllegalArgumentException("x должен быть положительным");

}

В данном примере мы проверяем, что значение переменной x является положительным. Если это не так, мы выбрасываем исключение IllegalArgumentException, передавая в его конструктор строку с сообщением об ошибке. Данное исключение является одним из стандартных исключений в Java.

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

Пример:

try {

    // some code that might throw an exception

} catch (IllegalArgumentException e) {

    System.err.println("Ошибка: " + e.getMessage());

}

В данном примере мы используем блок try-catch для обработки исключения IllegalArgumentException, которое может возникнуть внутри блока try. Если исключение выбрасывается, программа продолжает работу внутри блока catch. Мы выводим сообщение об ошибке, которое передали в конструктор исключения.

Таким образом, для выбрасывания ошибки в Java нужно создать объект класса исключения и вызвать метод throw, передав в него этот объект. Для обработки ошибок нужно написать блок catch, указав тип ожидаемого исключения и действия, которые нужно выполнить при его возникновении.

⚠️Зарепортить пост!

Время на прочтение
23 мин

Количество просмотров 419K

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

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

1. Ключевые слова: try, catch, finally, throw, throws
2. Почему используем System.err, а не System.out
3. Компилятор требует вернуть результат (или требует молчать)
4. Нелокальная передача управления (nonlocal control transfer)
5. try + catch (catch — полиморфен)
6. try + catch + catch + …
7. try + finally
8. try + catch + finally
9. Вложенные try + catch + finally

1. Ключевые слова: try, catch, finally, throw, throws

Механизм исключительных ситуаций в Java поддерживается пятью ключевыми словами

  • try
  • catch
  • finally
  • throw
  • throws

«Магия» (т.е. некоторое поведение никак не отраженное в исходном коде и потому неповторяемое пользователем) исключений #1 заключается в том, что catch, throw, throws можно использовать исключительно с java.lang.Throwable или его потомками.

throws:
Годится

public class App {
    public static void main(String[] args) throws Throwable {}
}

Не годится

public class App {
    public static void main(String[] args) throws String {}
}

>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'

catch:
Годится

public class App {
    public static void main(String[] args) {
        try {
        } catch (Throwable t) {}
    }
}

Не годится

public class App {
    public static void main(String[] args) {
        try {
        } catch (String s) {}
    }
}

>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'

throw:
Годится

public class App {
    public static void main(String[] args) {
        // Error - потомок Throwable
        throw new Error();
    }
}

Не годится

public class App {
    public static void main(String[] args) {
        throw new String("Hello!");
    }
}

>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'

Кроме того, throw требуется не-null аргумент, иначе NullPointerException в момент выполнения

public class App {
    public static void main(String[] args) {
        throw null;
    }
}

>> RUNTIME ERROR: Exception in thread "main" java.lang.NullPointerException

throw и new — это две независимых операции. В следующем коде мы независимо создаем объект исключения и «бросаем» его

public class App {
    public static void main(String[] args) {
        Error ref = new Error(); // создаем экземпляр
        throw ref;               // "бросаем" его
    }
}

>> RUNTIME ERROR: Exception in thread "main" java.lang.Error

Однако, попробуйте проанализировать вот это

public class App {
    public static void main(String[] args) {
        f(null);
    }
    public static void f(NullPointerException e) {
        try {
            throw e;
        } catch (NullPointerException npe) {
            f(npe);
        }
    }
}

>> RUNTIME ERROR: Exception in thread "main" java.lang.StackOverflowError

2. Почему используем System.err, а не System.out

System.out — buffered-поток вывода, а System.err — нет. Таким образом вывод может быть как таким

public class App {
    public static void main(String[] args) {
        System.out.println("sout");
        throw new Error();
    }
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
>> sout

Так и вот таким (err обогнало out при выводе в консоль)

public class App {
    public static void main(String[] args) {
        System.out.println("sout");
        throw new Error();
    }
}
>> sout
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error

Давайте это нарисуем

                      буфер сообщений
                    +----------------+
                 +->| msg2 msg1 msg0 | --> out 
                /   +----------------+        
               /                                 +-> +--------+
ВАШЕ ПРИЛОЖЕНИЕ                                      | КОНСОЛЬ|
                                                +-> +--------+
                                               /
                 +------------------------> err
                 нет буфера, сразу печатаем

когда Вы пишете в System.err — ваше сообщение тут же выводится на консоль, но когда пишете в System.out, то оно может на какое-то время быть буферизированно. Stacktrace необработанного исключение выводится через System.err, что позволяет им обгонять «обычные» сообщения.

3. Компилятор требует вернуть результат (или требует молчать)

Если в объявлении метода сказано, что он возвращает НЕ void, то компилятор зорко следит, что бы мы вернули экземпляр требуемого типа или экземпляр типа, который можно неявно привести к требуемому

public class App { 
    public double sqr(double arg) { // надо double
        return arg * arg;           // double * double - это double  
    }
}
public class App { 
    public double sqr(double arg) { // надо double
        int k = 1;                  // есть int
        return k;                   // можно неявно преобразовать int в double
    }
}
// на самом деле, компилятор сгенерирует байт-код для следующих исходников 
public class App { 
    public double sqr(double arg) { // надо double
        int k = 1;                  // есть int
        return (double) k;          // явное преобразование int в double
    }
}

вот так не пройдет (другой тип)

public class App {
    public static double sqr(double arg) {
        return "hello!";
    }
}

>> COMPILATION ERROR: Incompatible types. Required: double. Found: java.lang.String

Вот так не выйдет — нет возврата

public class App {
    public static double sqr(double arg) {
    }
}

>> COMPILATION ERROR: Missing return statement

и вот так не пройдет (компилятор не может удостовериться, что возврат будет)

public class App {
    public static double sqr(double arg) {
        if (System.currentTimeMillis() % 2 == 0) {
            return arg * arg; // если currentTimeMillis() - четное число, то все ОК
        }
        // а если нечетное, что нам возвращать?
    }
}

>> COMPILATION ERROR: Missing return statement

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

public class App {
    public static void main(String[] args) {
        double d = sqr(10.0); // ну, и чему равно d?
        System.out.println(d);
    }    
    public static double sqr(double arg) {
        // nothing
    }
}

>> COMPILATION ERROR: Missing return statement

Из-забавного, можно ничего не возвращать, а «повесить метод»

public class App {
    public static double sqr(double arg) {
        while (true); // Удивительно, но КОМПИЛИРУЕТСЯ!
    }
}

Тут в d никогда ничего не будет присвоено, так как метод sqr повисает

public class App {
    public static void main(String[] args) {
        double d = sqr(10.0);  // sqr - навсегда "повиснет", и 
        System.out.println(d); // d - НИКОГДА НИЧЕГО НЕ БУДЕТ ПРИСВОЕНО!
    }    
    public static double sqr(double arg) {
        while (true); // Вот тут мы на века "повисли"
    }
}

Компилятор пропустит «вилку» (таки берем в квадрат ИЛИ висим)

public class App {
    public static double sqr(double arg) {
        if (System.currentTimeMillis() % 2 == 0) {
            return arg * arg; // ну ладно, вот твой double
        } else {
            while (true);     // а тут "виснем" навсегда
        }
    }
}

Но механизм исключений позволяет НИЧЕГО НЕ ВОЗВРАЩАТЬ!

public class App {
    public static double sqr(double arg) {
        throw new RuntimeException();
    }
}

Итак, у нас есть ТРИ варианта для компилятора

public class App {
    public static double sqr(double arg) {// согласно объявлению метода ты должен вернуть double
        long time = System.currentTimeMillis();
        if (time % 2 == 0) {
            return arg * arg;             // ок, вот твой double
        } else if (time % 2 == 1) { {
            while (true);                 // не, я решил "повиснуть"
        } else {
            throw new RuntimeException(); // или бросить исключение
        }
    }
}

Но КАКОЙ ЖЕ double вернет функция, бросающая RuntimeException?
А НИКАКОЙ!

public class App {
    public static void main(String[] args) {
        // sqr - "сломается" (из него "выскочит" исключение),  
        double d = sqr(10.0);  // выполнение метода main() прервется в этой строчке и
                               // d - НИКОГДА НИЧЕГО НЕ БУДЕТ ПРИСВОЕНО!
        System.out.println(d); // и печатать нам ничего не придется!
    }    
    public static double sqr(double arg) {
        throw new RuntimeException(); // "бросаем" исключение
    }
}

>> RUNTIME ERROR: Exception in thread "main" java.lang.RuntimeException

Подытожим: бросаемое исключение — это дополнительный возвращаемый тип. Если ваш метод объявил, что возвращает double, но у вас нет double — можете бросить исключение. Если ваш метод объявил, что ничего не возвращает (void), но у вам таки есть что сказать — можете бросить исключение.

Давайте рассмотрим некоторый пример из практики.

Задача: реализовать функцию, вычисляющую площадь прямоугольника

public static int area(int width, int height) {...}

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

public static int area(int width, int height) {
    return width * height; // тут просто перемножаем
}

Вопрос: что делать, если мы обнаружили, что хотя бы один из аргументов — отрицательное число?
Если просто умножить, то мы пропустили ошибочные данные дальше. Что еще хуже, возможно, мы «исправили ситуацию» — сказали что площадь прямоугольника с двумя отрицательными сторонами -10 и -20 = 200.

Мы не можем ничего не вернуть

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        // у вас плохие аргументы, извините
    } else {
        return width * height;
    }
}

>> COMPILATION ERROR: Missing return statement

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

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        System.out.println("Bad ...");
    }
    return width * height;
}

Можно вернуть специальное значение, показывающее, что что-то не так (error code), но кто гарантирует, что его прочитают, а не просто воспользуются им?

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        return -1; // специальное "неправильное" значение площади
    }
    return width * height;
}

Можем, конечно, целиком остановить виртуальную машину

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        System.exit(0);
    }
    return width * height;
}

Но «правильный путь» таков: если обнаружили возможное некорректное поведение, то
1. Вычисления остановить, сгенерировать сообщение-поломку, которое трудно игнорировать, предоставить пользователю информацию о причине, предоставить пользователю возможность все починить (загрузить белье назад и повторно нажать кнопку старт)

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        throw new IllegalArgumentException("Negative sizes: w = " + width + ", h = " + height);
    }
    return width * height;
}

4. Нелокальная передача управления (nonlocal control transfer)

Механизм исключительных ситуация (исключений) — это механизм НЕЛОКАЛЬНОЙ ПЕРЕДАЧИ УПРАВЛЕНИЯ.
Что под этим имеется в виду?
Программа, в ходе своего выполнения (точнее исполнения инструкций в рамках отдельного потока), оперирует стеком («стопкой») фреймов. Передача управления осуществляется либо в рамках одного фрейма

public class App {
    public static void main(String[] args) {
        // Пример: ОПЕРАТОР ПОСЛЕДОВАТЕЛЬНОСТИ
        int x = 42;    // первый шаг
        int y = x * x; // второй шаг
        x = x * y;     // третий шаг
        ...
    }
}
public class App {
    public static void main(String[] args) {
        // Пример: ОПЕРАТОР ВЕТВЛЕНИЯ
        if (args.length > 2) { первый шаг
            // второй шаг или тут
            ...
        } else {
            // или тут
            ...
        }
        // третий шаг
        ...
    }
}
public class App {
    public static void main(String[] args) {
        // Пример: ОПЕРАТОР ЦИКЛА do..while
        int x = 1;      
        do {
            ...
        } while (x++ < 10);
        ...
    }
}

и другие операторы.

Либо передача управления происходит в «стопке» фреймов между СОСЕДНИМИ фреймами

  • вызов метода: создаем новый фрейм, помещаем его на верхушку стека и переходим в него
  • выход из метода: возвращаемся к предыдущему фрейму (через return или просто кончились инструкции в методе)

return — выходим из ОДНОГО фрейма (из фрейма #4(метод h()))

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        f(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println("#1.out"); // вернулись
    } // выходим из текущего фрейма, кончились инструкции

    public static void f() {
        System.err.println(".   #2.in");
        g(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   #2.out");  //вернулись
    } // выходим из текущего фрейма, кончились инструкции

    public static void g() {
        System.err.println(".   .   #3.in");
        h(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   .   #3.out"); // вернулись
    } // выходим из текущего фрейма, кончились инструкции

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.RETURN");
            return; // выходим из текущего фрейма по 'return'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСКАЕМ
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.RETURN
>> .   .   #3.out
>> .   #2.out
>> #1.out

throw — выходим из ВСЕХ фреймов

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        f(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println("#1.out"); // ПРОПУСТИЛИ!
    }

    public static void f() {
        System.err.println(".   #2.in");
        g(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   #2.out"); // ПРОПУСТИЛИ!
    }

    public static void g() {
        System.err.println(".   .   #3.in");
        h(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   .   #3.out"); // ПРОПУСТИЛИ!
    }

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.THROW");
            throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСТИЛИ!
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.THROW
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error

При помощи catch мы можем остановить летящее исключение (причина, по которой мы автоматически покидаем фреймы).
Останавливаем через 3 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g()) + фрейм #2(метод f())

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        try {
            f(); // создаем фрейм, помещаем в стек, передаем в него управление
        } catch (Error e) { // "перехватили" "летящее" исключение
            System.err.println("#1.CATCH");  // и работаем
        }
        System.err.println("#1.out");  // работаем дальше
    }

    public static void f() {
        System.err.println(".   #2.in");
        g(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   #2.out"); // ПРОПУСТИЛИ!
    }

    public static void g() {
        System.err.println(".   .   #3.in");
        h(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   .   #3.out"); // ПРОПУСТИЛИ!
    }

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.THROW");
            throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСТИЛИ!
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.THROW
>> #1.CATCH
>> #1.out

Обратите внимание, стандартный сценарий работы был восстановлен в методе main() (фрейм #1)

Останавливаем через 2 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g())

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        f(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println("#1.out"); // вернулись и работаем
    }

    public static void f() {
        System.err.println(".   #2.in");
        try {
            g(); // создаем фрейм, помещаем в стек, передаем в него управление
        } catch (Error e) { // "перехватили" "летящее" исключение
            System.err.println(".   #2.CATCH");  // и работаем
        }
        System.err.println(".   #2.out");  // работаем дальше
    }

    public static void g() {
        System.err.println(".   .   #3.in");
        h(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   .   #3.out"); // ПРОПУСТИЛИ!
    }

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.THROW");
            throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСТИЛИ!
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.THROW
>> .   #2.CATCH
>> .   #2.out
>> #1.out

Останавливаем через 1 фрейм (фактически аналог return, просто покинули фрейм «другим образом»)

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        f(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println("#1.out"); // вернулись и работаем
    }

    public static void f() {
        System.err.println(".   #2.in");
        g(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   #2.out"); // вернулись и работаем
    }

    public static void g() {
        System.err.println(".   .   #3.in");
        try {
            h(); // создаем фрейм, помещаем в стек, передаем в него управление
        } catch (Error e) { // "перехватили" "летящее" исключение
            System.err.println(".   .   #3.CATCH");  // и работаем
        }
        System.err.println(".   .   #3.out");  // работаем дальше
    }

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.THROW");
            throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСТИЛИ!
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.THROW
>> .   .   #3.CATCH
>> .   .   #3.out
>> .   #2.out
>> #1.out

Итак, давайте сведем все на одну картинку

// ---Используем RETURN--- // ---Используем THROW---
// Выход из 1-го фрейма    // Выход из ВСЕХ (из 4) фреймов
#1.in                        #1.in
.   #2.in                    .   #2.in
.   .   #3.in                .   .   #3.in
.   .   .   #4.in            .   .   .   #4.in
.   .   .   #4.RETURN        .   .   .   #4.THROW
.   .   #3.out               RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
.   #2.out                            
#1.out                              

// ---Используем THROW+CATCH---
// Выход из 3-х фреймов      // Выход из 2-х фреймов      // Выход из 1-го фрейма
#1.in                        #1.in                        #1.in
.   #2.in                    .   #2.in                    .   #2.in
.   .   #3.in                .   .   #3.in                .   .   #3.in
.   .   .   #4.in            .   .   .   #4.in            .   .   .   #4.in
.   .   .   #4.THROW         .   .   .   #4.THROW         .   .   .   #4.THROW
#1.CATCH                     .   #2.CATCH                 .   .   #3.CATCH
#1.out                       .   #2.out                   .   .   #3.out
                             #1.out                       . #2.out
                                                          #1.out

5. try + catch (catch — полиморфен)

Напомним иерархию исключений

                    Object
                      |
                  Throwable
                  /      
              Error     Exception
                            |
                    RuntimeException

То, что исключения являются объектами важно для нас в двух моментах
1. Они образуют иерархию с корнем java.lang.Throwable (java.lang.Object — предок java.lang.Throwable, но Object — уже не исключение)
2. Они могут иметь поля и методы (в этой статье это не будем использовать)

По первому пункту: catch — полиморфная конструкция, т.е. catch по типу Parent перехватывает летящие экземпляры любого типа, который является Parent-ом (т.е. экземпляры непосредственно Parent-а или любого потомка Parent-а)

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (Exception e) { // catch по Exception ПЕРЕХВАТЫВАЕТ RuntimeException
            System.err.print(" 2");
        }
        System.err.println(" 3");
    }
}

>> 0 2 3

Даже так: в блоке catch мы будем иметь ссылку типа Exception на объект типа RuntimeException

public class App {
    public static void main(String[] args) {
        try {
            throw new RuntimeException();
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                RuntimeException re = (RuntimeException) e;
                System.err.print("Это RuntimeException на самом деле!!!");              
            } else {
                System.err.print("В каком смысле не RuntimeException???");              
            }            
        }
    }
}

>> Это RuntimeException на самом деле!!!

catch по потомку не может поймать предка

public class App {
    public static void main(String[] args) throws Exception { // пока игнорируйте 'throws'
        try {
            System.err.print(" 0");
            if (true) {throw new Exception();}
            System.err.print(" 1");
        } catch (RuntimeException e) {
            System.err.print(" 2");              
        }
        System.err.print(" 3");              
    }
}

>> 0 
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Exception

catch по одному «брату» не может поймать другого «брата» (Error и Exception не находятся в отношении предок-потомок, они из параллельных веток наследования от Throwable)

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new Error();}
            System.err.print(" 1");
        } catch (Exception e) {
            System.err.print(" 2");              
        }
        System.err.print(" 3");              
    }
}

>> 0 
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error

По предыдущим примерам — надеюсь вы обратили внимание, что если исключение перехвачено, то JVM выполняет операторы идущие ПОСЛЕ последних скобок try+catch.
Но если не перехвачено, то мы
1. не заходим в блок catch
2. покидаем фрейм метода с летящим исключением

А что будет, если мы зашли в catch, и потом бросили исключение ИЗ catch?

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (RuntimeException e) {     // перехватили RuntimeException
            System.err.print(" 2");
            if (true) {throw new Error();} // но бросили Error
        }
        System.err.println(" 3");          // пропускаем - уже летит Error
    }
}

>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error

В таком случае выполнение метода тоже прерывается (не печатаем «3»). Новое исключение не имеет никакого отношения к try-catch

Мы можем даже кинуть тот объект, что у нас есть «на руках»

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (RuntimeException e) { // перехватили RuntimeException
            System.err.print(" 2");
            if (true) {throw e;}       // и бросили ВТОРОЙ раз ЕГО ЖЕ
        }
        System.err.println(" 3");      // пропускаем - опять летит RuntimeException
    }
}

>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.RuntimeException

И мы не попадем в другие секции catch, если они есть

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (RuntimeException e) {     // перехватили RuntimeException
            System.err.print(" 2");
            if (true) {throw new Error();} // и бросили новый Error
        } catch (Error e) { // хотя есть cath по Error "ниже", но мы в него не попадаем
            System.err.print(" 3");
        }
        System.err.println(" 4");
    }
}

>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error 

Обратите внимание, мы не напечатали «3», хотя у нас летит Error а «ниже» расположен catch по Error. Но важный момент в том, что catch имеет отношение исключительно к try-секции, но не к другим catch-секциям.

Как покажем ниже — можно строить вложенные конструкции, но вот пример, «исправляющий» эту ситуацию

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (RuntimeException e) { // перехватили RuntimeException
            System.err.print(" 2.1");
            try {
                System.err.print(" 2.2");
                if (true) {throw new Error();} // и бросили новый Error
                System.err.print(" 2.3");
            } catch (Throwable t) {            // перехватили Error
                System.err.print(" 2.4");                 
            }
            System.err.print(" 2.5");
        } catch (Error e) { // хотя есть cath по Error "ниже", но мы в него не попадаем
            System.err.print(" 3");
        }
        System.err.println(" 4");
    }
}

>> 0 2.1 2.2 2.4 2.5 4

6. try + catch + catch + …

Как вы видели, мы можем расположить несколько catch после одного try.

Но есть такое правило — нельзя ставить потомка после предка! (RuntimeException после Exception)

public class App {
    public static void main(String[] args) {
        try {
        } catch (Exception e) {
        } catch (RuntimeException e) {
        }
    }
}

>> COMPILATION ERROR: Exception 'java.lang.RuntimeException' has alredy been caught

Ставить брата после брата — можно (RuntimeException после Error)

public class App {
    public static void main(String[] args) {
        try {
        } catch (Error e) {
        } catch (RuntimeException e) {
        }
    }
}

Как происходит выбор «правильного» catch? Да очень просто — JVM идет сверху-вниз до тех пор, пока не найдет такой catch что в нем указано ваше исключение или его предок — туда и заходит. Ниже — не идет.

public class App {
    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (RuntimeException e) {
            System.err.println("catch RuntimeException");
        } catch (Exception e) {
            System.err.println("catch Exception");
        } catch (Throwable e) {
            System.err.println("catch Throwable");
        }
        System.err.println("next statement");
    }
}

>> catch Exception
>> next statement

Выбор catch осуществляется в runtime (а не в compile-time), значит учитывается не тип ССЫЛКИ (Throwable), а тип ССЫЛАЕМОГО (Exception)

public class App {
    public static void main(String[] args) {
        try {
            Throwable t = new Exception(); // ссылка типа Throwable указывает на объект типа Exception
            throw t;
        } catch (RuntimeException e) {
            System.err.println("catch RuntimeException");
        } catch (Exception e) {
            System.err.println("catch Exception");
        } catch (Throwable e) {
            System.err.println("catch Throwable");
        }
        System.err.println("next statement");
    }
}

>> catch Exception
>> next statement

7. try + finally

finally-секция получает управление, если try-блок завершился успешно

public class App {
    public static void main(String[] args) {
        try {
            System.err.println("try");
        } finally {
            System.err.println("finally");
        }
    }
}

>> try
>> finally

finally-секция получает управление, даже если try-блок завершился исключением

public class App {
    public static void main(String[] args) {
        try {
            throw new RuntimeException();
        } finally {
            System.err.println("finally");
        }
    }
}

>> finally
>> Exception in thread "main" java.lang.RuntimeException

finally-секция получает управление, даже если try-блок завершился директивой выхода из метода

public class App {
    public static void main(String[] args) {
        try {
            return;
        } finally {
            System.err.println("finally");
        }
    }
}

>> finally

finally-секция НЕ вызывается только если мы «прибили» JVM

public class App {
    public static void main(String[] args) {
        try {
            System.exit(42);
        } finally {
            System.err.println("finally");
        }
    }
}

>> Process finished with exit code 42

System.exit(42) и Runtime.getRuntime().exit(42) — это синонимы

public class App {
    public static void main(String[] args) {
        try {
            Runtime.getRuntime().exit(42);
        } finally {
            System.err.println("finally");
        }
    }
}

>> Process finished with exit code 42

И при Runtime.getRuntime().halt(42) — тоже не успевает зайти в finally

public class App {
    public static void main(String[] args) {
        try {
            Runtime.getRuntime().halt(42);
        } finally {
            System.err.println("finally");
        }
    }
}

>> Process finished with exit code 42

exit() vs halt()
javadoc: java.lang.Runtime#halt(int status)
… Unlike the exit method, this method does not cause shutdown hooks to be started and does not run uninvoked finalizers if finalization-on-exit has been enabled. If the shutdown sequence has already been initiated then this method does not wait for any running shutdown hooks or finalizers to finish their work.

Однако finally-секция не может «починить» try-блок завершившийся исключение (заметьте, «more» — не выводится в консоль)

public class App {
    public static void main(String[] args) {
        try {
            System.err.println("try");
            if (true) {throw new RuntimeException();}
        } finally {
            System.err.println("finally");
        }
        System.err.println("more");
    }
}

>> try
>> finally
>> Exception in thread "main" java.lang.RuntimeException

Трюк с «if (true) {…}» требуется, так как иначе компилятор обнаруживает недостижимый код (последняя строка) и отказывается его компилировать

public class App {
    public static void main(String[] args) {
        try {
            System.err.println("try");
            throw new RuntimeException();
        } finally {
            System.err.println("finally");
        }
        System.err.println("more");
    }
}

>> COMPILER ERROR: Unrechable statement 

И finally-секция не может «предотвратить» выход из метода, если try-блок вызвал return («more» — не выводится в консоль)

public class App {
    public static void main(String[] args) {
        try {
            System.err.println("try");
            if (true) {return;}
        } finally {
            System.err.println("finally");
        }
        System.err.println("more");
    }
}

>> try
>> finally

Однако finally-секция может «перебить» throw/return при помощи другого throw/return

public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        try {
            return 0;
        } finally {
            return 1;
        }
    }
}

>> 1
public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        try {
            throw new RuntimeException();
        } finally {
            return 1;
        }
    }
}

>> 1
public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        try {
            return 0;
        } finally {
            throw new RuntimeException();
        }
    }
}

>> Exception in thread "main" java.lang.RuntimeException
public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        try {
            throw new Error();
        } finally {
            throw new RuntimeException();
        }
    }
}

>> Exception in thread "main" java.lang.RuntimeException

finally-секция может быть использована для завершающего действия, которое гарантированно будет вызвано (даже если было брошено исключение или автор использовал return) по окончании работы

// open some resource
try {
    // use resource
} finally {
    // close resource
}

Например для освобождения захваченной блокировки

Lock lock = new ReentrantLock();
...
lock.lock();
try {
    // some code
} finally {
    lock.unlock();
}

Или для закрытия открытого файлового потока

InputStream input = new FileInputStream("...");
try {
    // some code
} finally {
    input.close();
}

Специально для этих целей в Java 7 появилась конструкция try-with-resources, ее мы изучим позже.

Вообще говоря, в finally-секция нельзя стандартно узнать было ли исключение.
Конечно, можно постараться написать свой «велосипед»

public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        long rnd = System.currenttimeMillis();
        boolean finished = false;
        try {
            if (rnd % 3 == 0) {
                throw new Error();
            } else if (rnd % 3 == 1) {
                throw new RuntimeException();
            } else {
                // nothing
            }
            finished = true;
        } finally {
            if (finished) {
                // не было исключений
            } else {
                // было исключение, но какое?
            }
        }
    }
}

Не рекомендуемые практики
— return из finally-секции (можем затереть исключение из try-блока)
— действия в finally-секции, которые могут бросить исключение (можем затереть исключение из try-блока)

8. try + catch + finally

Нет исключения

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            // nothing
            System.err.print(" 1");
        } catch(Error e) {
            System.err.print(" 2");
        } finally {
            System.err.print(" 3");
        }
        System.err.print(" 4");
    }
}

>> 0 1 3 4

Не заходим в catch, заходим в finally, продолжаем после оператора

Есть исключение и есть подходящий catch

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new Error();}
            System.err.print(" 1");
        } catch(Error e) {
            System.err.print(" 2");
        } finally {
            System.err.print(" 3");
        }
        System.err.print(" 4");
    }
}

>> 0 2 3 4

Заходим в catch, заходим в finally, продолжаем после оператора

Есть исключение но нет подходящего catch

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch(Error e) {
            System.err.print(" 2");
        } finally {
            System.err.print(" 3");
        }
        System.err.print(" 4");
    }
}

>> 0 3
>> RUNTIME ERROR: Exception in thread "main" java.lang.RuntimeException

Не заходим в catch, заходим в finally, не продолжаем после оператора — вылетаем с неперехваченным исключением

9. Вложенные try + catch + finally

Операторы обычно допускают неограниченное вложение.
Пример с if

public class App {
    public static void main(String[] args) {
        if (args.length > 1) {
            if (args.length > 2) {
                if (args.length > 3) {
                    ...
                }
            }
        }
    }
}

Пример с for

public class App {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; i++) {
                for (int k = 0; k < 10; k++) {
                    ...
                }
            }
        }
    }
}

Суть в том, что try-cacth-finally тоже допускает неограниченное вложение.
Например вот так

public class App {
    public static void main(String[] args) {
        try {
            try {
                try {
                    ...
                } catch (Exception e) {
                } finally {}
            } catch (Exception e) {
            } finally {}
        } catch (Exception e) {
        } finally {}
    }
}

Или даже вот так

public class App {
    public static void main(String[] args) {
        try {
            try {
                ...
            } catch (Exception e) {
                ...
            } finally {
                ...
            }
        } catch (Exception e) {
            try {
                ...
            } catch (Exception e) {
                ...
            } finally {
                ...
            }
        } finally {
            try {
                ...
            } catch (Exception e) {
                ...
            } finally {
                ...
            }
        }
    }
}

Ну что же, давайте исследуем как это работает.

Вложенный try-catch-finally без исключения

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            try {
                System.err.print(" 1");
                // НИЧЕГО
                System.err.print(" 2");
            } catch (RuntimeException e) {
                System.err.print(" 3"); // НЕ заходим - нет исключения
            } finally {                 
                System.err.print(" 4"); // заходим всегда
            }
            System.err.print(" 5");     // заходим - выполнение в норме
        } catch (Exception e) {
            System.err.print(" 6");     // НЕ заходим - нет исключения
        } finally {
            System.err.print(" 7");     // заходим всегда
        }
        System.err.print(" 8");         // заходим - выполнение в норме
    }
}

>> 0 1 2 4 5 7 8

Мы НЕ заходим в обе catch-секции (нет исключения), заходим в обе finally-секции и выполняем обе строки ПОСЛЕ finally.

Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНУТРЕННИЙ catch

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            try {
                System.err.print(" 1");
                if (true) {throw new RuntimeException();}
                System.err.print(" 2");
            } catch (RuntimeException e) {
                System.err.print(" 3"); // ЗАХОДИМ - есть исключение
            } finally {                 
                System.err.print(" 4"); // заходим всегда
            }
            System.err.print(" 5");     // заходим - выполнение УЖЕ в норме
        } catch (Exception e) {
            System.err.print(" 6");     // не заходим - нет исключения, УЖЕ перехвачено
        } finally {
            System.err.print(" 7");     // заходим всегда
        }
        System.err.print(" 8");         // заходим - выполнение УЖЕ в норме
    }
}

>> 0 1 3 4 5 7 8

Мы заходим в ПЕРВУЮ catch-секцию (печатаем «3»), но НЕ заходим во ВТОРУЮ catch-секцию (НЕ печатаем «6», так как исключение УЖЕ перехвачено первым catch), заходим в обе finally-секции (печатаем «4» и «7»), в обоих случаях выполняем код после finally (печатаем «5»и «8», так как исключение остановлено еще первым catch).

Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНЕШНИЙ catch

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            try {
                System.err.print(" 1");
                if (true) {throw new Exception();}
                System.err.print(" 2");
            } catch (RuntimeException e) {
                System.err.print(" 3"); // НЕ заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
            } finally {                 
                System.err.print(" 4"); // заходим всегда
            }
            System.err.print(" 5");     // не заходим - выполнение НЕ в норме
        } catch (Exception e) {
            System.err.print(" 6");     // ЗАХОДИМ - есть подходящее исключение
        } finally {
            System.err.print(" 7");     // заходим всегда
        }
        System.err.print(" 8");         // заходим - выполнение УЖЕ в норме
    }
}

>> 0 1 4 6 7 8

Мы НЕ заходим в ПЕРВУЮ catch-секцию (не печатаем «3»), но заходим в ВТОРУЮ catch-секцию (печатаем «6»), заходим в обе finally-секции (печатаем «4» и «7»), в ПЕРВОМ случае НЕ выполняем код ПОСЛЕ finally (не печатаем «5», так как исключение НЕ остановлено), во ВТОРОМ случае выполняем код после finally (печатаем «8», так как исключение остановлено).

Вложенный try-catch-finally с исключением, которое НИКТО НЕ ПЕРЕХВАТИТ

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            try {
                System.err.print(" 1");
                if (true) {throw new Error();}
                System.err.print(" 2");
            } catch (RuntimeException e) {
                System.err.print(" 3"); // НЕ заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
            } finally {                 
                System.err.print(" 4"); // заходим всегда
            }
            System.err.print(" 5");     // НЕ заходим - выполнение НЕ в норме
        } catch (Exception e) {
            System.err.print(" 6");     // не заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
        } finally {
            System.err.print(" 7");     // заходим всегда
        }
        System.err.print(" 8");         // не заходим - выполнение НЕ в норме
    }
}

>> 0 1 4 7
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error

Мы НЕ заходим в ОБЕ catch-секции (не печатаем «3» и «6»), заходим в обе finally-секции (печатаем «4» и «7») и в обоих случаях НЕ выполняем код ПОСЛЕ finally (не печатаем «5» и «8», так как исключение НЕ остановлено), выполнение метода прерывается по исключению.

Контакты

Я занимаюсь онлайн обучением Java (вот курсы программирования) и публикую часть учебных материалов в рамках переработки курса Java Core. Видеозаписи лекций в аудитории Вы можете увидеть на youtube-канале, возможно, видео канала лучше систематизировано в этой статье.

Мой метод обучения состоит в том, что я

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

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

skype: GolovachCourses
email: GolovachCourses@gmail.com

Catching and Handling Exceptions

Catching and Handling Exceptions

This section describes how to use the three exception handler components — the try, catch, and finally blocks — to write an exception handler. Then, the try-with-resources statement, introduced in Java SE 7, is explained. The try-with-resources statement is particularly suited to situations that use Closeable resources, such as streams.

The last part of this section walks through an example and analyzes what occurs during various scenarios.

The following example defines and implements a class named ListOfNumbers. When constructed, ListOfNumbers creates an ArrayList that contains 10 Integer elements with sequential values 0 through 9. The ListOfNumbers class also defines a method named writeList(), which writes the list of numbers into a text file called OutFile.txt. This example uses output classes defined in java.io, which are covered in the Basic I/O section.

// Note: This class will not compile yet.
import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class ListOfNumbers {

    private List<Integer> list;
    private static final int SIZE = 10;

    public ListOfNumbers () {
        list = new ArrayList<>(SIZE);
        for (int i = 0; i < SIZE; i++) {
            list.add(i);
        }
    }

    public void writeList() {
    // The FileWriter constructor throws IOException, which must be caught.
        PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));

        for (int i = 0; i < SIZE; i++) {
            // The get(int) method throws IndexOutOfBoundsException, which must be caught.
            out.println("Value at: " + i + " = " + list.get(i));
        }
        out.close();
    }
}

The first line in boldface is a call to a constructor. The constructor initializes an output stream on a file. If the file cannot be opened, the constructor throws an IOException. The second boldface line is a call to the ArrayList class’s get method, which throws an IndexOutOfBoundsException if the value of its argument is too small (less than 0) or too large (more than the number of elements currently contained by the ArrayList.

If you try to compile the ListOfNumbers class, the compiler prints an error message about the exception thrown by the FileWriter constructor. However, it does not display an error message about the exception thrown by get(). The reason is that the exception thrown by the constructor, IOException, is a checked exception, and the one thrown by the get() method, IndexOutOfBoundsException, is an unchecked exception.

Now that you’re familiar with the ListOfNumbers class and where the exceptions can be thrown within it, you’re ready to write exception handlers to catch and handle those exceptions.

The Try Block

The first step in constructing an exception handler is to enclose the code that might throw an exception within a try block. In general, a try block looks like the following:

try {
    code
}
catch and finally blocks . . .

The segment in the example labeled code contains one or more legal lines of code that could throw an exception. (The catch and finally blocks are explained in the next two subsections.)

To construct an exception handler for the writeList() method from the ListOfNumbers class, enclose the exception-throwing statements of the writeList() method within a try block. There is more than one way to do this. You can put each line of code that might throw an exception within its own try block and provide separate exception handlers for each. Or, you can put all the writeList() code within a single try block and associate multiple handlers with it. The following listing uses one try block for the entire method because the code in question is very short.

private List<Integer> list;
private static final int SIZE = 10;

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("Entered try statement");
        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    }
    catch and finally blocks  . . .
}

If an exception occurs within the try block, that exception is handled by an exception handler associated with it. To associate an exception handler with a try block, you must put a catch block after it; the next section, The catch Blocks, shows you how.

The Catch Blocks

You associate exception handlers with a try block by providing one or more catch blocks directly after the try block. No code can be between the end of the try block and the beginning of the first catch block.

try {

} catch (ExceptionType name) {

} catch (ExceptionType name) {

}

Each catch block is an exception handler that handles the type of exception indicated by its argument. The argument type, ExceptionType, declares the type of exception that the handler can handle and must be the name of a class that inherits from the Throwable class. The handler can refer to the exception with name.

The catch block contains code that is executed if and when the exception handler is invoked. The runtime system invokes the exception handler when the handler is the first one in the call stack whose ExceptionType matches the type of the exception thrown. The system considers it a match if the thrown object can legally be assigned to the exception handler’s argument.

The following are two exception handlers for the writeList() method:

try {

} catch (IndexOutOfBoundsException e) {
    System.err.println("IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
    System.err.println("Caught IOException: " + e.getMessage());
}

Exception handlers can do more than just print error messages or halt the program. They can do error recovery, prompt the user to make a decision, or propagate the error up to a higher-level handler using chained exceptions, as described in the Chained Exceptions section.

Multi-Catching Exceptions

You can catch more than one type of exception with one exception handler, with the multi-catch pattern.

In Java SE 7 and later, a single catch block can handle more than one type of exception. This feature can reduce code duplication and lessen the temptation to catch an overly broad exception.

In the catch clause, specify the types of exceptions that block can handle, and separate each exception type with a vertical bar (|):

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

Note: If a catch block handles more than one exception type, then the catch parameter is implicitly final. In this example, the catch parameter ex is final and therefore you cannot assign any values to it within the catch block.

The Finally Block

The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return, continue, or break. Putting cleanup code in a finally block is always a good practice, even when no exceptions are anticipated.

Note: If the JVM exits while the try or catch code is being executed, then the finally block may not execute.

The try block of the writeList() method that you’ve been working with here opens a PrintWriter. The program should close that stream before exiting the writeList() method. This poses a somewhat complicated problem because writeList()‘s try block can exit in one of three ways.

  1. The new FileWriter statement fails and throws an IOException.
  2. The list.get(i) statement fails and throws an IndexOutOfBoundsException.
  3. Everything succeeds and the try block exits normally.

The runtime system always executes the statements within the finally block regardless of what happens within the try block. So it’s the perfect place to perform cleanup.

The following finally block for the writeList() method cleans up and then closes the PrintWriter.

finally {
    if (out != null) {
        System.out.println("Closing PrintWriter");
        out.close();
    } else {
        System.out.println("PrintWriter not open");
    }
}

Important: The finally block is a key tool for preventing resource leaks. When closing a file or otherwise recovering resources, place the code in a finally block to ensure that resource is always recovered.

Consider using the try-with-resources statement in these situations, which automatically releases system resources when no longer needed. The try-with-resources Statement section has more information.

The Try-with-resources Statement

The try-with-resources statement is a try statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.

The following example reads the first line from a file. It uses an instance of BufferedReader to read data from the file. BufferedReader is a resource that must be closed after the program is finished with it:

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
                   new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

In this example, the resource declared in the try-with-resources statement is a BufferedReader. The declaration statement appears within parentheses immediately after the try keyword. The class BufferedReader, in Java SE 7 and later, implements the interface java.lang.AutoCloseable. Because the BufferedReader instance is declared in a try-with-resource statement, it will be closed regardless of whether the try statement completes normally or abruptly (as a result of the method BufferedReader.readLine() throwing an IOException.

Prior to Java SE 7, you can use a finally block to ensure that a resource is closed regardless of whether the try statement completes normally or abruptly. The following example uses a finally block instead of a try-with-resources statement:

static String readFirstLineFromFileWithFinallyBlock(String path)
                                                     throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}

However, in this example, if the methods readLine() and close both throw exceptions, then the method readFirstLineFromFileWithFinallyBlock() throws the exception thrown from the finally block; the exception thrown from the try block is suppressed. In contrast, in the example readFirstLineFromFile(), if exceptions are thrown from both the try block and the try-with-resources statement, then the method readFirstLineFromFile() throws the exception thrown from the try block; the exception thrown from the try-with-resources block is suppressed. In Java SE 7 and later, you can retrieve suppressed exceptions; see the section Suppressed Exceptions for more information.

You may declare one or more resources in a try-with-resources statement. The following example retrieves the names of the files packaged in the zip file zipFileName and creates a text file that contains the names of these files:

public static void writeToFileZipFileContents(String zipFileName,
                                           String outputFileName)
                                           throws java.io.IOException {

    java.nio.charset.Charset charset =
         java.nio.charset.StandardCharsets.US_ASCII;
    java.nio.file.Path outputFilePath =
         java.nio.file.Paths.get(outputFileName);

    // Open zip file and create output file with
    // try-with-resources statement

    try (
        java.util.zip.ZipFile zf =
             new java.util.zip.ZipFile(zipFileName);
        java.io.BufferedWriter writer =
            java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
    ) {
        // Enumerate each entry
        for (java.util.Enumeration entries =
                                zf.entries(); entries.hasMoreElements();) {
            // Get the entry name and write it to the output file
            String newLine = System.getProperty("line.separator");
            String zipEntryName =
                 ((java.util.zip.ZipEntry)entries.nextElement()).getName() +
                 newLine;
            writer.write(zipEntryName, 0, zipEntryName.length());
        }
    }
}

In this example, the try-with-resources statement contains two declarations that are separated by a semicolon: ZipFile and BufferedWriter. When the block of code that directly follows it terminates, either normally or because of an exception, the close() methods of the BufferedWriter and ZipFile objects are automatically called in this order. Note that the close methods of resources are called in the opposite order of their creation.

The following example uses a try-with-resources statement to automatically close a java.sql.Statement object:

public static void viewTable(Connection con) throws SQLException {

    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";

    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);

        while (rs.next()) {
            String coffeeName = rs.getString("COF_NAME");
            int supplierID = rs.getInt("SUP_ID");
            float price = rs.getFloat("PRICE");
            int sales = rs.getInt("SALES");
            int total = rs.getInt("TOTAL");

            System.out.println(coffeeName + ", " + supplierID + ", " +
                               price + ", " + sales + ", " + total);
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
}

The resource java.sql.Statement used in this example is part of the JDBC 4.1 and later API.

Note: A try-with-resources statement can have catch and finally blocks just like an ordinary try statement. In a try-with-resources statement, any catch or finally block is run after the resources declared have been closed.

Suppressed Exceptions

An exception can be thrown from the block of code associated with the try-with-resources statement. In the example writeToFileZipFileContents(), an exception can be thrown from the try block, and up to two exceptions can be thrown from the try-with-resources statement when it tries to close the ZipFile and BufferedWriter objects. If an exception is thrown from the try block and one or more exceptions are thrown from the try-with-resources statement, then those exceptions thrown from the try-with-resources statement are suppressed, and the exception thrown by the block is the one that is thrown by the writeToFileZipFileContents() method. You can retrieve these suppressed exceptions by calling the Throwable.getSuppressed() method from the exception thrown by the try block.

Classes That Implement the AutoCloseable or Closeable Interface

See the Javadoc of the AutoCloseable and Closeable interfaces for a list of classes that implement either of these interfaces. The Closeable interface extends the AutoCloseable interface. The close() method of the Closeable interface throws exceptions of type IOException while the close() method of the AutoCloseable interface throws exceptions of type Exception. Consequently, subclasses of the AutoCloseable interface can override this behavior of the close() method to throw specialized exceptions, such as IOException, or no exception at all.

Putting It All Together

The previous sections described how to construct the try, catch, and finally code blocks for the writeList() method in the ListOfNumbers class. Now, let’s walk through the code and investigate what can happen.

When all the components are put together, the writeList() method looks like the following.

public void writeList() {
    PrintWriter out = null;

    try {
        System.out.println("Entering" + " try statement");

        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    } catch (IndexOutOfBoundsException e) {
        System.err.println("Caught IndexOutOfBoundsException: "
                           +  e.getMessage());

    } catch (IOException e) {
        System.err.println("Caught IOException: " +  e.getMessage());

    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        }
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

As mentioned previously, this method’s try block has three different exit possibilities; here are two of them.

  1. Code in the try statement fails and throws an exception. This could be an IOException caused by the new FileWriter statement or an IndexOutOfBoundsException caused by a wrong index value in the for loop.
  2. Everything succeeds and the try statement exits normally.

Let’s look at what happens in the writeList() method during these two exit possibilities.

Scenario 1: An Exception Occurs

The statement that creates a FileWriter can fail for a number of reasons. For example, the constructor for the FileWriter throws an IOException if the program cannot create or write to the file indicated.

When FileWriter throws an IOException, the runtime system immediately stops executing the try block; method calls being executed are not completed. The runtime system then starts searching at the top of the method call stack for an appropriate exception handler. In this example, when the IOException occurs, the FileWriter constructor is at the top of the call stack. However, the FileWriter constructor doesn’t have an appropriate exception handler, so the runtime system checks the next method — the writeList() method — in the method call stack. The writeList() method has two exception handlers: one for IOException and one for IndexOutOfBoundsException.

The runtime system checks writeList()‘s handlers in the order in which they appear after the try statement. The argument to the first exception handler is IndexOutOfBoundsException. This does not match the type of exception thrown, so the runtime system checks the next exception handler — IOException. This matches the type of exception that was thrown, so the runtime system ends its search for an appropriate exception handler. Now that the runtime has found an appropriate handler, the code in that catch block is executed.

After the exception handler executes, the runtime system passes control to the finally block. Code in the finally block executes regardless of the exception caught above it. In this scenario, the FileWriter was never opened and doesn’t need to be closed. After the finally block finishes executing, the program continues with the first statement after the finally block.

Here’s the complete output from the ListOfNumbers program that appears when an IOException is thrown.

Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open

Scenario 2: The try Block Exits Normally

In this scenario, all the statements within the scope of the try block execute successfully and throw no exceptions. Execution falls off the end of the try block, and the runtime system passes control to the finally block. Because everything was successful, the PrintWriter is open when control reaches the finally block, which closes the PrintWriter. Again, after the finally block finishes executing, the program continues with the first statement after the finally block.

Here is the output from the ListOfNumbers program when no exceptions are thrown.

Entering try statement
Closing PrintWriter

Catching and Handling Exceptions

Обзор

Обработка исключений в 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.

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

Автор оригинала: 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 будет недоступен и вызовет ошибку компиляции.

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

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

попробуйте-поймайте блоки

Более распространенным подходом было бы использовать trycatch блок для перехвата и обработки возникающего исключения:

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-адреса приведет к:

наконец, Блоки

Вводя новый тип блока, блок наконец выполняется независимо от того, что происходит в блоке 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];
    }
  }
}

Вызов этого метода с допустимым индексом приведет к:

Но вызов этого метода с индексом, который выходит за рамки, приведет к:

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

Используйте попытку с ресурсами

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

Закройте ресурсы в 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.

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

Время на прочтение
23 мин

Количество просмотров 424K

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

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

1. Ключевые слова: try, catch, finally, throw, throws
2. Почему используем System.err, а не System.out
3. Компилятор требует вернуть результат (или требует молчать)
4. Нелокальная передача управления (nonlocal control transfer)
5. try + catch (catch — полиморфен)
6. try + catch + catch + …
7. try + finally
8. try + catch + finally
9. Вложенные try + catch + finally

1. Ключевые слова: try, catch, finally, throw, throws

Механизм исключительных ситуаций в Java поддерживается пятью ключевыми словами

  • try
  • catch
  • finally
  • throw
  • throws

«Магия» (т.е. некоторое поведение никак не отраженное в исходном коде и потому неповторяемое пользователем) исключений #1 заключается в том, что catch, throw, throws можно использовать исключительно с java.lang.Throwable или его потомками.

throws:
Годится

public class App {
    public static void main(String[] args) throws Throwable {}
}

Не годится

public class App {
    public static void main(String[] args) throws String {}
}

>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'

catch:
Годится

public class App {
    public static void main(String[] args) {
        try {
        } catch (Throwable t) {}
    }
}

Не годится

public class App {
    public static void main(String[] args) {
        try {
        } catch (String s) {}
    }
}

>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'

throw:
Годится

public class App {
    public static void main(String[] args) {
        // Error - потомок Throwable
        throw new Error();
    }
}

Не годится

public class App {
    public static void main(String[] args) {
        throw new String("Hello!");
    }
}

>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'

Кроме того, throw требуется не-null аргумент, иначе NullPointerException в момент выполнения

public class App {
    public static void main(String[] args) {
        throw null;
    }
}

>> RUNTIME ERROR: Exception in thread "main" java.lang.NullPointerException

throw и new — это две независимых операции. В следующем коде мы независимо создаем объект исключения и «бросаем» его

public class App {
    public static void main(String[] args) {
        Error ref = new Error(); // создаем экземпляр
        throw ref;               // "бросаем" его
    }
}

>> RUNTIME ERROR: Exception in thread "main" java.lang.Error

Однако, попробуйте проанализировать вот это

public class App {
    public static void main(String[] args) {
        f(null);
    }
    public static void f(NullPointerException e) {
        try {
            throw e;
        } catch (NullPointerException npe) {
            f(npe);
        }
    }
}

>> RUNTIME ERROR: Exception in thread "main" java.lang.StackOverflowError

2. Почему используем System.err, а не System.out

System.out — buffered-поток вывода, а System.err — нет. Таким образом вывод может быть как таким

public class App {
    public static void main(String[] args) {
        System.out.println("sout");
        throw new Error();
    }
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
>> sout

Так и вот таким (err обогнало out при выводе в консоль)

public class App {
    public static void main(String[] args) {
        System.out.println("sout");
        throw new Error();
    }
}
>> sout
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error

Давайте это нарисуем

                      буфер сообщений
                    +----------------+
                 +->| msg2 msg1 msg0 | --> out 
                /   +----------------+        \
               /                                 +-> +--------+
ВАШЕ ПРИЛОЖЕНИЕ                                      | КОНСОЛЬ|
               \                                 +-> +--------+
                \                               /
                 +------------------------> err
                 нет буфера, сразу печатаем

когда Вы пишете в System.err — ваше сообщение тут же выводится на консоль, но когда пишете в System.out, то оно может на какое-то время быть буферизированно. Stacktrace необработанного исключение выводится через System.err, что позволяет им обгонять «обычные» сообщения.

3. Компилятор требует вернуть результат (или требует молчать)

Если в объявлении метода сказано, что он возвращает НЕ void, то компилятор зорко следит, что бы мы вернули экземпляр требуемого типа или экземпляр типа, который можно неявно привести к требуемому

public class App { 
    public double sqr(double arg) { // надо double
        return arg * arg;           // double * double - это double  
    }
}

public class App { 
    public double sqr(double arg) { // надо double
        int k = 1;                  // есть int
        return k;                   // можно неявно преобразовать int в double
    }
}

// на самом деле, компилятор сгенерирует байт-код для следующих исходников 
public class App { 
    public double sqr(double arg) { // надо double
        int k = 1;                  // есть int
        return (double) k;          // явное преобразование int в double
    }
}

вот так не пройдет (другой тип)

public class App {
    public static double sqr(double arg) {
        return "hello!";
    }
}

>> COMPILATION ERROR: Incompatible types. Required: double. Found: java.lang.String

Вот так не выйдет — нет возврата

public class App {
    public static double sqr(double arg) {
    }
}

>> COMPILATION ERROR: Missing return statement

и вот так не пройдет (компилятор не может удостовериться, что возврат будет)

public class App {
    public static double sqr(double arg) {
        if (System.currentTimeMillis() % 2 == 0) {
            return arg * arg; // если currentTimeMillis() - четное число, то все ОК
        }
        // а если нечетное, что нам возвращать?
    }
}

>> COMPILATION ERROR: Missing return statement

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

public class App {
    public static void main(String[] args) {
        double d = sqr(10.0); // ну, и чему равно d?
        System.out.println(d);
    }    
    public static double sqr(double arg) {
        // nothing
    }
}

>> COMPILATION ERROR: Missing return statement

Из-забавного, можно ничего не возвращать, а «повесить метод»

public class App {
    public static double sqr(double arg) {
        while (true); // Удивительно, но КОМПИЛИРУЕТСЯ!
    }
}

Тут в d никогда ничего не будет присвоено, так как метод sqr повисает

public class App {
    public static void main(String[] args) {
        double d = sqr(10.0);  // sqr - навсегда "повиснет", и 
        System.out.println(d); // d - НИКОГДА НИЧЕГО НЕ БУДЕТ ПРИСВОЕНО!
    }    
    public static double sqr(double arg) {
        while (true); // Вот тут мы на века "повисли"
    }
}

Компилятор пропустит «вилку» (таки берем в квадрат ИЛИ висим)

public class App {
    public static double sqr(double arg) {
        if (System.currentTimeMillis() % 2 == 0) {
            return arg * arg; // ну ладно, вот твой double
        } else {
            while (true);     // а тут "виснем" навсегда
        }
    }
}

Но механизм исключений позволяет НИЧЕГО НЕ ВОЗВРАЩАТЬ!

public class App {
    public static double sqr(double arg) {
        throw new RuntimeException();
    }
}

Итак, у нас есть ТРИ варианта для компилятора

public class App {
    public static double sqr(double arg) {// согласно объявлению метода ты должен вернуть double
        long time = System.currentTimeMillis();
        if (time % 2 == 0) {
            return arg * arg;             // ок, вот твой double
        } else if (time % 2 == 1) { {
            while (true);                 // не, я решил "повиснуть"
        } else {
            throw new RuntimeException(); // или бросить исключение
        }
    }
}

Но КАКОЙ ЖЕ double вернет функция, бросающая RuntimeException?
А НИКАКОЙ!

public class App {
    public static void main(String[] args) {
        // sqr - "сломается" (из него "выскочит" исключение),  
        double d = sqr(10.0);  // выполнение метода main() прервется в этой строчке и
                               // d - НИКОГДА НИЧЕГО НЕ БУДЕТ ПРИСВОЕНО!
        System.out.println(d); // и печатать нам ничего не придется!
    }    
    public static double sqr(double arg) {
        throw new RuntimeException(); // "бросаем" исключение
    }
}

>> RUNTIME ERROR: Exception in thread "main" java.lang.RuntimeException

Подытожим: бросаемое исключение — это дополнительный возвращаемый тип. Если ваш метод объявил, что возвращает double, но у вас нет double — можете бросить исключение. Если ваш метод объявил, что ничего не возвращает (void), но у вам таки есть что сказать — можете бросить исключение.

Давайте рассмотрим некоторый пример из практики.

Задача: реализовать функцию, вычисляющую площадь прямоугольника

public static int area(int width, int height) {...}

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

public static int area(int width, int height) {
    return width * height; // тут просто перемножаем
}

Вопрос: что делать, если мы обнаружили, что хотя бы один из аргументов — отрицательное число?
Если просто умножить, то мы пропустили ошибочные данные дальше. Что еще хуже, возможно, мы «исправили ситуацию» — сказали что площадь прямоугольника с двумя отрицательными сторонами -10 и -20 = 200.

Мы не можем ничего не вернуть

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        // у вас плохие аргументы, извините
    } else {
        return width * height;
    }
}

>> COMPILATION ERROR: Missing return statement

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

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        System.out.println("Bad ...");
    }
    return width * height;
}

Можно вернуть специальное значение, показывающее, что что-то не так (error code), но кто гарантирует, что его прочитают, а не просто воспользуются им?

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        return -1; // специальное "неправильное" значение площади
    }
    return width * height;
}

Можем, конечно, целиком остановить виртуальную машину

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        System.exit(0);
    }
    return width * height;
}

Но «правильный путь» таков: если обнаружили возможное некорректное поведение, то
1. Вычисления остановить, сгенерировать сообщение-поломку, которое трудно игнорировать, предоставить пользователю информацию о причине, предоставить пользователю возможность все починить (загрузить белье назад и повторно нажать кнопку старт)

public static int area(int width, int height) {
    if (width < 0 || height < 0) {
        throw new IllegalArgumentException("Negative sizes: w = " + width + ", h = " + height);
    }
    return width * height;
}

4. Нелокальная передача управления (nonlocal control transfer)

Механизм исключительных ситуация (исключений) — это механизм НЕЛОКАЛЬНОЙ ПЕРЕДАЧИ УПРАВЛЕНИЯ.
Что под этим имеется в виду?
Программа, в ходе своего выполнения (точнее исполнения инструкций в рамках отдельного потока), оперирует стеком («стопкой») фреймов. Передача управления осуществляется либо в рамках одного фрейма

public class App {
    public static void main(String[] args) {
        // Пример: ОПЕРАТОР ПОСЛЕДОВАТЕЛЬНОСТИ
        int x = 42;    // первый шаг
        int y = x * x; // второй шаг
        x = x * y;     // третий шаг
        ...
    }
}

public class App {
    public static void main(String[] args) {
        // Пример: ОПЕРАТОР ВЕТВЛЕНИЯ
        if (args.length > 2) { первый шаг
            // второй шаг или тут
            ...
        } else {
            // или тут
            ...
        }
        // третий шаг
        ...
    }
}

public class App {
    public static void main(String[] args) {
        // Пример: ОПЕРАТОР ЦИКЛА do..while
        int x = 1;      
        do {
            ...
        } while (x++ < 10);
        ...
    }
}

и другие операторы.

Либо передача управления происходит в «стопке» фреймов между СОСЕДНИМИ фреймами

  • вызов метода: создаем новый фрейм, помещаем его на верхушку стека и переходим в него
  • выход из метода: возвращаемся к предыдущему фрейму (через return или просто кончились инструкции в методе)

return — выходим из ОДНОГО фрейма (из фрейма #4(метод h()))

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        f(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println("#1.out"); // вернулись
    } // выходим из текущего фрейма, кончились инструкции

    public static void f() {
        System.err.println(".   #2.in");
        g(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   #2.out");  //вернулись
    } // выходим из текущего фрейма, кончились инструкции

    public static void g() {
        System.err.println(".   .   #3.in");
        h(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   .   #3.out"); // вернулись
    } // выходим из текущего фрейма, кончились инструкции

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.RETURN");
            return; // выходим из текущего фрейма по 'return'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСКАЕМ
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.RETURN
>> .   .   #3.out
>> .   #2.out
>> #1.out

throw — выходим из ВСЕХ фреймов

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        f(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println("#1.out"); // ПРОПУСТИЛИ!
    }

    public static void f() {
        System.err.println(".   #2.in");
        g(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   #2.out"); // ПРОПУСТИЛИ!
    }

    public static void g() {
        System.err.println(".   .   #3.in");
        h(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   .   #3.out"); // ПРОПУСТИЛИ!
    }

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.THROW");
            throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСТИЛИ!
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.THROW
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error

При помощи catch мы можем остановить летящее исключение (причина, по которой мы автоматически покидаем фреймы).
Останавливаем через 3 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g()) + фрейм #2(метод f())

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        try {
            f(); // создаем фрейм, помещаем в стек, передаем в него управление
        } catch (Error e) { // "перехватили" "летящее" исключение
            System.err.println("#1.CATCH");  // и работаем
        }
        System.err.println("#1.out");  // работаем дальше
    }

    public static void f() {
        System.err.println(".   #2.in");
        g(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   #2.out"); // ПРОПУСТИЛИ!
    }

    public static void g() {
        System.err.println(".   .   #3.in");
        h(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   .   #3.out"); // ПРОПУСТИЛИ!
    }

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.THROW");
            throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСТИЛИ!
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.THROW
>> #1.CATCH
>> #1.out

Обратите внимание, стандартный сценарий работы был восстановлен в методе main() (фрейм #1)

Останавливаем через 2 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g())

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        f(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println("#1.out"); // вернулись и работаем
    }

    public static void f() {
        System.err.println(".   #2.in");
        try {
            g(); // создаем фрейм, помещаем в стек, передаем в него управление
        } catch (Error e) { // "перехватили" "летящее" исключение
            System.err.println(".   #2.CATCH");  // и работаем
        }
        System.err.println(".   #2.out");  // работаем дальше
    }

    public static void g() {
        System.err.println(".   .   #3.in");
        h(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   .   #3.out"); // ПРОПУСТИЛИ!
    }

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.THROW");
            throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСТИЛИ!
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.THROW
>> .   #2.CATCH
>> .   #2.out
>> #1.out

Останавливаем через 1 фрейм (фактически аналог return, просто покинули фрейм «другим образом»)

public class App {
    public static void main(String[] args) {
        System.err.println("#1.in");
        f(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println("#1.out"); // вернулись и работаем
    }

    public static void f() {
        System.err.println(".   #2.in");
        g(); // создаем фрейм, помещаем в стек, передаем в него управление
        System.err.println(".   #2.out"); // вернулись и работаем
    }

    public static void g() {
        System.err.println(".   .   #3.in");
        try {
            h(); // создаем фрейм, помещаем в стек, передаем в него управление
        } catch (Error e) { // "перехватили" "летящее" исключение
            System.err.println(".   .   #3.CATCH");  // и работаем
        }
        System.err.println(".   .   #3.out");  // работаем дальше
    }

    public static void h() {
        System.err.println(".   .   .   #4.in");
        if (true) {
            System.err.println(".   .   .   #4.THROW");
            throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
        }
        System.err.println(".   .   .   #4.out"); // ПРОПУСТИЛИ!
    }
}

>> #1.in
>> .   #2.in
>> .   .   #3.in
>> .   .   .   #4.in
>> .   .   .   #4.THROW
>> .   .   #3.CATCH
>> .   .   #3.out
>> .   #2.out
>> #1.out

Итак, давайте сведем все на одну картинку

// ---Используем RETURN--- // ---Используем THROW---
// Выход из 1-го фрейма    // Выход из ВСЕХ (из 4) фреймов
#1.in                        #1.in
.   #2.in                    .   #2.in
.   .   #3.in                .   .   #3.in
.   .   .   #4.in            .   .   .   #4.in
.   .   .   #4.RETURN        .   .   .   #4.THROW
.   .   #3.out               RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
.   #2.out                            
#1.out                              

// ---Используем THROW+CATCH---
// Выход из 3-х фреймов      // Выход из 2-х фреймов      // Выход из 1-го фрейма
#1.in                        #1.in                        #1.in
.   #2.in                    .   #2.in                    .   #2.in
.   .   #3.in                .   .   #3.in                .   .   #3.in
.   .   .   #4.in            .   .   .   #4.in            .   .   .   #4.in
.   .   .   #4.THROW         .   .   .   #4.THROW         .   .   .   #4.THROW
#1.CATCH                     .   #2.CATCH                 .   .   #3.CATCH
#1.out                       .   #2.out                   .   .   #3.out
                             #1.out                       . #2.out
                                                          #1.out

5. try + catch (catch — полиморфен)

Напомним иерархию исключений

                    Object
                      |
                  Throwable
                  /      \
              Error     Exception
                            |
                    RuntimeException

То, что исключения являются объектами важно для нас в двух моментах
1. Они образуют иерархию с корнем java.lang.Throwable (java.lang.Object — предок java.lang.Throwable, но Object — уже не исключение)
2. Они могут иметь поля и методы (в этой статье это не будем использовать)

По первому пункту: catch — полиморфная конструкция, т.е. catch по типу Parent перехватывает летящие экземпляры любого типа, который является Parent-ом (т.е. экземпляры непосредственно Parent-а или любого потомка Parent-а)

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (Exception e) { // catch по Exception ПЕРЕХВАТЫВАЕТ RuntimeException
            System.err.print(" 2");
        }
        System.err.println(" 3");
    }
}

>> 0 2 3

Даже так: в блоке catch мы будем иметь ссылку типа Exception на объект типа RuntimeException

public class App {
    public static void main(String[] args) {
        try {
            throw new RuntimeException();
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                RuntimeException re = (RuntimeException) e;
                System.err.print("Это RuntimeException на самом деле!!!");              
            } else {
                System.err.print("В каком смысле не RuntimeException???");              
            }            
        }
    }
}

>> Это RuntimeException на самом деле!!!

catch по потомку не может поймать предка

public class App {
    public static void main(String[] args) throws Exception { // пока игнорируйте 'throws'
        try {
            System.err.print(" 0");
            if (true) {throw new Exception();}
            System.err.print(" 1");
        } catch (RuntimeException e) {
            System.err.print(" 2");              
        }
        System.err.print(" 3");              
    }
}

>> 0 
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Exception

catch по одному «брату» не может поймать другого «брата» (Error и Exception не находятся в отношении предок-потомок, они из параллельных веток наследования от Throwable)

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new Error();}
            System.err.print(" 1");
        } catch (Exception e) {
            System.err.print(" 2");              
        }
        System.err.print(" 3");              
    }
}

>> 0 
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error

По предыдущим примерам — надеюсь вы обратили внимание, что если исключение перехвачено, то JVM выполняет операторы идущие ПОСЛЕ последних скобок try+catch.
Но если не перехвачено, то мы
1. не заходим в блок catch
2. покидаем фрейм метода с летящим исключением

А что будет, если мы зашли в catch, и потом бросили исключение ИЗ catch?

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (RuntimeException e) {     // перехватили RuntimeException
            System.err.print(" 2");
            if (true) {throw new Error();} // но бросили Error
        }
        System.err.println(" 3");          // пропускаем - уже летит Error
    }
}

>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error

В таком случае выполнение метода тоже прерывается (не печатаем «3»). Новое исключение не имеет никакого отношения к try-catch

Мы можем даже кинуть тот объект, что у нас есть «на руках»

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (RuntimeException e) { // перехватили RuntimeException
            System.err.print(" 2");
            if (true) {throw e;}       // и бросили ВТОРОЙ раз ЕГО ЖЕ
        }
        System.err.println(" 3");      // пропускаем - опять летит RuntimeException
    }
}

>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.RuntimeException

И мы не попадем в другие секции catch, если они есть

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (RuntimeException e) {     // перехватили RuntimeException
            System.err.print(" 2");
            if (true) {throw new Error();} // и бросили новый Error
        } catch (Error e) { // хотя есть cath по Error "ниже", но мы в него не попадаем
            System.err.print(" 3");
        }
        System.err.println(" 4");
    }
}

>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error 

Обратите внимание, мы не напечатали «3», хотя у нас летит Error а «ниже» расположен catch по Error. Но важный момент в том, что catch имеет отношение исключительно к try-секции, но не к другим catch-секциям.

Как покажем ниже — можно строить вложенные конструкции, но вот пример, «исправляющий» эту ситуацию

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch (RuntimeException e) { // перехватили RuntimeException
            System.err.print(" 2.1");
            try {
                System.err.print(" 2.2");
                if (true) {throw new Error();} // и бросили новый Error
                System.err.print(" 2.3");
            } catch (Throwable t) {            // перехватили Error
                System.err.print(" 2.4");                 
            }
            System.err.print(" 2.5");
        } catch (Error e) { // хотя есть cath по Error "ниже", но мы в него не попадаем
            System.err.print(" 3");
        }
        System.err.println(" 4");
    }
}

>> 0 2.1 2.2 2.4 2.5 4

6. try + catch + catch + …

Как вы видели, мы можем расположить несколько catch после одного try.

Но есть такое правило — нельзя ставить потомка после предка! (RuntimeException после Exception)

public class App {
    public static void main(String[] args) {
        try {
        } catch (Exception e) {
        } catch (RuntimeException e) {
        }
    }
}

>> COMPILATION ERROR: Exception 'java.lang.RuntimeException' has alredy been caught

Ставить брата после брата — можно (RuntimeException после Error)

public class App {
    public static void main(String[] args) {
        try {
        } catch (Error e) {
        } catch (RuntimeException e) {
        }
    }
}

Как происходит выбор «правильного» catch? Да очень просто — JVM идет сверху-вниз до тех пор, пока не найдет такой catch что в нем указано ваше исключение или его предок — туда и заходит. Ниже — не идет.

public class App {
    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (RuntimeException e) {
            System.err.println("catch RuntimeException");
        } catch (Exception e) {
            System.err.println("catch Exception");
        } catch (Throwable e) {
            System.err.println("catch Throwable");
        }
        System.err.println("next statement");
    }
}

>> catch Exception
>> next statement

Выбор catch осуществляется в runtime (а не в compile-time), значит учитывается не тип ССЫЛКИ (Throwable), а тип ССЫЛАЕМОГО (Exception)

public class App {
    public static void main(String[] args) {
        try {
            Throwable t = new Exception(); // ссылка типа Throwable указывает на объект типа Exception
            throw t;
        } catch (RuntimeException e) {
            System.err.println("catch RuntimeException");
        } catch (Exception e) {
            System.err.println("catch Exception");
        } catch (Throwable e) {
            System.err.println("catch Throwable");
        }
        System.err.println("next statement");
    }
}

>> catch Exception
>> next statement

7. try + finally

finally-секция получает управление, если try-блок завершился успешно

public class App {
    public static void main(String[] args) {
        try {
            System.err.println("try");
        } finally {
            System.err.println("finally");
        }
    }
}

>> try
>> finally

finally-секция получает управление, даже если try-блок завершился исключением

public class App {
    public static void main(String[] args) {
        try {
            throw new RuntimeException();
        } finally {
            System.err.println("finally");
        }
    }
}

>> finally
>> Exception in thread "main" java.lang.RuntimeException

finally-секция получает управление, даже если try-блок завершился директивой выхода из метода

public class App {
    public static void main(String[] args) {
        try {
            return;
        } finally {
            System.err.println("finally");
        }
    }
}

>> finally

finally-секция НЕ вызывается только если мы «прибили» JVM

public class App {
    public static void main(String[] args) {
        try {
            System.exit(42);
        } finally {
            System.err.println("finally");
        }
    }
}

>> Process finished with exit code 42

System.exit(42) и Runtime.getRuntime().exit(42) — это синонимы

public class App {
    public static void main(String[] args) {
        try {
            Runtime.getRuntime().exit(42);
        } finally {
            System.err.println("finally");
        }
    }
}

>> Process finished with exit code 42

И при Runtime.getRuntime().halt(42) — тоже не успевает зайти в finally

public class App {
    public static void main(String[] args) {
        try {
            Runtime.getRuntime().halt(42);
        } finally {
            System.err.println("finally");
        }
    }
}

>> Process finished with exit code 42

exit() vs halt()
javadoc: java.lang.Runtime#halt(int status)
… Unlike the exit method, this method does not cause shutdown hooks to be started and does not run uninvoked finalizers if finalization-on-exit has been enabled. If the shutdown sequence has already been initiated then this method does not wait for any running shutdown hooks or finalizers to finish their work.

Однако finally-секция не может «починить» try-блок завершившийся исключение (заметьте, «more» — не выводится в консоль)

public class App {
    public static void main(String[] args) {
        try {
            System.err.println("try");
            if (true) {throw new RuntimeException();}
        } finally {
            System.err.println("finally");
        }
        System.err.println("more");
    }
}

>> try
>> finally
>> Exception in thread "main" java.lang.RuntimeException

Трюк с «if (true) {…}» требуется, так как иначе компилятор обнаруживает недостижимый код (последняя строка) и отказывается его компилировать

public class App {
    public static void main(String[] args) {
        try {
            System.err.println("try");
            throw new RuntimeException();
        } finally {
            System.err.println("finally");
        }
        System.err.println("more");
    }
}

>> COMPILER ERROR: Unrechable statement 

И finally-секция не может «предотвратить» выход из метода, если try-блок вызвал return («more» — не выводится в консоль)

public class App {
    public static void main(String[] args) {
        try {
            System.err.println("try");
            if (true) {return;}
        } finally {
            System.err.println("finally");
        }
        System.err.println("more");
    }
}

>> try
>> finally

Однако finally-секция может «перебить» throw/return при помощи другого throw/return

public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        try {
            return 0;
        } finally {
            return 1;
        }
    }
}

>> 1
public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        try {
            throw new RuntimeException();
        } finally {
            return 1;
        }
    }
}

>> 1
public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        try {
            return 0;
        } finally {
            throw new RuntimeException();
        }
    }
}

>> Exception in thread "main" java.lang.RuntimeException
public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        try {
            throw new Error();
        } finally {
            throw new RuntimeException();
        }
    }
}

>> Exception in thread "main" java.lang.RuntimeException

finally-секция может быть использована для завершающего действия, которое гарантированно будет вызвано (даже если было брошено исключение или автор использовал return) по окончании работы

// open some resource
try {
    // use resource
} finally {
    // close resource
}

Например для освобождения захваченной блокировки

Lock lock = new ReentrantLock();
...
lock.lock();
try {
    // some code
} finally {
    lock.unlock();
}

Или для закрытия открытого файлового потока

InputStream input = new FileInputStream("...");
try {
    // some code
} finally {
    input.close();
}

Специально для этих целей в Java 7 появилась конструкция try-with-resources, ее мы изучим позже.

Вообще говоря, в finally-секция нельзя стандартно узнать было ли исключение.
Конечно, можно постараться написать свой «велосипед»

public class App {
    public static void main(String[] args) {
        System.err.println(f());
    }
    public static int f() {
        long rnd = System.currenttimeMillis();
        boolean finished = false;
        try {
            if (rnd % 3 == 0) {
                throw new Error();
            } else if (rnd % 3 == 1) {
                throw new RuntimeException();
            } else {
                // nothing
            }
            finished = true;
        } finally {
            if (finished) {
                // не было исключений
            } else {
                // было исключение, но какое?
            }
        }
    }
}

Не рекомендуемые практики
— return из finally-секции (можем затереть исключение из try-блока)
— действия в finally-секции, которые могут бросить исключение (можем затереть исключение из try-блока)

8. try + catch + finally

Нет исключения

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            // nothing
            System.err.print(" 1");
        } catch(Error e) {
            System.err.print(" 2");
        } finally {
            System.err.print(" 3");
        }
        System.err.print(" 4");
    }
}

>> 0 1 3 4

Не заходим в catch, заходим в finally, продолжаем после оператора

Есть исключение и есть подходящий catch

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new Error();}
            System.err.print(" 1");
        } catch(Error e) {
            System.err.print(" 2");
        } finally {
            System.err.print(" 3");
        }
        System.err.print(" 4");
    }
}

>> 0 2 3 4

Заходим в catch, заходим в finally, продолжаем после оператора

Есть исключение но нет подходящего catch

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            if (true) {throw new RuntimeException();}
            System.err.print(" 1");
        } catch(Error e) {
            System.err.print(" 2");
        } finally {
            System.err.print(" 3");
        }
        System.err.print(" 4");
    }
}

>> 0 3
>> RUNTIME ERROR: Exception in thread "main" java.lang.RuntimeException

Не заходим в catch, заходим в finally, не продолжаем после оператора — вылетаем с неперехваченным исключением

9. Вложенные try + catch + finally

Операторы обычно допускают неограниченное вложение.
Пример с if

public class App {
    public static void main(String[] args) {
        if (args.length > 1) {
            if (args.length > 2) {
                if (args.length > 3) {
                    ...
                }
            }
        }
    }
}

Пример с for

public class App {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 10; i++) {
                for (int k = 0; k < 10; k++) {
                    ...
                }
            }
        }
    }
}

Суть в том, что try-cacth-finally тоже допускает неограниченное вложение.
Например вот так

public class App {
    public static void main(String[] args) {
        try {
            try {
                try {
                    ...
                } catch (Exception e) {
                } finally {}
            } catch (Exception e) {
            } finally {}
        } catch (Exception e) {
        } finally {}
    }
}

Или даже вот так

public class App {
    public static void main(String[] args) {
        try {
            try {
                ...
            } catch (Exception e) {
                ...
            } finally {
                ...
            }
        } catch (Exception e) {
            try {
                ...
            } catch (Exception e) {
                ...
            } finally {
                ...
            }
        } finally {
            try {
                ...
            } catch (Exception e) {
                ...
            } finally {
                ...
            }
        }
    }
}

Ну что же, давайте исследуем как это работает.

Вложенный try-catch-finally без исключения

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            try {
                System.err.print(" 1");
                // НИЧЕГО
                System.err.print(" 2");
            } catch (RuntimeException e) {
                System.err.print(" 3"); // НЕ заходим - нет исключения
            } finally {                 
                System.err.print(" 4"); // заходим всегда
            }
            System.err.print(" 5");     // заходим - выполнение в норме
        } catch (Exception e) {
            System.err.print(" 6");     // НЕ заходим - нет исключения
        } finally {
            System.err.print(" 7");     // заходим всегда
        }
        System.err.print(" 8");         // заходим - выполнение в норме
    }
}

>> 0 1 2 4 5 7 8

Мы НЕ заходим в обе catch-секции (нет исключения), заходим в обе finally-секции и выполняем обе строки ПОСЛЕ finally.

Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНУТРЕННИЙ catch

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            try {
                System.err.print(" 1");
                if (true) {throw new RuntimeException();}
                System.err.print(" 2");
            } catch (RuntimeException e) {
                System.err.print(" 3"); // ЗАХОДИМ - есть исключение
            } finally {                 
                System.err.print(" 4"); // заходим всегда
            }
            System.err.print(" 5");     // заходим - выполнение УЖЕ в норме
        } catch (Exception e) {
            System.err.print(" 6");     // не заходим - нет исключения, УЖЕ перехвачено
        } finally {
            System.err.print(" 7");     // заходим всегда
        }
        System.err.print(" 8");         // заходим - выполнение УЖЕ в норме
    }
}

>> 0 1 3 4 5 7 8

Мы заходим в ПЕРВУЮ catch-секцию (печатаем «3»), но НЕ заходим во ВТОРУЮ catch-секцию (НЕ печатаем «6», так как исключение УЖЕ перехвачено первым catch), заходим в обе finally-секции (печатаем «4» и «7»), в обоих случаях выполняем код после finally (печатаем «5»и «8», так как исключение остановлено еще первым catch).

Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНЕШНИЙ catch

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            try {
                System.err.print(" 1");
                if (true) {throw new Exception();}
                System.err.print(" 2");
            } catch (RuntimeException e) {
                System.err.print(" 3"); // НЕ заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
            } finally {                 
                System.err.print(" 4"); // заходим всегда
            }
            System.err.print(" 5");     // не заходим - выполнение НЕ в норме
        } catch (Exception e) {
            System.err.print(" 6");     // ЗАХОДИМ - есть подходящее исключение
        } finally {
            System.err.print(" 7");     // заходим всегда
        }
        System.err.print(" 8");         // заходим - выполнение УЖЕ в норме
    }
}

>> 0 1 4 6 7 8

Мы НЕ заходим в ПЕРВУЮ catch-секцию (не печатаем «3»), но заходим в ВТОРУЮ catch-секцию (печатаем «6»), заходим в обе finally-секции (печатаем «4» и «7»), в ПЕРВОМ случае НЕ выполняем код ПОСЛЕ finally (не печатаем «5», так как исключение НЕ остановлено), во ВТОРОМ случае выполняем код после finally (печатаем «8», так как исключение остановлено).

Вложенный try-catch-finally с исключением, которое НИКТО НЕ ПЕРЕХВАТИТ

public class App {
    public static void main(String[] args) {
        try {
            System.err.print(" 0");
            try {
                System.err.print(" 1");
                if (true) {throw new Error();}
                System.err.print(" 2");
            } catch (RuntimeException e) {
                System.err.print(" 3"); // НЕ заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
            } finally {                 
                System.err.print(" 4"); // заходим всегда
            }
            System.err.print(" 5");     // НЕ заходим - выполнение НЕ в норме
        } catch (Exception e) {
            System.err.print(" 6");     // не заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
        } finally {
            System.err.print(" 7");     // заходим всегда
        }
        System.err.print(" 8");         // не заходим - выполнение НЕ в норме
    }
}

>> 0 1 4 7
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error

Мы НЕ заходим в ОБЕ catch-секции (не печатаем «3» и «6»), заходим в обе finally-секции (печатаем «4» и «7») и в обоих случаях НЕ выполняем код ПОСЛЕ finally (не печатаем «5» и «8», так как исключение НЕ остановлено), выполнение метода прерывается по исключению.

Контакты

Я занимаюсь онлайн обучением Java (вот курсы программирования) и публикую часть учебных материалов в рамках переработки курса Java Core. Видеозаписи лекций в аудитории Вы можете увидеть на youtube-канале, возможно, видео канала лучше систематизировано в этой статье.

Мой метод обучения состоит в том, что я

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

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

skype: GolovachCourses
email: GolovachCourses@gmail.com

Исключение (exception) — это ненормальная ситуация (термин «исключение» здесь следует понимать как «исключительная ситуация»), возникающая во время выполнения программного кода. Иными словами, исключение — это ошибка, возникающая во время выполнения программы (в runtime).

Исключение — это способ системы Java (в частности, JVM — виртуальной машины Java) сообщить вашей программе, что в коде произошла ошибка. К примеру, это может быть деление на ноль, попытка обратиться к массиву по несуществующему индексу, очень распространенная ошибка нулевого указателя (NullPointerException) — когда вы обращаетесь к ссылочной переменной, у которой значение равно null и так далее.

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

Обработка исключений (exception handling) — название объектно-ориентированной техники, которая пытается разрешить эти ошибки.

Программа в Java может сгенерировать различные исключения, например:

  • программа может пытаться прочитать файл из диска, но файл не существует;

  • программа может попытаться записать файл на диск, но диск заполнен или не отформатирован;

  • программа может попросить пользователя ввести данные, но пользователь ввел данные неверного типа;

  • программа может попытаться осуществить деление на ноль;

  • программа может попытаться обратиться к массиву по несуществующему индексу.

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

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

В Java все исключения представлены отдельными классами. Все классы исключений являются потомками класса Throwable. Так, если в программе возникнет исключительная ситуация, будет сгенерирован объект класса, соответствующего определенному типу исключения. У класса Throwable имеются два непосредственных подкласса: Exception и Error.

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

Ошибки, связанные с работой программы, представлены отдельными подклассами, производными от класса Exception. В частности, к этой категории относятся ошибки деления на нуль, выхода за пределы массива и обращения к файлам. Подобные ошибки следует обрабатывать в самой программе. Важным подклассом, производным от Exception, является класс RuntimeException, который служит для представления различных видов ошибок, часто возникающих во время выполнения программ.

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


Так как в Java
ВСЁ ЯВЛЯЕТСЯ ОБЪЕКТОМ, то исключение тоже является объектом некоторого класса, который описывает исключительную ситуацию, возникающую в определенной части программного кода.

«Обработка исключений» работает следующим образом:

  • когда возникает исключительная ситуация, JVM генерирует (говорят, что JVM ВЫБРАСЫВАЕТ исключение, для описания этого процесса используется ключевое слово throw) объект исключения и передает его в метод, в котором произошло исключение;

  • вы можете перехватить исключение (используется ключевое слово catch), чтобы его каким-то образом обработать. Для этого, необходимо определить специальный блок кода, который называется обработчиком исключений, этот блок будет выполнен при возникновении исключения, код должен содержать реакцию на исключительную ситуацию;

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

Если вы не предусмотрите обработчик исключений, то исключение будет перехвачено стандартным обработчиком Java. Стандартный обработчик прекратит выполнение программы и выведет сообщение об ошибке.

Рассмотрим пример исключения и реакцию стандартного обработчика Java.

public static void main(String[] args) {

System.out.println(5 / 0);

Мы видим, что стандартный обработчик вывел в консоль сообщение об ошибке. Давайте разберемся с содержимым этого сообщения:

«C:\Program Files\Java\jdk1.8.0_60\bin\java»

Exception in thread «main» java.lang.ArithmeticException: / by zero

at ua.opu.Main.main(Main.java:6)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:497)

at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

Process finished with exit code 1

Exception in thread «main» java.lang.ArithmeticException: / by zero

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

at ua.opu.Main.main(Main.java:6)

в каком классе, методе и строке произошло исключение. Используя эту информацию, мы можем найти ту строчку кода, которая привела к исключительной ситуации, и предпринять какие-то действия. Строки

at ua.opu.Main.main(Main.java:6)

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

at java.lang.reflect.Method.invoke(Method.java:497)

at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

называются «трассировкой стека» (stack tracing). О каком стеке идет речь? Речь идет о стеке вызовов (call stack). Соответственно, эти строки означают последовательность вызванных методов, начиная от метода, в котором произошло исключение, заканчивая самым первым вызванным методом.

Для вызова методов в программе используется инструкция «call». Когда вы вызываете метод в программе, важно сохранить адрес следующей инструкции, чтобы, когда вызванный метод отработал, программа продолжила работу со следующей инструкции. Этот адрес нужно где-то хранить в памяти. Также перед вызовом необходимо сохранить аргументы функции, которые тоже необходимо где-то хранить.

Вся эта информация хранится в специальной структуре – стеке вызовов. Каждая запись в стеке вызовов называется кадром или фреймом (stack frame).

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

Как уже было сказано выше, исключение это объект некоторого класса. В Java существует разветвленная иерархия классов исключений.

В Java, класс исключения служит для описания типа исключения. Например, класс NullPointerException описывает исключение нулевого указателя, а FileNotFoundException означает исключение, когда файл, с которым пытается работать приложение, не найден. Рассмотрим иерархию классов исключений:

На самом верхнем уровне расположен класс Throwable, который является базовым для всех исключений (как мы помним, JVM «выбрасывает» исключение», поэтому класс Throwable означает – то, что может «выбросить» JVM).

От класса Throwable наследуются классы Error и Exception. Среди подклассов Exception отдельно выделен класс RuntimeException, который играет важную роль в иерархии исключений.

В Java существует некоторая неопределенность насчет того – существует ли два или три вида исключений.

Если делить исключения на два вида, то это:

  1. 1.

    контролируемые исключения (checked exceptions) – подклассы класса Exception, кроме подкласса RuntimeException и его производных;

  2. 2.

    неконтролируемые исключения (unchecked exceptions) – класс Error с подклассами, а также класс RuntimeException и его производные;

В некоторых источниках класс Error и его подклассы выделяют в отдельный вид исключений — ошибки (errors).

Далее мы видим класс Error. Классы этой ветки составляют вид исключений, который можно обозначить как «ошибки» (errors). Ошибки представляют собой серьезные проблемы, которые не следует пытаться обработать в собственной программе, поскольку они связаны с проблемами уровня JVM.

На самом деле, вы конечно можете предпринять некоторые действия при возникновении ошибок, например, вывести сообщение для пользователя в удобном формате, выслать трассировку стека себе на почту, чтобы понять – что вообще произошло.

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

В качестве примеров «ошибок» можно привести: переполнение стека вызова (класс StackOverflowError); нехватка памяти в куче (класс OutOfMemoryError), вследствие чего JVM не может выделить память под новый объект и сборщик мусора не помогает; ошибка виртуальной машины, вследствие которой она не может работать дальше (класс VirtualMachineError) и так далее.

Несмотря на то, что в нашей программе мы никак не можем помочь этой проблеме, и приложение не может работать дальше (ну как может работать приложение, если стек вызовов переполнен или JVM не может дальше выполнять код?!); знание природы этих ошибок поможет вам предпринять некоторые действия, чтобы избежать этих ошибок в дальнейшем. Например, ошибки типа StackOverflowError и OutOfMemoryError могут быть следствием вашего некорректного кода.

Например, попробуем спровоцировать ошибку StackOverflowError

public static void main(String[] args) {

public static void methodA() {

private static void methodB() {

Получим такое сообщение об ошибке

Exception in thread «main» java.lang.StackOverflowError

at com.company.Main.methodB(Main.java:14)

at com.company.Main.methodA(Main.java:10)

at com.company.Main.methodB(Main.java:14)

at com.company.Main.methodA(Main.java:10)

at com.company.Main.methodB(Main.java:14)

at com.company.Main.methodA(Main.java:10)

at com.company.Main.methodB(Main.java:14)

at com.company.Main.methodA(Main.java:10)

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

Exception in thread «main» java.lang.OutOfMemoryError: Java heap space

at java.base/java.util.Arrays.copyOf(Arrays.java:3511)

at java.base/java.util.Arrays.copyOf(Arrays.java:3480)

at java.base/java.util.ArrayList.grow(ArrayList.java:237)

at java.base/java.util.ArrayList.grow(ArrayList.java:244)

at java.base/java.util.ArrayList.add(ArrayList.java:454)

at java.base/java.util.ArrayList.add(ArrayList.java:467)

at com.company.Main.main(Main.java:13)

Process finished with exit code 1

Ошибка VirtualMachineError может означать, что следует переустановить библиотеки Java.

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

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

В классе Exception описаны исключения двух видов: контролируемые исключения (checked exceptions) и неконтролируемые исключения (unchecked exceptions).

Неконтролируемые исключения содержатся в подклассе RuntimeException и его наследниках. Контролируемые исключения содержатся в остальных подклассах Exception.

В чем разница между контролируемыми и неконтролируемыми исключениями, мы узнаем позже, а теперь рассмотрим вопрос – а как же именно нам обрабатывать исключения?

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

  1. 1.

    с помощью связки try-catch;

  2. 2.

    с помощью ключевого слова throws в сигнатуре метода.

Рассмотрим оба метода поподробнее:

Способ 1. Связка try-catch

Этот способ кратко можно описать следующим образом.

Код, который теоретически может вызвать исключение, записывается в блоке try{}. Сразу за блоком try идет блок код catch{}, в котором содержится код, который будет выполнен в случае генерации исключения. В блоке finally{} содержится код, который будет выполнен в любом случае – произошло ли исключение или нет.

Теперь разберемся с этим способом более подробно. Рассмотрим следующий пример – программу, которая складывает два числа, введенные пользователем из консоли

public static void main(String[] args) {

Scanner scanner = new Scanner(System.in);

System.out.println(«Введите первое число: «);

String firstNumber = scanner.nextLine();

System.out.println(«Введите второе число: «);

String secondNumber = scanner.nextLine();

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

System.out.println(«Результат: « + (a + b));

Первое, что нам нужно определить – и что является главным при работе с исключениями, КАКАЯ ИНСТРУКЦИЯ МОЖЕТ ПРИВЕСТИ К ВОЗНИКНОВЕНИЮ ИСКЛЮЧЕНИЯ?

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

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

в которых происходит преобразование ввода пользователя в целое число (метод parseInt() преобразует цифры в строке в число).

Почему здесь может возникнуть исключение? Потому что пользователь может ввести не число, а просто какой-то текст и тогда непонятно – что записывать в переменную a или b. И да, действительно, если пользователь введет некорректное значение, возникнет исключение в методе Integer.parseInt().

Итак, что мы можем сделать. «Опасный код» нужно поместить в блок try{}

Обратите внимание на синтаксис блока try. В самом простом случае это просто ключевое слово try, после которого идут парные фигурные скобки. Внутри этих скобок и заключается «опасный» код, который может вызвать исключение. Сразу после блока try должен идти блок catch().

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

} catch (NumberFormatException e) {

// сохранить текст ошибки в лог

System.out.println(«Одно или оба значения некорректны!»);

System.out.println(«Результат: « + (a + b));

Обратите внимание на синтаксис блока catch. После ключевого слова, в скобках описывается аргумент с именем e типа NumberFormatException.

Когда произойдет исключение, то система Java прервет выполнение инструкций в блоке try и передаст управление блоку catch и запишет в этот аргумент объект исключения, который сгенерировала Java-машина.

То есть, как только в блоке try возникнет исключение, то дальше инструкции в блоке try выполняться не будут! А сразу же начнут выполняться действия в блоке catch.

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

Блок catch сработает только в том случае, если указанный в скобках тип объекта исключения будет суперклассом или будет того же типа, что и объект исключения, который сгенерировала Java.

Например, если в нашем примере мы напишем код, который потенциально может выбросить исключение типа IOException, но не изменим блок catch

} catch (NumberFormatException e) {

// сохранить текст ошибки в лог

System.out.println(«Одно или оба значения некорректны!»);

тогда обработчик не будет вызван и исключение будет обработано стандартным обработчиком Java.

Способ 2. Использование ключевого слова throws

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

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

public static void main(String[] args) {

int a = getNumberFromConsole(«Введите первое число»);

int b = getNumberFromConsole(«Введите второе число»);

System.out.println(«Результат: « + (a + b));

public static int getNumberFromConsole(String message) {

Scanner scanner = new Scanner(System.in);

System.out.print(message + «: «);

String s = scanner.nextLine();

return Integer.parseInt(s);

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

  1. 1.

    обработка исключений может происходить централизованно однотипным способом (например, показ окошка с сообщением и с определенным текстом);

  2. 2.

    это не входит в нашу компетенцию как программиста – обработкой исключений занимается другой программист;

  3. 3.

    мы пишем только некоторую часть программы и непонятно – как будет обрабатывать исключение другой программист, который потом будет использовать наш код (например, мы пишем просто какую-то библиотеку, которая производит вычисления, и как будет выглядеть обработка – это не наше дело).

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

public static int getNumberFromConsole(String message) throws NumberFormatException {

Scanner scanner = new Scanner(System.in);

System.out.print(message + «: «);

String s = scanner.nextLine();

return Integer.parseInt(s);

Обратите внимание на расположение сигнатуру метода. Мы привыкли, что при объявлении метода сразу после скобок входных аргументов мы открываем фигурную скобку и записываем тело метода. Здесь же, после входных аргументов, мы пишем ключевое слово throws и потом указываем тип исключения, которое может быть сгенерировано в нашем методе. Если метод может выбрасывать несколько типов исключений, они записываются через запятую

public static void foo() throws NumberFormatException, ArithmeticException, IOException {

Тогда, в методе main мы должны написать примерно следующее

public static void main(String[] args) {

a = getNumberFromConsole(«Введите первое число»);

b = getNumberFromConsole(«Введите второе число»);

} catch (NumberFormatException e) {

// сохранить текст ошибки в лог

System.out.println(«Одно или оба значения некорректны!»);

System.out.println(«Результат: « + (a + b));

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

Отличия между контролируемыми и неконтролируемыми исключениями

Если вы вызываете метод, который выбрасывает checked исключение, то вы ОБЯЗАНЫ предусмотреть обработку возможного исключения, то есть связку try-catch.

Яркий пример checked исключения – класс IOException и его подклассы.

Рассмотрим пример – попробуем прочитать файл и построчно вывести его содержимое на экран консоли:

public static void main(String[] args) {

Path p = Paths.get(«c:\\temp\\file.txt»);

BufferedReader reader = Files.newBufferedReader(p);

while ((line = reader.readLine()) != null) {

System.out.println(line);

Как мы видим, компилятор не хочет компилировать наш код. Чем же он недоволен? У нас в коде происходит вызов двух методов – статического метода Files.newBufferedReader() и обычного метода BufferedReader.readLine().

Если посмотреть на сигнатуры этих методов то можно увидеть, что оба этих метода выбрасывают исключения типа IOException. Этот тип исключения относится к checked-исключению и поэтому, если вы вызываете эти методы, компилятор ТРЕБУЕТ от вас предусмотреть блок catch, либо в самом вашем методе указать throws IOException и, таким образом, передать обязанность обрабатывать исключение другому методу, который будет вызывать ваш.

Таким образом, «оборачиваем» наш код в блок try и пишем блок catch.

public static void main(String[] args) {

Path p = Paths.get(«c:\\temp\\file.txt»);

BufferedReader reader = Files.newBufferedReader(p);

while ((line = reader.readLine()) != null) {

System.out.println(line);

} catch (IOException e) {

System.out.println(«Ошибка при чтении файла!»);

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

public static void main(String[] args) {

Path p = Paths.get(«c:\\temp\\file.txt»);

} catch (IOException e) {

System.out.println(«Ошибка при чтении файла!»);

public static void printFile(Path p) throws IOException {

BufferedReader reader = Files.newBufferedReader(p);

while ((line = reader.readLine()) != null) {

System.out.println(line);

Eсли метод выбрасывает checked-исключение, то проверка на наличие catch-блока происходит на этапе компиляции. И вы обязаны предусмотреть обработку исключения для checked-исключения.

Что касается unchecked-исключения, то обязательной обработки исключения нет – вы можете оставить подобные ситуации без обработки.

Зачем необходимо наличие двух видов исключений?

В большинстве языков существует всего лишь один тип исключений – unchecked. Некоторые языки, например, C#, в свое время отказались от checked-исключений.

Во-первых, мы не можем сделать все исключения checked, т.к. очень многие операции могут генерировать исключения, и если каждый такой участок кода «оборачивать» в блок try-catch, то код получится слишком громоздким и нечитабельным.

С другой стороны, зачем нужно делать некоторые типы исключений checked? Почему просто не сделать все исключения unchecked и оставить решения об обработке исключений целиком на совести программиста?

В официальной документации написано, что unchecked-исключения – это те исключения, от которых программа «не может восстановиться», тогда как checked-исключения позволяют откатить некоторую операцию и повторить ее снова.

На самом деле, если вы посмотрите на различные типы unchecked-исключений, то вы увидите, что большинство их связаны с ошибками самого программиста. Выход за пределы массива, исключение нулевого указателя, деление на ноль – большинство из подобного рода исключений целиком лежат на совести программистов. Тогда мы можем сказать, что лучше программист пишет более хороший код, чем везде вставляет проверки на исключения.

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

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

Дополнительно об исключениях

Рассмотрим детально различные возможности механизма исключений, которые позволяют программисту максимально эффективно противодействовать исключениям:

Java позволяет вам для одного блока try предусмотреть несколько блоков catch, каждый из которых должен обрабатывать свой тип исключения

public static void foo() {

} catch (ArithmeticException e) {

// обработка арифметического исключения

} catch (IndexOutOfBoundsException e) {

// обработка выхода за пределы коллекции

} catch (IllegalArgumentException e) {

// обработка некорректного аргумента

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

Так как вы можете указать как точный класс, так и суперкласс, то если первым блоком будет блок для суперкласса – выполнится он. Например, исключение FileNotFoundException является подклассом IOException. И поэтому если вы первым поставите блок с IOException – он будет вызываться для всех подтипов исключений, в том числе и для FileNotFoundException и блок c FileNotFoundException никогда не выполнится.

public static void main(String[] args) {

Path p = Paths.get(«c:\\temp\\file.txt»);

} catch (IOException e) {

System.out.println(«Ошибка при чтении файла!»);

} catch (FileNotFoundException e) {

// данный блок никогда не будет вызван

public static void printFile(Path p) throws IOException {

BufferedReader reader = Files.newBufferedReader(p);

while ((line = reader.readLine()) != null) {

System.out.println(line);

Один блок для обработки нескольких типов исключений

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

public static void foo() {

} catch (ArithmeticException | IllegalArgumentException | IndexOutOfBoundsException e) {

// три типа исключений обрабатываются одинаково

Как мы видим, один блок catch используется для обработки и типа IOException и NullPointerException и NumberFormaException.

Вы можете использовать вложенные блоки try, которые могут помещаться в других блоках try. После вложенного блока try обязательно идет блок catch

public static void foo() {

} catch (IllegalArgumentException e) {

// обработка вложенного блока try

} catch (ArithmeticException e) {

Выбрасывание исключения с помощью ключевого слова throw

С помощью ключевого слова throw вы можете преднамеренно «выбросить» определенный тип исключения.

public static void foo(int a) {

throw new IllegalArgumentException(«Аргумент не может быть отрицательным!»);

Кроме блока try и catch существует специальный блок finally. Его отличительная особенность – он гарантированно отработает, вне зависимости от того, будет выброшено исключение в блоке try или нет. Как правило, блок finally используется для того, чтобы выполнить некоторые «завершающие» операции, которые могли быть инициированы в блоке try.

public static void foo(int a) {

FileOutputStream fout = null;

File file = new File(«file.txt»);

fout = new FileOutputStream(file);

} catch (IOException e) {

// обработка исключения при записи в файл

} catch (IOException e) {

При любом развитии события в блоке try, код в блоке finally отработает в любом случае.

Блок finally отработает, даже если в try-catch присутствует оператор return.

Как правило, блок finally используется, когда мы в блоке try работаем с ресурсами (файлы, базы данных, сокеты и т.д.), когда по окончании блока try-catch мы освобождаем ресурсы. Например, допустим, в процессе работы программы возникло исключение, требующее ее преждевременного закрытия. Но в программе открыт файл или установлено сетевое соединение, а, следовательно, файл нужно закрыть, а соединение – разорвать. Для этого удобно использовать блок finally.

Блок try-with-resources является модификацией блока try. Данный блок позволяет автоматически закрывать ресурс после окончания работы блока try и является удобной альтернативой блоку finally.

public static void foo() {

Path p = Paths.get(«c:\\temp\\file.txt»);

try (BufferedReader reader = Files.newBufferedReader(p)) {

while ((line = reader.readLine()) != null)

System.out.println(line);

} catch (IOException e) {

Внутри скобок блока try объявляется один или несколько ресурсов, которые после отработки блока try-catch будут автоматически освобождены. Для этого объект ресурса должен реализовывать интерфейс java.lang.AutoCloseable.

Создание собственных подклассов исключений

Встроенные в Java исключения позволяют обрабатывать большинство распространенных ошибок. Тем не менее, вы можете создавать и обрабатывать собственные типы исключений. Для того, чтобы создать класс собственного исключения, достаточно определить как его произвольный от Exception или от RuntimeException (в зависимости от того, хотите ли вы использовать checked или unchecked – исключения).

Насчет создания рекомендуется придерживаться двух правил:

  1. 1.

    определитесь, исключения какого типа вы хотите использовать для собственных исключений (checked или unchecked) и старайтесь создавать исключения только этого типа;

  2. 2.

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

Плохие практики при обработке исключений

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

  1. 1.

    Указание в блоке catch объекта исключения типа Exception. Существует очень большой соблазн при создании блока catch указать тип исключения Exception и, таким образом, перехватывать все исключения, которые относятся к этому классу (а это все исключения, кроме системных ошибок). Делать так крайне не рекомендуется, т.к. вместо того чтобы решать проблему с исключениями, мы фактически игнорируем ее и просто реализуем некоторую «заглушку», чтобы приложение продолжило работу дальше. Кроме того, каждый тип исключения должен быть обработан своим определенным образом.

  2. 2.

    Помещение в блок try всего тела метода. Следующий плохой прием используется, когда программист не хочет разбираться с кодом, который вызывает исключение и просто, опять же, реализует «заглушку». Этот прием очень «хорошо» сочетается с первым приемом. В блок try должен помещаться только тот код, который потенциально может вызвать исключение, а не всё подряд, т.к. лень обрабатывать исключения нормально.

  3. 3.

    Игнорирование исключения. Следующий плохой прием состоит в том, что мы просто игнорируем исключение и оставляем блок catch пустым. Программа должна реагировать на исключения и должна информировать пользователя и разработчика о том, что что-то пошло не так. Безусловно, исключение это не повод тут же закрывать приложение, а попытаться повторить то действие, которое привело к исключению (например, повторно указать название файла, попытаться открыть базу данных через время и т.д.). В любом случае, когда приложение в ответ на ошибку никак не реагирует – не выдает сообщение, но и не делает того, чего от нее ожидали – это самый плохой вариант.

Автор оригинала: 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 будет недоступен и вызовет ошибку компиляции.

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

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

попробуйте-поймайте блоки

Более распространенным подходом было бы использовать trycatch блок для перехвата и обработки возникающего исключения:

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-адреса приведет к:

наконец, Блоки

Вводя новый тип блока, блок наконец выполняется независимо от того, что происходит в блоке 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];
    }
  }
}

Вызов этого метода с допустимым индексом приведет к:

Но вызов этого метода с индексом, который выходит за рамки, приведет к:

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

Используйте попытку с ресурсами

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

Закройте ресурсы в 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.

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

Понравилась статья? Поделить с друзьями:
  • Если мужчина не признает своих ошибок
  • Если обнаружили ошибку нажмите
  • Если машинка показывает ошибку ое
  • Если неправильно написал слово как называется ошибка
  • Если компьютер пишет критическая ошибка