Это первая часть статьи, посвященной такому языковому механизму 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-канале, возможно, видео канала лучше систематизировано в этой статье.
Мой метод обучения состоит в том, что я
- показываю различные варианты применения
- строю усложняющуюся последовательность примеров по каждому варианту
- объясняю логику двигавшую авторами (по мере возможности)
- даю большое количество тестов (50-100) всесторонне проверяющее понимание и демонстрирующих различные комбинации
- даю лабораторные для самостоятельной работы
Данная статье следует пунктам #1 (различные варианты) и #2(последовательность примеров по каждому варианту).
skype: GolovachCourses
email: GolovachCourses@gmail.com
Содержание
- Вопросы по Java на собеседовании (3)
- 1. Понятие «Исключение»
- 2. Операторы исключений
- 3. Оператор throws
- 4. Блоки кода try/catch и try/finally
- 5. Может ли блок finally не выполняться?
- 6. Проверяемые и непроверяемые исключения
- 7. Возбуждение исключения
- 8. Определение исключения в сигнатуре метода
- 9. Особенность RuntimeException
- 10. Возбуждение исключения в методе main
- 11. Множественные исключения
- 12. Последовательность нескольких блоков catch
- 13. Поглащение исключений в блоке try. finally
- 14. Исключение SQLException
- 15. Ошибка Error
- 16. Обобщение исключений
- 17. Логирование исключений
- Исключения в Java, Часть I (try-catch-finally)
- 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
- Контакты
Вопросы по Java на собеседовании (3)
1. Понятие «Исключение» |
2. Операторы исключений |
3. Оператор throws |
4. Блоки кода try/catch и try/finally |
5. Может ли блок finally не выполняться? |
6. Проверяемые и непроверяемые исключения |
7. Возбуждение исключения |
8. Определение исключения в сигнатуре метода |
9. Особенность RuntimeException |
10. Возбуждение исключения в методе main |
11. Множественные исключения |
12. Последовательность нескольких блоков catch |
13. Поглащение исключений в блоке try. finally |
14. Исключение SQLException |
15. Ошибка Error |
16. Обобщение исключений |
17. Логирование исключений |
1. Понятие «Исключение»
Исключение — это ошибка, возникающая во время выполнения программы. Причины возникновения исключения могут разные, например :
- некорректно определены (не определены) данные;
- невозможно прочитать или создать файл;
- обрыв сетевого соединения или соединения с сервером базы данных.
Исключение в Java является объектом. Поэтому они могут не только создаваться автоматически виртуальной машиной JVM при возникновении исключительной ситуации, но и порождаться самим разработчиком.
2. Операторы исключений
Java имеет пять ключевых операторов для определения блока исключений, перехвата и возбуждения исключений :
- try — начала блока кода, в котором может возникнуть исключение, и которое следует перехватить;
- catch — начала блока кода, предназначенного для перехвата и обработки исключений (параметром catch является тип ожидаемого исключения);
- throw — оператор для генерации исключений;
- throws — ключевое слово, используемое в сигнатуре метода, и обозначающее, что метод потенциально может вызвать исключение с определенным типом;
- finally — начала дополнительного блока кода, размещаемый после последнего блока catch. Блок finally не является обязательным, но всегда получает управление.
Общая структура «перехвата» исключительной ситуации выглядит следующим образом :
3. Оператор throws
Оператор throws включается в сигнатуру метода с целью обозначения возожности возникновения исключительной ситуации с определенным типом. Использовать данный оператор следует в описании тех методов, которые могут возбуждать исключения, но сами их не обрабатывают. Таким образом, оператором throws метод предупреждает другие методы, вызывающие данный, что у него могут быть вызваны необработанные исключения, чтобы вызывающие методы могли защитить себя от этих исключений.
4. Блоки кода try/catch и try/finally
Каждый оператор try требует наличия либо catch, либо finally, либо сочетания catch и finally. Блок кода finally не является обязательным, и может отсутствовать при использовании try/catch. Оператором finally создаётся блок кода, который должен быть выполнен после завершения блока try/catch.
Если необходимо гарантировано выполнить определенный участок кода, то используется finally. Связка try/finally позволяет обеспечить выполнение блока кода независимо от того, какие исключения были возбуждены и перехвачены, даже в тех случаях, когда в методе нет соответствующего возбужденному исключению раздела catch.
Пример использования try/finally представлен здесь.
5. Может ли блок finally не выполняться?
Код блока finally не будет исполнен, если в код программы включен предшествующий блоку finally системный выход. Следующий пример демонстрирует данную ситуацию.
6. Проверяемые и непроверяемые исключения
Все исключения делятся на «проверяемые» (checked) и «непроверяемые» (unchecked). Данное свойство присуще базовому классу исключения Throwable и передается по наследству (Error, Exception, RuntimeException). В исходном коде класса исключения данное свойство недоступно. Ниже представлена иерархия классов исключений.
Исключения Throwable и Exception, а также все их наследники, за исключением Error и RuntimeException, являются «проверяемыми» checked исключениями. Error и RuntimeException, а также все их наследники, относятся к «непроверяемым» unchecked исключениям.
Проверка исключения на checked выполняется компилятором (compile-time checking). Непроверяемые исключения можно перехватить (catch) в момент исполнения программы (runtime checking).
Java – это язык программирования со статической типизацией, т.е. его компилятор отслеживает корректности использования типов : наличие полей и методов, checked исключения, и т.д. Следующий пример демонстрирует наличие двух ошибок программирования, определенные на этапе копиляции.
Сигнатура метода divide (операция деления) включает определение возможного исключения типа Exception, наследуемого от Throwable. Если делитель (i2) равен 0, то создается объект исключения типа Throwable и возбуждается исключение (throw t), которое не пропускает компилятор, т.к. тип возбуждаемого исключения не соответствует типу исключения в сигнатуре метода. Если в сигнатуре метода определить исключение типа Throwable, то ошибки не будет.
В методе main определен объект obj с инициализацией определенным значением. В следующей строке компилятор находит ошибку, связанную с отсутствием метода charAt(int) в объекте типа Object. Если выполнить приведение типа obj к String, то компилятор пропустит код : char c = ((String)ref).charAt(0).
Пример unchecked исключения представлен здесь.
7. Возбуждение исключения
Как было отмечено выше, исключение является объектом, который можно создать программно. Чтобы возбудить (выбросить) исключение используется оператор throw.
8. Определение исключения в сигнатуре метода
В сигнатуре метода можно определить возможное проверяемое (checked) исключение. Следующий пример демонстрирует определение исключения в сигнатуре метода f1() и его перехват в конструкторе класса.
9. Особенность RuntimeException
Исключение RuntimeException расширяет свойства Exception и является базовым классом для ошибок во время выполнения приложения. Данное исключение относится к необрабатываемым исключениям (unchecked). Согласно описанию класса это исключение может возникнуть во время нормальной работы JVM.
Следующий код демонстрирует пример использования непроверяемого исключения NumberFormatException (наследующего свойства RuntimeException). В функции parseInt при преобразовании строкового значения в число в режиме run-time может возникнуть исключение. Можно метод функции Integer.parseInt() «обернуть» в try/catch, а можно передать обработку исключения функции в вызывающий метод, для чего в сигнатуре определяется соответствующее исключение (throws).
10. Возбуждение исключения в методе main
Если в методе main возбудить исключение, то оно будет передано в виртуальную машину Java (JVM).
11. Множественные исключения
В сигнатуре метода можно определить несколько возможных исключений. Для этого используется оператор throws и исключения, разделенные запятыми. Следующий пример демонстрирует метод callMethods с множественными возможными исключениями :
Чтобы перехватить несколько возможных исключений можно искользовать конструкцию try с несколькими catch.
12. Последовательность нескольких блоков catch
При определение нескольких блоков catch следует руководствоваться правилом обработки исключений от «младшего» к старшему. Т.е. нельзя размещать первым блоком catch (Exception e) <. >, поскольку все остальные блоки catch() уже не смогут перехватить исключение. Помните, что Exception является базовым классом, поэтому его стоит размещать последним.
Рассмотрим следующую иерархию наследования исключений :
Cамым младшим исключением является EOFException, поэтому он должен располагаться перед IOException и Exception, если используется несколько блоков catch с данными типами исключений. Следующий код является демонстрацией данного принципа.
13. Поглащение исключений в блоке try. finally
Если было вызвано два исключения — одно в блоке try, а второе в finally — то, при отсутствии catch, исключение в finally «проглотит» предыдущее исключение. Следует блоки с возможными исключениями всегда обрамлять операторами try/catch, чтобы не потерять важную информацию. Следующий пример демонстрирует «поглащение» исключения в блоке try новым исключением в блоке finally.
В результате в консоль будет выведено следующее сообщение :
Чтобы не «потерять» исключение, необходимо его корректно перехватить и обработать. В примере следует убрать комментарий с блока catch.
14. Исключение SQLException
Исключение SQLException связано с ошибками при работе с базой данных. Данное исключением относится к checked исключениям, и, следовательно, проверяется на этапе компиляции.
Споры вокруг SQLException связаны с тем, что исключение возникает во время исполнения, а обрабатывать его приходится в коде, чтобы не ругался компилятор; может быть следовало бы отнести его к unchecked run-time исключениям? Убедительный довод разработчиков данного исключения связан с тем, что необходимо программисту обработать свои возможные ошибки при работе с базой данных.
15. Ошибка Error
Ошибка Error относится к подклассу не проверяемых (unchecked) исключений, которая показывает серьезные проблемы, возникающие во время выполнения программы. Большинство из ошибок данного класса сигнализируют о ненормальном ходе выполнения программы, т.е. о возникновении критических проблем.
Согласно спецификации Java, не следует пытаться обрабатывать Error в собственной программе, поскольку они связаны с проблемами уровня JVM. Исключения такого рода возникают, если, например, закончилась память, доступная виртуальной машине.
16. Обобщение исключений
При определении в сигнатуре метода возможных исключений можно вместо нескольких проверяемых исключений указать общее (базовое) java.lang.Throwable. В этом случае, компилятор «пропустит код» и программа возможно отработает без сбоев. Например :
Использование Exception или Throwable в сигнатуре метода делает почти невозможным правильное обращение с исключениями при вызове метода. Вызывающий метод получает только информацию о том, что что-то может отработать некорректно.
Перехват обобщенных исключений не позволяет «решить» проблему, т.е. «вылечить» программу, а помогает только обойти периодически возникающую проблему. Это может привести к нескольким непредвиденным ошибкам в других местах приложения.
17. Логирование исключений
Лучше всего логировать (регистрировать) исключение в месте его обработки. Это связано с тем, что именно в данном месте кода достаточно информации для описания возникшей проблемы. Кроме этого, одно и то же исключение при вызове одного и того же метода можно перехватывать в разных местах программы; регистрировать же следует в одном месте.
Иногда исключение может быть частью ожидаемого поведения. В этом случае нет необходимости его регистрировать.
Источник
Исключения в Java, Часть I (try-catch-finally)
Это первая часть статьи, посвященной такому языковому механизму Java как исключения (вторая (checked/unchecked) вот). Она имеет вводный характер и рассчитана на начинающих разработчиков или тех, кто только приступает к изучению языка.
Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).
1. Ключевые слова: try, catch, finally, throw, throws
Механизм исключительных ситуаций в Java поддерживается пятью ключевыми словами
- try
- catch
- finally
- throw
- throws
«Магия» (т.е. некоторое поведение никак не отраженное в исходном коде и потому неповторяемое пользователем) исключений #1 заключается в том, что catch, throw, throws можно использовать исключительно с java.lang.Throwable или его потомками.
throws:
Годится
catch:
Годится
throw:
Годится
Кроме того, throw требуется не-null аргумент, иначе NullPointerException в момент выполнения
throw и new — это две независимых операции. В следующем коде мы независимо создаем объект исключения и «бросаем» его
Однако, попробуйте проанализировать вот это
2. Почему используем System.err, а не System.out
System.out — buffered-поток вывода, а System.err — нет. Таким образом вывод может быть как таким
Так и вот таким (err обогнало out при выводе в консоль)
Давайте это нарисуем
когда Вы пишете в System.err — ваше сообщение тут же выводится на консоль, но когда пишете в System.out, то оно может на какое-то время быть буферизированно. Stacktrace необработанного исключение выводится через System.err, что позволяет им обгонять «обычные» сообщения.
3. Компилятор требует вернуть результат (или требует молчать)
Если в объявлении метода сказано, что он возвращает НЕ void, то компилятор зорко следит, что бы мы вернули экземпляр требуемого типа или экземпляр типа, который можно неявно привести к требуемому
вот так не пройдет (другой тип)
Вот так не выйдет — нет возврата
и вот так не пройдет (компилятор не может удостовериться, что возврат будет)
Компилятор отслеживает, что бы мы что-то вернули, так как иначе непонятно, что должна была бы напечатать данная программа
Из-забавного, можно ничего не возвращать, а «повесить метод»
Тут в d никогда ничего не будет присвоено, так как метод sqr повисает
Компилятор пропустит «вилку» (таки берем в квадрат ИЛИ висим)
Но механизм исключений позволяет НИЧЕГО НЕ ВОЗВРАЩАТЬ!
Итак, у нас есть ТРИ варианта для компилятора
Но КАКОЙ ЖЕ double вернет функция, бросающая RuntimeException?
А НИКАКОЙ!
Подытожим: бросаемое исключение — это дополнительный возвращаемый тип. Если ваш метод объявил, что возвращает double, но у вас нет double — можете бросить исключение. Если ваш метод объявил, что ничего не возвращает (void), но у вам таки есть что сказать — можете бросить исключение.
Давайте рассмотрим некоторый пример из практики.
Задача: реализовать функцию, вычисляющую площадь прямоугольника
важно, что задание звучит именно так, в терминах предметной области — «вычислить площадь прямоугольника», а не в терминах решения «перемножить два числа»:
Вопрос: что делать, если мы обнаружили, что хотя бы один из аргументов — отрицательное число?
Если просто умножить, то мы пропустили ошибочные данные дальше. Что еще хуже, возможно, мы «исправили ситуацию» — сказали что площадь прямоугольника с двумя отрицательными сторонами -10 и -20 = 200.
Мы не можем ничего не вернуть
Можно, конечно, отписаться в консоль, но кто ее будет читать и как определить где была поломка. При чем, вычисление то продолжится с неправильными данными
Можно вернуть специальное значение, показывающее, что что-то не так (error code), но кто гарантирует, что его прочитают, а не просто воспользуются им?
Можем, конечно, целиком остановить виртуальную машину
Но «правильный путь» таков: если обнаружили возможное некорректное поведение, то
1. Вычисления остановить, сгенерировать сообщение-поломку, которое трудно игнорировать, предоставить пользователю информацию о причине, предоставить пользователю возможность все починить (загрузить белье назад и повторно нажать кнопку старт)
4. Нелокальная передача управления (nonlocal control transfer)
Механизм исключительных ситуация (исключений) — это механизм НЕЛОКАЛЬНОЙ ПЕРЕДАЧИ УПРАВЛЕНИЯ.
Что под этим имеется в виду?
Программа, в ходе своего выполнения (точнее исполнения инструкций в рамках отдельного потока), оперирует стеком («стопкой») фреймов. Передача управления осуществляется либо в рамках одного фрейма
и другие операторы.
Либо передача управления происходит в «стопке» фреймов между СОСЕДНИМИ фреймами
- вызов метода: создаем новый фрейм, помещаем его на верхушку стека и переходим в него
- выход из метода: возвращаемся к предыдущему фрейму (через return или просто кончились инструкции в методе)
return — выходим из ОДНОГО фрейма (из фрейма #4(метод h()))
throw — выходим из ВСЕХ фреймов
При помощи catch мы можем остановить летящее исключение (причина, по которой мы автоматически покидаем фреймы).
Останавливаем через 3 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g()) + фрейм #2(метод f())
Обратите внимание, стандартный сценарий работы был восстановлен в методе main() (фрейм #1)
Останавливаем через 2 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g())
Останавливаем через 1 фрейм (фактически аналог return, просто покинули фрейм «другим образом»)
Итак, давайте сведем все на одну картинку
5. try + catch (catch — полиморфен)
Напомним иерархию исключений
То, что исключения являются объектами важно для нас в двух моментах
1. Они образуют иерархию с корнем java.lang.Throwable (java.lang.Object — предок java.lang.Throwable, но Object — уже не исключение)
2. Они могут иметь поля и методы (в этой статье это не будем использовать)
По первому пункту: catch — полиморфная конструкция, т.е. catch по типу Parent перехватывает летящие экземпляры любого типа, который является Parent-ом (т.е. экземпляры непосредственно Parent-а или любого потомка Parent-а)
Даже так: в блоке catch мы будем иметь ссылку типа Exception на объект типа RuntimeException
catch по потомку не может поймать предка
catch по одному «брату» не может поймать другого «брата» (Error и Exception не находятся в отношении предок-потомок, они из параллельных веток наследования от Throwable)
По предыдущим примерам — надеюсь вы обратили внимание, что если исключение перехвачено, то JVM выполняет операторы идущие ПОСЛЕ последних скобок try+catch.
Но если не перехвачено, то мы
1. не заходим в блок catch
2. покидаем фрейм метода с летящим исключением
А что будет, если мы зашли в catch, и потом бросили исключение ИЗ catch?
В таком случае выполнение метода тоже прерывается (не печатаем «3»). Новое исключение не имеет никакого отношения к try-catch
Мы можем даже кинуть тот объект, что у нас есть «на руках»
И мы не попадем в другие секции catch, если они есть
Обратите внимание, мы не напечатали «3», хотя у нас летит Error а «ниже» расположен catch по Error. Но важный момент в том, что catch имеет отношение исключительно к try-секции, но не к другим catch-секциям.
Как покажем ниже — можно строить вложенные конструкции, но вот пример, «исправляющий» эту ситуацию
6. try + catch + catch + .
Как вы видели, мы можем расположить несколько catch после одного try.
Но есть такое правило — нельзя ставить потомка после предка! (RuntimeException после Exception)
Ставить брата после брата — можно (RuntimeException после Error)
Как происходит выбор «правильного» catch? Да очень просто — JVM идет сверху-вниз до тех пор, пока не найдет такой catch что в нем указано ваше исключение или его предок — туда и заходит. Ниже — не идет.
Выбор catch осуществляется в runtime (а не в compile-time), значит учитывается не тип ССЫЛКИ (Throwable), а тип ССЫЛАЕМОГО (Exception)
7. try + finally
finally-секция получает управление, если try-блок завершился успешно
finally-секция получает управление, даже если try-блок завершился исключением
finally-секция получает управление, даже если try-блок завершился директивой выхода из метода
finally-секция НЕ вызывается только если мы «прибили» JVM
System.exit(42) и Runtime.getRuntime().exit(42) — это синонимы
И при Runtime.getRuntime().halt(42) — тоже не успевает зайти в finally
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» — не выводится в консоль)
Трюк с «if (true) <. >» требуется, так как иначе компилятор обнаруживает недостижимый код (последняя строка) и отказывается его компилировать
И finally-секция не может «предотвратить» выход из метода, если try-блок вызвал return («more» — не выводится в консоль)
Однако finally-секция может «перебить» throw/return при помощи другого throw/return
finally-секция может быть использована для завершающего действия, которое гарантированно будет вызвано (даже если было брошено исключение или автор использовал return) по окончании работы
Например для освобождения захваченной блокировки
Или для закрытия открытого файлового потока
Специально для этих целей в Java 7 появилась конструкция try-with-resources, ее мы изучим позже.
Вообще говоря, в finally-секция нельзя стандартно узнать было ли исключение.
Конечно, можно постараться написать свой «велосипед»
Не рекомендуемые практики
— return из finally-секции (можем затереть исключение из try-блока)
— действия в finally-секции, которые могут бросить исключение (можем затереть исключение из try-блока)
8. try + catch + finally
Не заходим в catch, заходим в finally, продолжаем после оператора
Есть исключение и есть подходящий catch
Заходим в catch, заходим в finally, продолжаем после оператора
Есть исключение но нет подходящего catch
Не заходим в catch, заходим в finally, не продолжаем после оператора — вылетаем с неперехваченным исключением
9. Вложенные try + catch + finally
Операторы обычно допускают неограниченное вложение.
Пример с if
Суть в том, что try-cacth-finally тоже допускает неограниченное вложение.
Например вот так
Или даже вот так
Ну что же, давайте исследуем как это работает.
Вложенный try-catch-finally без исключения
Мы НЕ заходим в обе catch-секции (нет исключения), заходим в обе finally-секции и выполняем обе строки ПОСЛЕ finally.
Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНУТРЕННИЙ catch
Мы заходим в ПЕРВУЮ catch-секцию (печатаем «3»), но НЕ заходим во ВТОРУЮ catch-секцию (НЕ печатаем «6», так как исключение УЖЕ перехвачено первым catch), заходим в обе finally-секции (печатаем «4» и «7»), в обоих случаях выполняем код после finally (печатаем «5»и «8», так как исключение остановлено еще первым catch).
Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНЕШНИЙ catch
Мы НЕ заходим в ПЕРВУЮ catch-секцию (не печатаем «3»), но заходим в ВТОРУЮ catch-секцию (печатаем «6»), заходим в обе finally-секции (печатаем «4» и «7»), в ПЕРВОМ случае НЕ выполняем код ПОСЛЕ finally (не печатаем «5», так как исключение НЕ остановлено), во ВТОРОМ случае выполняем код после finally (печатаем «8», так как исключение остановлено).
Вложенный try-catch-finally с исключением, которое НИКТО НЕ ПЕРЕХВАТИТ
Мы НЕ заходим в ОБЕ catch-секции (не печатаем «3» и «6»), заходим в обе finally-секции (печатаем «4» и «7») и в обоих случаях НЕ выполняем код ПОСЛЕ finally (не печатаем «5» и «8», так как исключение НЕ остановлено), выполнение метода прерывается по исключению.
Контакты
Я занимаюсь онлайн обучением Java (вот курсы программирования) и публикую часть учебных материалов в рамках переработки курса Java Core. Видеозаписи лекций в аудитории Вы можете увидеть на youtube-канале, возможно, видео канала лучше систематизировано в этой статье.
Мой метод обучения состоит в том, что я
- показываю различные варианты применения
- строю усложняющуюся последовательность примеров по каждому варианту
- объясняю логику двигавшую авторами (по мере возможности)
- даю большое количество тестов (50-100) всесторонне проверяющее понимание и демонстрирующих различные комбинации
- даю лабораторные для самостоятельной работы
Данная статье следует пунктам #1 (различные варианты) и #2(последовательность примеров по каждому варианту).
Источник
Афоризм
Чтобы оставаться худой, женщине надо есть перед зеркалом и обнаженной.
Поддержка проекта
Если Вам сайт понравился и помог, то будем признательны за Ваш «посильный» вклад в его поддержку и развитие
• Yandex.Деньги
410013796724260
• Webmoney
R335386147728
Z369087728698
1. Понятие «Исключение» |
2. Операторы исключений |
3. Оператор throws |
4. Блоки кода try/catch и try/finally |
5. Может ли блок finally не выполняться? |
6. Проверяемые и непроверяемые исключения |
7. Возбуждение исключения |
8. Определение исключения в сигнатуре метода |
9. Особенность RuntimeException |
10. Возбуждение исключения в методе main |
11. Множественные исключения |
12. Последовательность нескольких блоков catch |
13. Поглащение исключений в блоке try…finally |
14. Исключение SQLException |
15. Ошибка Error |
16. Обобщение исключений |
17. Логирование исключений |
Вопросы и ответы для собеседование по Java, Содержание.
Вопросы и ответы для собеседование по Java, часть 1.
Вопросы и ответы для собеседование по Java, часть 2.
Вопросы и ответы для собеседование по Java, часть 4.
Вопросы и ответы для собеседование по Java, часть 5.
Вопросы и ответы для собеседование по Java, часть 6.
1. Понятие «Исключение»
Исключение — это ошибка, возникающая во время выполнения программы. Причины возникновения исключения
могут разные, например :
- некорректно определены (не определены) данные;
- невозможно прочитать или создать файл;
- обрыв сетевого соединения или соединения с сервером базы данных.
Исключение в Java является объектом. Поэтому они могут не только создаваться автоматически виртуальной
машиной JVM при возникновении исключительной ситуации, но и порождаться самим разработчиком.
2. Операторы исключений
Java имеет пять ключевых операторов для определения блока исключений, перехвата и возбуждения исключений :
- try — начала блока кода, в котором может возникнуть исключение, и которое следует перехватить;
- catch — начала блока кода, предназначенного для перехвата и обработки исключений (параметром catch
является тип ожидаемого исключения); - throw — оператор для генерации исключений;
- throws — ключевое слово, используемое в сигнатуре метода, и обозначающее, что метод
потенциально может вызвать исключение с определенным типом; - finally — начала дополнительного блока кода, размещаемый после последнего блока catch. Блок finally
не является обязательным, но всегда получает управление.
Общая структура «перехвата» исключительной ситуации выглядит следующим образом :
try { // код программы, который может привести к ошибке } catch(Exception e ) { // код программы для обработки исключений } finally { // выполнение блока программы независимо от наличия исключения }
3. Оператор throws
Оператор throws включается в сигнатуру метода с целью обозначения возожности возникновения
исключительной ситуации с определенным типом. Использовать данный оператор следует в описании тех методов,
которые могут возбуждать исключения, но сами их не обрабатывают. Таким образом, оператором throws метод
предупреждает другие методы, вызывающие данный, что у него могут быть вызваны необработанные исключения,
чтобы вызывающие методы могли защитить себя от этих исключений.
public class TestThrow { static void method() throws IllegalAccessException { System.out.println("inside method"); // . . . throw new IllegalAccessException ("Exception in method"); } public static void main(String args[]) { try { method(); } catch(IllegalAccessException e) { System.out.println("Catch inside main : " + e.getMessage()); } } }
4. Блоки кода try/catch и try/finally
Каждый оператор try требует наличия либо catch, либо finally, либо сочетания catch и finally. Блок кода finally
не является обязательным, и может отсутствовать при использовании try/catch. Оператором finally создаётся блок кода,
который должен быть выполнен после завершения блока try/catch.
Если необходимо гарантировано выполнить определенный участок кода, то используется finally. Связка try/finally
позволяет обеспечить выполнение блока кода независимо от того, какие исключения были возбуждены и перехвачены,
даже в тех случаях, когда в методе нет соответствующего возбужденному исключению раздела catch.
Пример использования try/finally представлен здесь.
5. Может ли блок finally не выполняться?
Код блока finally не будет исполнен, если в код программы включен предшествующий блоку finally системный выход.
Следующий пример демонстрирует данную ситуацию.
try { System.exit(0); } catch(Exception e) { System.err.println(e.getMessage()); } finally { // код блока }
6. Проверяемые и непроверяемые исключения
Все исключения делятся на «проверяемые» (checked) и «непроверяемые» (unchecked). Данное свойство присуще
базовому классу исключения Throwable и передается по наследству (Error, Exception, RuntimeException). В исходном
коде класса исключения данное свойство недоступно. Ниже представлена иерархия классов исключений.
Object | Throwable(CHECKED) / Error(UNCHECKED) Exception(CHECKED) | RuntimeException(UNCHECKED)
Исключения Throwable и Exception, а также все их наследники, за исключением Error и RuntimeException,
являются «проверяемыми» checked исключениями. Error и RuntimeException, а также все их наследники, относятся
к «непроверяемым» unchecked исключениям.
Проверка исключения на checked выполняется компилятором (compile-time checking). Непроверяемые исключения
можно перехватить (catch) в момент исполнения программы (runtime checking).
Java – это язык программирования со статической типизацией, т.е. его компилятор отслеживает корректности
использования типов : наличие полей и методов, checked исключения, и т.д. Следующий пример демонстрирует наличие
двух ошибок программирования, определенные на этапе копиляции.
public class TestException { public static Double divide(int i1, int i2) throws Exception { if (i2 != 0) return new Double (i2 / i2); else { Throwable e = new Exception(); throw e; // Unhandled exception type Throwable } } public static void main(String[] args) { Object obj = "Hello!"; char c = obj.charAt(0); // The method charAt(int) is // undefined for the type Object } }
Сигнатура метода divide (операция деления) включает определение возможного исключения типа Exception, наследуемого
от Throwable. Если делитель (i2) равен 0, то создается объект исключения типа Throwable и возбуждается исключение
(throw t), которое не пропускает компилятор, т.к. тип возбуждаемого исключения не соответствует типу исключения в
сигнатуре метода. Если в сигнатуре метода определить исключение типа Throwable, то ошибки не будет.
В методе main определен объект obj с инициализацией определенным значением. В следующей строке компилятор находит
ошибку, связанную с отсутствием метода charAt(int) в объекте типа Object. Если выполнить приведение типа obj к String,
то компилятор пропустит код : char c = ((String)ref).charAt(0).
Пример unchecked исключения представлен здесь.
7. Возбуждение исключения
Как было отмечено выше, исключение является объектом, который можно создать программно. Чтобы возбудить (выбросить)
исключение используется оператор throw.
Exception e = new SQLException(); throw e;
8. Определение исключения в сигнатуре метода
В сигнатуре метода можно определить возможное проверяемое (checked) исключение. Следующий
пример демонстрирует определение исключения в сигнатуре метода f1() и его перехват в конструкторе класса.
import java.io.EOFException; import java.io.FileNotFoundException; public class TestException { public TestException() { try { f1(); } catch (FileNotFoundException e) { e.printStackTrace(); } } public void f1() throws FileNotFoundException { throw new FileNotFoundException(); } public static void main(String[] args) { new TestException (); } }
9. Особенность RuntimeException
Исключение RuntimeException расширяет свойства Exception и является базовым классом для ошибок во время выполнения
приложения. Данное исключение относится к необрабатываемым исключениям (unchecked). Согласно описанию класса это
исключение может возникнуть во время нормальной работы JVM.
Следующий код демонстрирует пример использования непроверяемого исключения NumberFormatException (наследующего
свойства RuntimeException). В функции parseInt при преобразовании строкового значения в число в режиме run-time может
возникнуть исключение. Можно метод функции Integer.parseInt() «обернуть» в try/catch, а можно передать обработку
исключения функции в вызывающий метод, для чего в сигнатуре определяется соответствующее исключение (throws).
public int parseInt(String s) throws NumberFormatException { return Integer.parseInt(s); }
10. Возбуждение исключения в методе main
Если в методе main возбудить исключение, то оно будет передано в виртуальную машину Java (JVM).
11. Множественные исключения
В сигнатуре метода можно определить несколько возможных исключений. Для этого используется оператор throws
и исключения, разделенные запятыми. Следующий пример демонстрирует метод callMethods с множественными
возможными исключениями :
import java.io.EOFException; import java.io.FileNotFoundException; public class TestException { public void callMethods() throws EOFException, FileNotFoundException { if (f1()) f0(); } public void f0() throws EOFException { // ... throw new EOFException(); } public boolean f1() throws FileNotFoundException { // ... throw new FileNotFoundException(); } }
Чтобы перехватить несколько возможных исключений можно искользовать конструкцию try с несколькими catch.
try { if (f1()) f0(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (EOFException e) { e.printStackTrace(); }
12. Последовательность нескольких блоков catch
При определение нескольких блоков catch следует руководствоваться правилом обработки исключений от «младшего»
к старшему. Т.е. нельзя размещать первым блоком catch (Exception e) {…}, поскольку все остальные блоки catch()
уже не смогут перехватить исключение. Помните, что Exception является базовым классом, поэтому его стоит размещать
последним.
Рассмотрим следующую иерархию наследования исключений :
java.lang.Object java.lang.Throwable java.lang.Exception java.io.IOException java.io.EOFException
Cамым младшим исключением является EOFException, поэтому он должен располагаться перед IOException и Exception,
если используется несколько блоков catch с данными типами исключений. Следующий код является демонстрацией
данного принципа.
String x = "Hello"; try { if (!x.equals("Hello")) throw new IOException(); else throw new EOFException(); } catch (EOFException e) { System.err.println("EOFException : " + e.getMessage()); } catch (IOException e) { System.err.println("IOException : " + e.getMessage()); } catch (Exception e) { System.err.println("Exception : " + e.getMessage()); }
13. Поглащение исключений в блоке try…finally
Если было вызвано два исключения — одно в блоке try, а второе в finally — то, при отсутствии catch,
исключение в finally «проглотит» предыдущее исключение. Следует блоки с возможными исключениями всегда
обрамлять операторами try/catch, чтобы не потерять важную информацию. Следующий пример демонстрирует
«поглащение» исключения в блоке try новым исключением в блоке finally.
public class TestException { public TestException() { try { System.out.println(absorbingEx()); } catch (EOFException e) { System.out.println(e.getMessage()); } catch (IOException e) { System.out.println(e.getMessage()); } } public String absorbingEx() throws IOException, EOFException { try { throw new EOFException("EOFException"); // } catch (EOFException e) { // System.out.println("catch " + e.getMessage()); } finally { throw new IOException("finally IOException"); } } public static void main(String[] args) { new TestException(); System.exit(0); } }
В результате в консоль будет выведено следующее сообщение :
Чтобы не «потерять» исключение, необходимо его корректно перехватить и обработать. В примере следует
убрать комментарий с блока catch.
14. Исключение SQLException
Исключение SQLException связано с ошибками при работе с базой данных. Данное исключением относится
к checked исключениям, и, следовательно, проверяется на этапе компиляции.
java.lang.Object java.lang.Throwable java.lang.Exception java.sql.SQLException
Споры вокруг SQLException связаны с тем, что исключение возникает во время исполнения, а обрабатывать
его приходится в коде, чтобы не ругался компилятор; может быть следовало бы отнести его к unchecked
run-time исключениям? Убедительный довод разработчиков данного исключения связан с тем, что необходимо
программисту обработать свои возможные ошибки при работе с базой данных.
15. Ошибка Error
Ошибка Error относится к подклассу не проверяемых (unchecked) исключений, которая показывает серьезные
проблемы, возникающие во время выполнения программы. Большинство из ошибок данного класса сигнализируют о
ненормальном ходе выполнения программы, т.е. о возникновении критических проблем.
Согласно спецификации Java, не следует пытаться обрабатывать Error в собственной программе, поскольку
они связаны с проблемами уровня JVM. Исключения такого рода возникают, если, например,
закончилась память, доступная виртуальной машине.
16. Обобщение исключений
При определении в сигнатуре метода возможных исключений можно вместо нескольких проверяемых исключений
указать общее (базовое) java.lang.Throwable. В этом случае, компилятор «пропустит код» и программа возможно
отработает без сбоев. Например :
public void callCommonException() throws Exception { Object obj = new Object(); method(obj); } public void method(Object obj) throws NumberFormatException, IllegalArgumentException { // ... }
Использование Exception или Throwable в сигнатуре метода делает почти невозможным правильное обращение с
исключениями при вызове метода. Вызывающий метод получает только информацию о том, что что-то может отработать
некорректно.
Перехват обобщенных исключений не позволяет «решить» проблему, т.е. «вылечить» программу, а помогает только
обойти периодически возникающую проблему. Это может привести к нескольким непредвиденным ошибкам в других местах
приложения.
17. Логирование исключений
Лучше всего логировать (регистрировать) исключение в месте его обработки. Это связано с тем, что именно
в данном месте кода достаточно информации для описания возникшей проблемы. Кроме этого, одно и то же исключение
при вызове одного и того же метода можно перехватывать в разных местах программы; регистрировать же следует в
одном месте.
Иногда исключение может быть частью ожидаемого поведения. В этом случае нет необходимости его регистрировать.
Вопросы и ответы для собеседование по Java, Содержание.
Вопросы и ответы для собеседование по Java, часть 1.
Вопросы и ответы для собеседование по Java, часть 2.
Вопросы и ответы для собеседование по Java, часть 4.
Вопросы и ответы для собеседование по Java, часть 5.
Вопросы и ответы для собеседование по Java, часть 6.
I tried this,
It is single threaded.
public static void main(String args[]) throws Exception {
Object obj = new Object();
try {
synchronized (obj) {
obj.wait();
System.out.println("after wait()");
}
} catch (Exception ignored) {
} finally {
System.out.println("finally");
}
}
The main
Thread
will be on wait
state forever, hence finally
will never be called,
so console output will not print
String
: after wait()
or finally
Agreed with @Stephen C, the above example is one of the 3rd case mention here:
Adding some more such infinite loop possibilities in following code:
// import java.util.concurrent.Semaphore;
public static void main(String[] args) {
try {
// Thread.sleep(Long.MAX_VALUE);
// Thread.currentThread().join();
// new Semaphore(0).acquire();
// while (true){}
System.out.println("after sleep join semaphore exit infinite while loop");
} catch (Exception ignored) {
} finally {
System.out.println("finally");
}
}
Case 2: If the JVM crashes first
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public static void main(String args[]) {
try {
unsafeMethod();
//Runtime.getRuntime().halt(123);
System.out.println("After Jvm Crash!");
} catch (Exception e) {
} finally {
System.out.println("finally");
}
}
private static void unsafeMethod() throws NoSuchFieldException, IllegalAccessException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
unsafe.putAddress(0, 0);
}
Ref: How do you crash a JVM?
Case 6: If finally
block is going to be executed by daemon Thread
and all other non-daemon Threads
exit before finally
is called.
public static void main(String args[]) {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
printThreads("Daemon Thread printing");
// just to ensure this thread will live longer than main thread
Thread.sleep(10000);
} catch (Exception e) {
} finally {
System.out.println("finally");
}
}
};
Thread daemonThread = new Thread(runnable);
daemonThread.setDaemon(Boolean.TRUE);
daemonThread.setName("My Daemon Thread");
daemonThread.start();
printThreads("main Thread Printing");
}
private static synchronized void printThreads(String str) {
System.out.println(str);
int threadCount = 0;
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (Thread t : threadSet) {
if (t.getThreadGroup() == Thread.currentThread().getThreadGroup()) {
System.out.println("Thread :" + t + ":" + "state:" + t.getState());
++threadCount;
}
}
System.out.println("Thread count started by Main thread:" + threadCount);
System.out.println("-------------------------------------------------");
}
output: This does not print «finally» which implies «Finally block» in «daemon thread» did not execute
main Thread Printing Thread :Thread[My Daemon Thread,5,main]:state:BLOCKED Thread :Thread[main,5,main]:state:RUNNABLE Thread :Thread[Monitor Ctrl-Break,5,main]:state:RUNNABLE Thread count started by Main thread:3 ------------------------------------------------- Daemon Thread printing Thread :Thread[My Daemon Thread,5,main]:state:RUNNABLE Thread :Thread[Monitor Ctrl-Break,5,main]:state:RUNNABLE Thread count started by Main thread:2 ------------------------------------------------- Process finished with exit code 0
In this code will someVar
be set even if the catch block is executed and the second Exception is thrown?
public void someFunction() throws Exception {
try {
//CODE HERE
} catch (Exception e) {
Log.e(TAG, "", e);
throw new Exception(e);
} finally {
this.someVar= true;
}
}
sharptooth
166k99 gold badges508 silver badges963 bronze badges
asked Nov 24, 2010 at 8:49
2
Yes, the finally blocks always runs… except when:
- The thread running the try-catch-finally block is killed or interrupted
- You use
System.exit(0);
- The underlying VM is destroyed in some other way
- The underlying hardware is unusable in some way
Additionally, if a method in your finally block throws an uncaught exception, then nothing after that will be executed (i.e. the exception will be thrown as it would in any other code). A very common case where this happens is java.sql.Connection.close()
.
As an aside, I am guessing that the code sample you have used is merely an example, but be careful of putting actual logic inside a finally block. The finally block is intended for resource clean-up (closing DB connections, releasing file handles etc), not for must-run logic. If it must-run do it before the try-catch block, away from something that could throw an exception, as your intention is almost certainly functionally the same.
answered Nov 24, 2010 at 8:56
GaryFGaryF
23.7k10 gold badges58 silver badges73 bronze badges
8
Yes.
See the documentation:
The finally block always executes when
the try block exits.
Exceptions:
Note: If the JVM exits while the try
or catch code is being executed, then
the finally block may not execute.
Likewise, if the thread executing the
try or catch code is interrupted or
killed, the finally block may not
execute even though the application as
a whole continues.
answered Nov 24, 2010 at 8:53
froadiefroadie
78.6k74 gold badges166 silver badges234 bronze badges
Finally, block always executes.
public class ExceptionTest {
public static void someFunction(String input) throws Exception {
try {
if( input.equals("ABC") ) {
System.out.println("Matched");
}
} catch (Exception e) {
throw new Exception(e);
} finally {
System.out.println("Input Is "+input+" Finally Executed!!!");
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
System.out.println("********* Test with VALUE ********* ");
someFunction("ABC");
System.out.println("rn********* Test with NULL ********* ");
someFunction(null);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
answered Apr 3, 2019 at 14:51
Finally is always executed, no matter what your case is i.e
- try-catch-finally block
- throws
For unchecked exceptions, java does not mandate, error handling.
this being the reason, if an unchecked exception occurs in finally block then and no handling is done for that, then code written below this point (where the error has occurred) will not be executed.
So I suggest to always handle all the exceptions may it be checked or unchecked.
This way you can make sure that code block in finally is also executed no matter if unchecked exception also occurs. you have a place in sub-nest catch and Finally block to get your necessary work done.
answered Mar 23, 2016 at 4:40
Yogesh KumarYogesh Kumar
6641 gold badge9 silver badges29 bronze badges
answered Nov 24, 2010 at 8:54
urmalpurmalp
3722 gold badges4 silver badges20 bronze badges
Yes. finally
block executes always except the case you call System.exit() because it stops Java VM.
answered Nov 24, 2010 at 8:54
Vladimir IvanovVladimir Ivanov
42.6k18 gold badges77 silver badges102 bronze badges
1
Yes, Finally block will executes always but in your case if you do not handle the exception thrown in catch block your Application will stop without running the finally block.
You must have to handle the catch exception also. Let me know if you want more information.
answered Dec 31, 2022 at 16:21
In this code will someVar
be set even if the catch block is executed and the second Exception is thrown?
public void someFunction() throws Exception {
try {
//CODE HERE
} catch (Exception e) {
Log.e(TAG, "", e);
throw new Exception(e);
} finally {
this.someVar= true;
}
}
sharptooth
166k99 gold badges508 silver badges963 bronze badges
asked Nov 24, 2010 at 8:49
2
Yes, the finally blocks always runs… except when:
- The thread running the try-catch-finally block is killed or interrupted
- You use
System.exit(0);
- The underlying VM is destroyed in some other way
- The underlying hardware is unusable in some way
Additionally, if a method in your finally block throws an uncaught exception, then nothing after that will be executed (i.e. the exception will be thrown as it would in any other code). A very common case where this happens is java.sql.Connection.close()
.
As an aside, I am guessing that the code sample you have used is merely an example, but be careful of putting actual logic inside a finally block. The finally block is intended for resource clean-up (closing DB connections, releasing file handles etc), not for must-run logic. If it must-run do it before the try-catch block, away from something that could throw an exception, as your intention is almost certainly functionally the same.
answered Nov 24, 2010 at 8:56
GaryFGaryF
23.7k10 gold badges58 silver badges73 bronze badges
8
Yes.
See the documentation:
The finally block always executes when
the try block exits.
Exceptions:
Note: If the JVM exits while the try
or catch code is being executed, then
the finally block may not execute.
Likewise, if the thread executing the
try or catch code is interrupted or
killed, the finally block may not
execute even though the application as
a whole continues.
answered Nov 24, 2010 at 8:53
froadiefroadie
78.6k74 gold badges166 silver badges234 bronze badges
Finally, block always executes.
public class ExceptionTest {
public static void someFunction(String input) throws Exception {
try {
if( input.equals("ABC") ) {
System.out.println("Matched");
}
} catch (Exception e) {
throw new Exception(e);
} finally {
System.out.println("Input Is "+input+" Finally Executed!!!");
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
try {
System.out.println("********* Test with VALUE ********* ");
someFunction("ABC");
System.out.println("rn********* Test with NULL ********* ");
someFunction(null);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
answered Apr 3, 2019 at 14:51
Finally is always executed, no matter what your case is i.e
- try-catch-finally block
- throws
For unchecked exceptions, java does not mandate, error handling.
this being the reason, if an unchecked exception occurs in finally block then and no handling is done for that, then code written below this point (where the error has occurred) will not be executed.
So I suggest to always handle all the exceptions may it be checked or unchecked.
This way you can make sure that code block in finally is also executed no matter if unchecked exception also occurs. you have a place in sub-nest catch and Finally block to get your necessary work done.
answered Mar 23, 2016 at 4:40
Yogesh KumarYogesh Kumar
6641 gold badge9 silver badges29 bronze badges
answered Nov 24, 2010 at 8:54
urmalpurmalp
3722 gold badges4 silver badges20 bronze badges
Yes. finally
block executes always except the case you call System.exit() because it stops Java VM.
answered Nov 24, 2010 at 8:54
Vladimir IvanovVladimir Ivanov
42.6k18 gold badges77 silver badges102 bronze badges
1
Yes, Finally block will executes always but in your case if you do not handle the exception thrown in catch block your Application will stop without running the finally block.
You must have to handle the catch exception also. Let me know if you want more information.
answered Dec 31, 2022 at 16:21
Список вопросов и ответов по теме «Исключения в Java».
К списку вопросов по всем темам
Вопросы
1. Дайте определение понятию “исключение”
2. Какова иерархия исключений.
3. Можно/нужно ли обрабатывать ошибки jvm?
4. Какие существуют способы обработки исключений?
5. О чем говорит ключевое слово throws?
6. В чем особенность блока finally? Всегда ли он исполняется?
7. Может ли не быть ни одного блока catch при отлавливании исключений?
8. Могли бы вы придумать ситуацию, когда блок finally не будет выполнен?
9. Может ли один блок catch отлавливать несколько исключений (с одной и разных веток наследований)?
10. Что вы знаете об обрабатываемых и не обрабатываемых (checked/unchecked) исключениях?
11. В чем особенность RuntimeException?
12. Как написать собственное (“пользовательское”) исключение? Какими мотивами вы будете руководствоваться при выборе типа исключения: checked/unchecked?
13. Какой оператор позволяет принудительно выбросить исключение?
14. Есть ли дополнительные условия к методу, который потенциально может выбросить исключение?
15. Может ли метод main выбросить исключение во вне и если да, то где будет происходить обработка данного исключения?
16. Если оператор return содержится и в блоке catch и в finally, какой из них “главнее”?
17. Что вы знаете о OutOfMemoryError?
18. Что вы знаете о SQLException? К какому типу checked или unchecked оно относится, почему?
19. Что такое Error? В каком случае используется Error. Приведите пример Error’а.
20. Какая конструкция используется в Java для обработки исключений?
21. Предположим, есть блок try-finally. В блоке try возникло исключение и выполнение переместилось в блок finally. В блоке finally тоже возникло исключение. Какое из двух исключений “выпадет” из блока try-finally? Что случится со вторым исключением?
22. Предположим, есть метод, который может выбросить IOException и FileNotFoundException в какой последовательности должны идти блоки catch? Сколько блоков catch будет выполнено?
Ответы
1. Дайте определение понятию “исключение”
Исключение — это проблема(ошибка) возникающая во время выполнения программы. Исключения могут возникать во многих случаях, например:
- Пользователь ввел некорректные данные.
- Файл, к которому обращается программа, не найден.
- Сетевое соединение с сервером было утеряно во время передачи данных. И т.д.
Все исключения в Java являются объектами. Поэтому они могут порождаться не только автоматически при возникновении исключительной ситуации, но и создаваться самим разработчиком.
2. Какова иерархия исключений.
Исключения делятся на несколько классов, но все они имеют общего предка — класс Throwable. Его потомками являются подклассы Exception и Error.
Исключения (Exceptions) являются результатом проблем в программе, которые в принципе решаемы и предсказуемы. Например, произошло деление на ноль в целых числах.
Ошибки (Errors) представляют собой более серьёзные проблемы, которые, согласно спецификации Java, не следует пытаться обрабатывать в собственной программе, поскольку они связаны с проблемами уровня JVM. Например, исключения такого рода возникают, если закончилась память, доступная виртуальной машине. Программа дополнительную память всё равно не сможет обеспечить для JVM.
В Java все исключения делятся на два типа: контролируемые исключения (checked) и неконтролируемые исключения (unchecked), к которым относятся ошибки (Errors) и исключения времени выполнения (RuntimeExceptions, потомок класса Exception).
Контролируемые исключения представляют собой ошибки, которые можно и нужно обрабатывать в программе, к этому типу относятся все потомки класса Exception (но не RuntimeException).
3. Можно/нужно ли обрабатывать ошибки jvm?
Обрабатывать можно, но делать этого не стоит. Разработчику не предоставлены инструменты для обработки ошибок системы и виртуальной машины.
4. Какие существуют способы обработки исключений?
В Java есть пять ключевых слов для работы с исключениями:
- try — данное ключевое слово используется для отметки начала блока кода, который потенциально может привести к ошибке.
- catch — ключевое слово для отметки начала блока кода, предназначенного для перехвата и обработки исключений.
- finally — ключевое слово для отметки начала блока кода, которой является дополнительным. Этот блок помещается после последнего блока ‘catch’. Управление обычно передаётся в блок ‘finally’ в любом случае.
- throw — служит для генерации исключений.
- throws — ключевое слово, которое прописывается в сигнатуре метода, и обозначающее что метод потенциально может выбросить исключение с указанным типом.
Общий вид конструкции для «поимки» исключительной ситуации выглядит следующим образом:
try{ //здесь код, который потенциально может привести к ошибке } catch(SomeException e ){ //в скобках указывается класс конкретной ожидаемой ошибки //здесь описываются действия, направленные на обработку исключений } finally{ //выполняется в любом случае ( блок finally не обязателен) } |
Подробнее http://www.quizful.net/post/java-exceptions
5. О чем говорит ключевое слово throws?
throws — ключевое слово, которое прописывается в сигнатуре метода, и обозначающее что метод потенциально может выбросить исключение с указанным типом.
6. В чем особенность блока finally? Всегда ли он исполняется?
Когда исключение передано, выполнение метода направляется по нелинейному пути. Это может стать источником проблем. Например, при входе метод открывает файл и закрывает при выходе. Чтобы закрытие файла не было пропущено из-за обработки исключения, был предложен механизм finally.
Ключевое слово finally создаёт блок кода, который будет выполнен после завершения блока try/catch, но перед кодом, следующим за ним. Блок будет выполнен, независимо от того, передано исключение или нет. Оператор finally не обязателен, однако каждый оператор try требует наличия либо catch, либо finally. Код в блоке finally будет выполнен всегда.
7. Может ли не быть ни одного блока catch при отлавливании исключений?
Такая запись допустима, если имеется связка try{} finally {}. Но смысла в такой записи не так много, всё же лучше иметь блок catch в котором будет обрабатываться необходимое исключение.
String x = «z»; try { x=«234»; } finally { x = «Finally»; } |
8. Могли бы вы придумать ситуацию, когда блок finally не будет выполнен?
Блок finally выполняется не всегда, например в такой ситуации:
try { System.exit(0); } catch(Exception e) { e.printStackTrace(); } finally { } |
Здесь finally недостижим, так как происходит системный выход из программы. Общими словами: когда jvm умирает, ей не до finally (отсюда можете придумать другие примеры как убить jvm и ответить на вопрос в заголовке).
9. Может ли один блок catch отлавливать несколько исключений (с одной и разных веток наследований)?
В Java 7 стала доступна новая конструкция, с помощью которой можно перехватывать несколько исключений одним блоком catch:
try { ... } catch( IOException | SQLException ex ) { logger.log(ex); throw ex; } |
10. Что вы знаете об обрабатываемых и не обрабатываемых (checked/unchecked) исключениях?
Все исключительные ситуации делятся на «проверяемые» (checked) и «непроверяемые» (unchecked) (смотрите картинку в начале статьи). Это свойство присуще «корневищу» (Throwable, Error, Exception, RuntimeException) и передается по наследству. Никак не видимо в исходном коде класса исключения.
В дальнейших примерах просто учтите, что— Throwable и Exception и все их наследники (за исключением наследников Error-а и RuntimeException-а) — checked
— Error и RuntimeException и все их наследники — unchecked
checked exception = проверяемое исключение, проверяемое компилятором.
Тема достаточно обширная для того, чтобы уместить ее в одном ответе. К примеру, можно разобрать примеры Головача: http://habrahabr.ru/company/golovachcourses/blog/225585/
И еще с quizful.net
1. Checked исключения, это те, которые должны обрабатываться блоком catch или описываться в сигнатуре метода. Unchecked могут не обрабатываться и не быть описанными.
2. Unchecked исключения в Java — наследованные от RuntimeException, checked — от Exception (не включая unchecked).
Checked исключения отличаются от Unchecked исключения в Java, тем что:
1)Наличиеобработка Checked исключения проверяются на этапе компиляции. Наличиеобработка Unchecked исключения происходит на этапе выполнения.
11. В чем особенность RuntimeException?
public class RuntimeException extends Exception — базовый класс для ошибок во время выполнения. Относится к необрабатываемым исключениям (uncatchedunchecked). Как сказано в описании класса — это суперкласс, исключения которого могут быть выброшены во время нормальной работы JVM.
12. Как написать собственное (“пользовательское”) исключение? Какими мотивами вы будете руководствоваться при выборе типа исключения: checked/unchecked?
Необходимо унаследоваться от базового класса требуемого типа исключений (например от Exception или RuntimeException).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class ExcClass extends Exception { private String someString; public ExcClass (String string) { this.someString = string; System.out.println(«Exception ExcClass»); } public void myOwnExceptionMsg() { System.err.println(«This is exception message for string: « + someString); } } public class TestExc { public static void main(String[] args) { try { String s = «SomeString»; throw new ExcClass(s); } catch (ExcClass ex) { ex.myOwnExceptionMsg(); } } } //Вывод Exception ExcClass This is exception message for string: SomeString |
Руководствоваться нужно определением типа исключения. В зависимости от того, что вы хотите обрабатывать или видеть нужно и наследоваться от нужного класса.
13. Какой оператор позволяет принудительно выбросить исключение?
throw new Exception();
14. Есть ли дополнительные условия к методу, который потенциально может выбросить исключение?
Если это проверяемое исключение, то оно должно быть задекларировано в сигнатуре метода.
public void someMethod() throws Exception { } |
15. Может ли метод main выбросить исключение во вне и если да, то где будет происходить обработка данного исключения?
Может и оно будет передано в виртуальную машину Java (JVM).
16. Если оператор return содержится и в блоке catch и в finally, какой из них “главнее”?
Вернется из блока finally.
public static void main(String[] args) { String what = method(); System.out.println(what); } public static String method() { try { return «SomeString»; } catch(Exception ex) { return «Catch message»; } finally { return «Finally message»; } } //Вывод Finally message |
17. Что вы знаете о OutOfMemoryError?
OutOfMemoryError выбрасывается, когда виртуальная машина Java не может выделить (разместить) объект из-за нехватки памяти, а сборщик мусора не может высвободить ещё.
Область памяти, занимаемая java процессом, состоит из нескольких частей. Тип OutOfMemoryError зависит от того, в какой из них не хватило места.
1. java.lang.OutOfMemoryError: Java heap space
Не хватает места в куче, а именно, в области памяти в которую помещаются объекты, создаваемые программно в вашем приложении. Размер задается параметрами -Xms и -Xmx. Если вы пытаетесь создать объект, а места в куче не осталось, то получаете эту ошибку. Обычно проблема кроется в утечке памяти, коих бывает великое множество, и интернет просто пестрит статьями на эту тему.
2. java.lang.OutOfMemoryError: PermGen space
Данная ошибка возникает при нехватке места в Permanent области, размер которой задается параметрами -XX:PermSize и -XX:MaxPermSize.
3. java.lang.OutOfMemoryError: GC overhead limit exceeded
Данная ошибка может возникнуть как при переполнении первой, так и второй областей. Связана она с тем, что памяти осталось мало и GC постоянно работает, пытаясь высвободить немного места. Данную ошибку можно отключить с помощью параметра -XX:-UseGCOverheadLimit, но, конечно же, её надо не отключать, а либо решать проблему утечки памяти, либо выделять больше объема, либо менять настройки GC.
4. java.lang.OutOfMemoryError: unable to create new native thread
Выбрасывается, когда нет возможности создать еще потоки.
Подробнее в статье http://habrahabr.ru/post/117274/
18. Что вы знаете о SQLException? К какому типу checked или unchecked оно относится, почему?
SQLException предоставляет информацию об ошибках доступа к базе данных или других ошибках связанных с работой с базами данных.
SQLException относится к checked исключениям, а значит проверяется на этапе компиляции.
Споры об этом типе исключения идут о том, что разработчику приходится постоянно обрабатывать это исключение в коде, хотя большая часть ошибок возникает во время выполнения программы, т.е., по мнению многих, лучше бы отнести его к unchecked runtime исключениям.
try { // make some SQL call(s) } catch {SQLException e) { // log the exception return; // and give up } |
Аргумент Joshua Bloch из Effective Java Second Edition такой: сделав SQLException проверяемым — это попытка заставить разработчиков обработать исключение и обернуть его в новом уровне абстракции.
19. Что такое Error? В каком случае используется Error. Приведите пример Error’а.
Ошибки (Errors) представляют собой более серьёзные проблемы, которые, согласно спецификации Java, не следует пытаться обрабатывать в собственной программе, поскольку они связаны с проблемами уровня JVM. Например, исключения такого рода возникают, если закончилась память, доступная виртуальной машине.
За примером посмотрите картинку иерархии исключений в начале статьи. Как пример — OutOfMemoryError.
20. Какая конструкция используется в Java для обработки исключений?
Можно использовать try-catch-finally и c 7й Java try-with-resources. Первый способ:
try{ //здесь код, который потенциально может привести к ошибке } catch(SomeException e ){ //в скобках указывается класс конкретной ожидаемой ошибки //здесь описываются действия, направленные на обработку исключений } finally{ //выполняется в любом случае ( блок finally не обязателен) } |
Try с ресурсами:
try(открываем файл и т.п. здесь){ //… } //после блока файл закроется автоматически. |
Пример:
Старый способ BufferedReader br = new BufferedReader(new FileReader(path)); try { return br.readLine(); } finally { if (br != null) { br.close(); } } JDK 7 try (BufferedReader br = new BufferedReader(new FileReader(path)) ) { return br.readLine(); } |
Так же смотрите ответ к «Какие существуют способы обработки исключений?»
21. Предположим, есть блок try-finally. В блоке try возникло исключение и выполнение переместилось в блок finally. В блоке finally тоже возникло исключение. Какое из двух исключений “выпадет” из блока try-finally? Что случится со вторым исключением?
Ответ аналогичный случаю с двумя return — будет обработано в finally блоке. Если было выброшено два исключения — одно в try, второе в finally, то исключение в finally «проглотит» исключение выше (см. пример). Если до блока finally исключение было обработано, то мы можем получить информацию об исключении в блоке try и тем самым не потерять исключение, которое впоследствии может быть перезаписано в finally другим исключением.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class TestExc { public static void main(String[] args) { Exception ex = twoExceptionsMethod(); System.out.println(ex.getClass()); String s = twoExceptionsMethod2(); System.out.println(s); } public static Exception twoExceptionsMethod() { try { return new IndexOutOfBoundsException(); } finally { return new NullPointerException(); } } public static String twoExceptionsMethod2() { try { throw new NullPointerException(); }catch (NullPointerException ex) { System.out.println(ex.getMessage()+ » catchBlock»);; } finally { Exception ex2 = new Exception(); return ex2.getMessage() + «finallyBlock»; } } } //Вывод class java.lang.NullPointerException null catchBlock null finallyBlock |
22. Предположим, есть метод, который может выбросить IOException и FileNotFoundException в какой последовательности должны идти блоки catch? Сколько блоков catch будет выполнено?
Общее правило — обрабатывать исключения нужно от «младшего» к старшему. Т.е. нельзя поставить в первый блок catch(Exception e) {}, иначе все дальнейшие блоки catch() уже ничего не смогут обработать, т.к. любое исключение будет попадать под ExceptionName extends Exception.
Таким образом сначала нужно обработать public class FileNotFoundException extends IOException, а затем уже IOException.
public static void ioExcAndFileNotFoundEx() { try { //TODO: some code String x = «abc»; if (x.equals(«abc»)) { throw new IOException(); } else { throw new FileNotFoundException(); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.getMessage(); } } |
К списку вопросов по всем темам
88
167958 Total Views 21 Views Today
Views:
139 206
Исключения
- Исключения
- Введение
- Иерархия исключений
- Проверяемые и непроверяемые
- Иерархия
- Классификация
- Error и Exception
- Работа с исключениями
- Обработка исключений
- Правила try/catch/finally
- Расположение catch блоков
- Транзакционность
- Делегирование
- Методы и практики работы с исключительными ситуацими
- Собственные исключения
- Реагирование через re-throw
- Не забывайте указывать причину возникновения исключения
- Сохранение исключения
- Логирование
- Чего нельзя делать при обработке исключений
- Try-with-resources или try-с-ресурсами
- Общие советы
- Избегайте генерации исключений, если их можно избежать простой проверкой
- Предпочитайте
Optional
, если отсутствие значения — не исключительная ситуация - Заранее обдумывайте контракты методов
- Предпочитайте исключения кодам ошибок и
boolean
флагам-признакам успеха
- Обработка исключений
- Исключения и статические блоки
- Многопоточность и исключения
- Проверяемые исключения и их необходимость
- Заключение
- Полезные ссылки
Введение
Начав заниматься программированием, мы, к своему удивлению, обнаружили, что не так уж просто заставить программы делать задуманное. Я могу точно вспомнить момент, когда я понял, что большая часть моей жизни с этих пор будет посвящена поиску ошибок в собственных программах.
(c) Морис Уилкс.
Предположим, вам понадобилась программа, считывающая содержимое файла.
В целом, здесь нет ничего сложного и код, выполняющий поставленную задачу, мог бы выглядеть как-то так:
public List<String> readAll(String path) { BufferedReader br = new BufferedReader(new FileReader(path)); String line; List<String> lines = new ArrayList<>(); while ((line = br.readLine()) != null) { lines.add(line); } return lines; }
И это был бы вполне рабочий вариант, если бы не одно но: мы живём не в идеальном мире. Код, приведённый выше, рассчитан на то, что всё работает идеально: путь до файла указан верный, файл можно прочитать, во время чтения с файлом ничего не происходит, место хранения файла работает без ошибок и еще огромное количество предположений.
Однако, как показывает практика, мир не идеален, а нас повсюду преследуют ошибки и проблемы. Кто-то может указать путь до несуществующего файла, во время чтения может произойти ошибка, например, файл повреждён или удален в процессе чтения и т.д.
Игнорирование подобных ситуаций недопустимо, так как это ведет к нестабильно и непредсказуемо работающему коду.
Значит, на такие ситуации надо реагировать.
Самая простая реакция — это возвращать boolean
— признак успеха или некоторый код ошибки, например, какое-то число.
Пусть, 0 — это код удачного завершения приложения, 1 — это аварийное завершение и т.д.
Мы получаем код возврата и уже на него реагируем.
Подобный ход имеет право на жизнь, однако, он крайне неудобен в повседневной разработке с её тысячами возможных ошибок и проблемных ситуаций.
Во-первых, он слишком немногословен, так как необходимо помнить что означает каждый код возврата, либо постоянно сверяться с таблицей расшифровки, где они описаны.
Во-вторых, такой подход предоставляет не совсем удобный способ обработки возникших ошибок. Более того, нередки ситуации, когда в месте возникновения ошибки непонятно, как реагировать на возникшую проблему. В таком случае было бы удобнее делегировать обработку ошибки вызывающему коду, до места, где будет понятно как реагировать на ошибку.
В-третьих, и это, на мой взгляд, самое главное — это небезопасно, так как подобный способ можно легко проигнорировать.
Lots of newbie’s coming in from the C world complain about exceptions and the fact that they have to put exception handling all over the place—they want to just write their code. But that’s stupid: most C code never checks return codes and so it tends to be very fragile. If you want to build something really robust, you need to pay attention to things that can go wrong, and most folks don’t in the C world because it’s just too damn hard.
One of the design principles behind Java is that I don’t care much about how long it takes to slap together something that kind of works. The real measure is how long it takes to write something solid.In Java you can ignore exceptions, but you have to willfully do it. You can’t accidentally say, «I don’t care.» You have to explicitly say, «I don’t care.»
(c) James Gosling.
Поэтому, в Java
используется другой механизм работы с такими ситуациями: исключения.
Что такое исключение? В некотором смысле можно сказать, что исключение — это некоторое сообщение, уведомляющее о проблеме, незапланированном поведении.
В нашем примере с чтением содержимого файла, источником такого сообщения может являться BufferedReader
или FileReader
. Сообщению необходим получатель/обработчик, чтобы перехватить его и что-то сделать, как-то отреагировать.
Важно понимать, что генерация исключения ломает поток выполнения программы, так как либо это сообщение будет перехвачено и обработано каким-то зарегистрированным получателем, либо программа завершится.
Что значит «ломает поток выполнения программы»?
Представьте, что по дороге едет грузовик. Движение машины и есть поток выполнения программы. Вдруг водитель видит, что впереди разрушенный мост — исключение, ошибка. Теперь он либо поедет по объездной дороге, т.е перехватит и отреагирует на исключение, либо остановится и поездка будет завершена.
Исключения могут быть разных типов, под разные ситуации, а значит и получателей(обработчиков) может быть несколько — на каждый отдельный тип может быть своя реакция, свой обработчик.
Исключение также может хранить информацию о возникшей проблеме: причину, описание-комментарий и т.д.
Исходя из описания можно сказать, что исключение — это объект некоторого, специально для этого предназначенного, класса. Так как проблемы и ошибки бывают разного рода, их можно классифицировать и логически разделить, значит и классы исключений можно выстроить в некоторую иерархию.
Как генерировать исключения и регистрировать обработчики мы рассмотрим позднее, а пока давайте взглянем на иерархию этих классов.
Иерархия исключений
Ниже приведена иерархия исключений:
Картинка большая, чтобы лучше запоминалась.
Для начала разберем загадочные подписи checked
и unchecked
на рисунке.
Проверяемые и непроверяемые
Все исключения в Java
делятся на два типа: проверяемые (checked
) и непроверяемые исключения (unchecked
).
Как видно на рисунке, java.lang.Throwable
и java.lang.Exception
относятся к проверяемым исключениям, в то время как java.lang.RuntimeException
и java.lang.Error
— это непроверяемые исключения.
Принадлежность к тому или иному типу каждое исключение наследует от родителя.
Это значит, что наследники java.lang.RuntimeException
будут unchecked
исключениями, а наследники java.lang.Exception
— checked
.
Что это за разделение?
В первую очередь напомним, что Java
— это компилируемый язык, а значит, помимо runtime
(время выполнения кода), существует ещё и compile-time
(то, что происходит во время компиляции).
Так вот проверяемые исключения — это исключения, на которые разработчик обязан отреагировать, т.е написать обработчики, и наличие этих обработчиков будет проверено на этапе компиляции. Ваш код не будет скомпилирован, если какое-то проверяемое исключение не обработано, компилятор этого не допустит.
Непроверяемые исключения — это исключения времени выполнения. Компилятор не будет от вас требовать обработки непроверяемых исключений.
В чём же смысл этого разделения на проверяемые и непроверяемые исключения?
Я думаю так: проверяемые исключения в Java
— это ситуации, которые разработчик никак не может предотвратить и исключение является одним из вариантов нормальной работы кода.
Например, при чтении файла требуется обрабатывать java.io.FileNotFoundException
и java.io.IOException
, которые является потомками java.io.Exception
.
Потому, что отсутствие файла или ошибка работы с вводом/выводом — это вполне допустимая ситуация при чтении.
С другой стороны, java.lang.RuntimeException
— это скорее ошибки разработчика.
Например, java.lang.NullPointerException
— это ошибка обращения по null
ссылке, данную ситуацию можно предотвратить: проверить ссылку на null
перед вызовом.
Представьте, что вы едете по дороге, так вот предупредительные знаки — это проверяемые исключения. Например, знак «Осторожно, дети!» говорит о том, что рядом школа и дорогу может перебежать ребенок. Вы обязаны отреагировать на это, не обязательно ребенок перебежит вам дорогу, но вы не можете это проконтролировать, но в данном месте — это нормальная ситуация, ведь рядом школа.
Делать абсолютно все исключения проерямыми — не имеет никакого смысла, потому что вы просто с ума сойдете, пока будете писать обработчики таких ситуаций. Да и зачастую это будет только мешать: представьте себе дорогу, которая утыкана постоянными предупредительными знаками, на которые вы должны реагировать. Ехать по такой дороге будет крайне утомительно.
Разделение на проверяемые и непроверяемые исключения существует только в
Java
, в других языках программирования, таких какScala
,Groovy
,Kotlin
илиPython
, все исключения непроверяемые.Это довольно холиварная тема и свои мысли по ней я изложу в конце статьи.
Теперь рассмотрим непосредственно иерархию исключений.
Иерархия
Итак, корнем иерархии является java.lang.Throwable
, у которого два наследника: java.lang.Exception
и java.lang.Error
.
В свою очередь java.lang.Exception
является родительским классом для java.lang.RuntimeException
.
Занятно, что класс
java.lang.Throwable
назван так, как обычно называют интерфейсы, что иногда вводит в заблуждение новичков. Однако помните, что это класс! Запомнить это довольно просто, достаточно держать в уме то, что исключения могут содержать состояние (например, информация о возникшей проблеме).
Так как в Java
все классы являются наследниками java.lang.Object
, то и исключения (будучи тоже классами) наследуют все стандартные методы, такие как equals
, hashCode
, toString
и т.д.
Раз мы работаем с классами, то можно с помощью наследования создавать свои собственные иерархии исключений, добавляя в них какое-то специфическое поведение и состояние.
Чтобы создать свой собственный класс исключение необходимо отнаследоваться от одного из классов в иерархии исключений. При этом помните, что наследуется еще и тип исключения: проверяемое или непроверяемое.
Классификация
Каждый тип исключения отвечает за свою область ошибок.
-
java.lang.Exception
Это ситуации, которые разработчик никак не может предотвратить, например, не получилось закрыть файловый дескриптор или отослать письмо, и исключение является одним из вариантов нормальной работы кода.
Это проверяемые исключения, мы обязаны на такие исключения реагировать, это будет проверено на этапе компиляции.
Пример:
java.io.IOException
,java.io.FileNotFoundException
. -
java.lang.RuntimeException
Это ситуации, когда основной причиной ошибки является сам разработчик, например, происходит обращение к
null
ссылке, деление на ноль, выход за границы массива и т.д. При этом исключение не является одним из вариантов нормальной работы кода.Это непроверяемые исключения, реагировать на них или нет решает разработчик.
Пример:
java.lang.NullPointerException
. -
java.lang.Error
Это критические ошибки, аварийные ситуации, после которых мы с трудом или вообще не в состоянии продолжить работу. Например, закончилась память, переполнился стек вызовов и т.д.
Это непроверяемые исключения, реагировать на них или нет решает разработчик.
Реагировать на подобные ошибки следует только в том случае, если разработчик точно знает как поступить в такой ситуации. Перехватывать такие ошибки не рекомендуется, так как чаще всего разработчик не знает как реагировать на подобного рода аварийные ситуации.
Теперь перейдем к вопросу: в чем же разница между java.lang.Error
и java.lang.Exception
?
Error и Exception
Все просто. Исключения java.lang.Error
— это более серьезная ситуация, нежели java.lang.Exception
.
Это серьезные проблемы в работе приложения, которые тяжело исправить, либо вообще неясно, можно ли это сделать.
Это не просто исключительная ситуация — это ситуация, в которой работоспособность всего приложения под угрозой! Например, исключение java.lang.OutOfMemoryError
, сигнализирующее о том, что кончается память или java.lang.StackOverflowError
– переполнение стека вызовов, которое можно встретить при бесконечной рекурсии.
Согласитесь, что если не получается преобразовать строку к числу, то это не та ситуация, когда все приложение должно завершаться. Это ситуация, после которой приложение может продолжить работать.
Да, это неприятно, что вы не смогли найти файл по указанному пути, но не настолько критично, как переполнение стека вызовов.
Т.е разница — в логическом разделении.
Поэтому, java.lang.Error
и его наследники используются только для критических ситуаций.
Работа с исключениями
Обработка исключений
Корнем иерархии является класс java.lang.Throwable
, т.е. что-то «бросаемое».
А раз исключения бросаются, то для обработки мы будем ловить их!
В Java
исключения ловят и обрабатывают с помощью конструкции try/catch/finally
.
При заключении кода в один или несколько блоков try
указывается потенциальная возможность выбрасывания исключения в этом месте, все операторы, которые могут сгенерировать исключение, помещаются в этом блоке.
В блоках catch
перечисляются исключения, на которые решено реагировать. Тут определяются блоки кода, предназначенные для решения возникших проблем. Это и есть объявление тех самых получателей/обработчиков исключений.
Пример:
public class ExceptionHandling { public static void main(String[] args) { try { // код } catch(FileNotFoundException fnf) { // обработчик на FileNotFoundException } } }
Тот тип исключения, что указывается в catch
блоке можно расценивать как фильтр, который перехватывает все исключения того типа, что вы указали и всех его потомков, расположенных ниже по иерархии.
Представьте себе просеивание муки. Это процесс целью которого является удаление посторонних частиц, отличающихся по размерам от частиц муки. Вы просеиваете через несколько фильтров муку, так как вам не нужны крупные комочки, осколки и другие посторонние частицы, вам нужна именно мука определенного качества. И в зависимости от выставленных фильтров вы будете перехватывать разные частицы, комочки и т.д. Эти частицы и есть исключения. И если выставляется мелкий фильтр, то вы словите как крупные частицы, так и мелкие.
Точно также и в Java
, ставя фильтр на java.lang.RuntimeException
вы ловите не только java.lang.RuntimeException
, но и всех его наследников! Ведь эти потомки — это тоже runtime
ошибки!
В блоке finally
определяется код, который будет всегда выполнен, независимо от результата выполнения блоков try/catch
. Этот блок будет выполняться независимо от того, выполнился или нет блок try
до конца, было ли сгенерировано исключение или нет, и было ли оно обработано в блоке catch
или нет.
Пример:
public class ExceptionHandling { public static void main(String[] args) { try { // some code } catch(FileNotFoundException fnf) { // обработчик 1 } catch(RuntimeException re) { // обработчик 2 } finally { System.out.println("Hello from finally block."); } } }
В примере выше объявлен try
блок с кодом, который потенциально может сгенерировать исключения, после try
блока описаны два обработчика исключений, на случай генерации FileNotFoundException
и на случай генерации любого RuntimeException
.
Объект исключения доступен по ссылке exception
.
Правила try/catch/finally
-
Блок
try
находится перед блокомcatch
илиfinally
. При этом должен присутствовать хотя бы один из этих блоков. -
Между
try
,catch
иfinally
не может быть никаких операторов. -
Один блок
try
может иметь несколькоcatch
блоков. В таком случае будет выполняться первый подходящий блок.Поэтому сначала должны идти более специальные блоки обработки исключений, а потом уже более общие.
-
Блок
finally
будет выполнен всегда, кроме случая, когдаJVM
преждевременно завершит работу или будет сгенерировано исключение непосредственно в самомfinally
блоке. -
Допускается использование вложенных конструкций
try/catch/finally
.public class ExceptionHandling { public static void main(String[] args) { try { try { // some code } catch(FileNotFoundException fnf) { // обработчик 1 } } catch(RuntimeException re) { // обработчик 2 } finally { System.out.println("Hello from finally block."); } } }
Вопрос:
Каков результат выполнения примера выше, если в блоке try
не будет сгенерировано ни одного исключения?
Ответ:
Будет выведено на экран: «Hello from finally block.».
Так как блок finally
выполняется всегда.
Вопрос:
Теперь немного видоизменим код, каков результат выполнения будет теперь?
public class ExceptionHandling { public static void main(String[] args) { try { return; } finally { System.out.println("Hello from finally block"); } } }
Ответ:
На экран будет выведено: Hello from finally block
.
Вопрос:
Плохим тоном считается прямое наследование от java.lang.Throwable
.
Это строго не рекомендуется делать, почему?
Ответ:
Наследование от наиболее общего класса, а в данном случае от корневого класса иерархии, усложняет обработку ваших исключений. Проблему надо стараться локализовать, а не делать ее описание/объявление максимально общим. Согласитесь, что java.lang.IllegalArgumentException
говорит гораздо больше, чем java.lang.RuntimeException
. А значит и реакция на первое исключение будет более точная, чем на второе.
Далее приводится несколько примеров перехвата исключений разных типов:
Обработка java.lang.RuntimeException
:
try { String numberAsString = "one"; Double res = Double.valueOf(numberAsString); } catch (RuntimeException re) { System.err.println("Error while convert string to double!"); }
Результатом будет печать на экран: Error while convert string to double!
.
Обработка java.lang.Error
:
try { throw new Error(); } catch (RuntimeException re) { System.out.println("RuntimeException"); } catch (Error error) { System.out.println("ERROR"); }
Результатом будет печать на экран: ERROR
.
Расположение catch блоков
Как уже было сказано, один блок try
может иметь несколько catch
блоков. В таком случае будет выполняться первый подходящий блок.
Это значит, что порядок расположения catch
блоков важен.
Рассмотрим ситуацию, когда некоторый используемый нами метод может выбросить два разных исключения:
void method() throws Exception { if (new Random((System.currentTimeMillis())).nextBoolean()) { throw new Exception(); } else { throw new IOException(); } }
Конструкция
new Random((System.currentTimeMillis())).nextBoolean()
генерирует нам случайное значениеfalse
илиtrue
.
Для обработки исключений этого метода написан следующий код:
try { method(); } catch (Exception e) { // Обработчик 1 } catch (IOException e) { // Обработчик 2 }
Все ли хорошо с приведенным выше кодом?
Нет, код выше неверен, так как обработчик java.io.IOException
в данном случае недостижим. Все дело в том, что первый обработчик, ответсвенный за Exception
, перехватит все исключения, а значит не может быть ситуации, когда мы сможем попасть во второй обработчик.
Снова вспомним пример с мукой, приведенный в начале.
Так вот песчинка, которую мы ищем, это и есть наше исключение, а каждый фильтр это catch
блок.
Если первым установлен фильтр ловить все, что является Exception и его потомков, то до фильтра ловить все, что является IOException и его потомков ничего не дойдет, так как верхний фильтр уже перехватит все песчинки.
Отсюда следует правило:
Сначала должны идти более специальные блоки обработки исключений, а потом уже более общие.
А что если на два разных исключения предусмотрена одна и та же реакция? Написание двух одинаковых catch
блоков не приветствуется, ведь дублирование кода — это зло.
Поэтому допускается объединить два catch
блока с помощью |
:
try { method2(); } catch (IllegalArgumentException | IndexOutOfBoundsException e) { // Обработчик }
Вопрос:
Есть ли способ перехватить все возможные исключения?
Ответ:
Есть! Если взглянуть еще раз на иерархию, то можно отметить, что java.lang.Throwable
является родительским классом для всех исключений, а значит, чтобы поймать все, необходимо написать что-то в виде:
try { method(); } catch (Throwable t) { // Обработчик }
Однако, делать так не рекомендуется, что наталкивает на следующий вопрос.
Вопрос:
Почему перехватывать java.lang.Throwable
— плохо?
Ответ:
Дело в том, что написав:
try { method(); } catch (Throwable t) { // catch all }
Будут перехвачены абсолютно все исключения: и java.lang.Exception
, и java.lang.RuntimeException
, и java.lang.Error
, и все их потомки.
И как реагировать на все? При этом надо учесть, что обычно на java.lang.Error
исключений вообще не ясно как реагировать. А значит, мы можем неверно отреагировать на исключение и вообще потерять данные. А ловить то, что не можешь и не собирался обрабатывать — плохо.
Поэтому перехватывать все исключения — плохая практика.
Вопрос-Тест:
Что будет выведено на экран при запуске данного куска кода?
public static void main(String[] args) { try { try { throw new Exception("0"); } finally { if (true) { throw new IOException("1"); } System.err.println("2"); } } catch (IOException ex) { System.err.println(ex.getMessage()); } catch (Exception ex) { System.err.println("3"); System.err.println(ex.getMessage()); } }
Ответ:
При выполнении данного кода выведется «1».
Давайте разберем почему.
Мы кидаем исключение во вложенном try
блоке: throw new Exception("0");
.
После этого поток программы ломается и мы попадаем в finally
блок:
if (true) { throw new IOException("1"); } System.err.println("2");
Здесь мы гарантированно зайдем в if
и кинем уже новое исключение: throw new IOException("1");
.
При этом вся информация о первом исключении будет потеряна! Ведь мы никак не отреагировали на него, а в finally
блоке и вовсе ‘перезатерли’ новым исключением.
На try
, оборачивающий наш код, настроено два фильтра: первый на IOException
, второй на Exception
.
Так как порядок расположения задан так, что мы прежде всего смотрим на IOException
, то и сработает этот фильтр, который выполнит следующий код:
System.err.println(ex.getMessage());
Именно поэтому выведется 1
.
Транзакционность
Важным моментом, который нельзя пропустить, является то, что try
блок не транзакционный.
Под термином
транзакционность
я имею в виду то, что либо действия будут выполнены целиком и успешно, либо не будут выполнены вовсе.
Что это значит?
Это значит, что при возникновении исключения в try
блоке все совершенные действия не откатываются к изначальному состоянию, а так и остаются совершенными.
Все выделенные ресурсы так и остаются занятыми, в том числе и при возникновении исключения.
По сути именно поэтому и существует finally
блок, так как туда, как уже было сказано выше, мы зайдем в любом случае, то там и освобождают выделенные ресурсы.
Вопрос:
Работа с объектами из try
блока в других блоках невозможна:
public class ExceptionExample { public static void main(String[] args) { try { String line = "hello"; } catch (Exception e) { System.err.println(e); } // Compile error System.out.println(line); // Cannot resolve symbol `line` } }
Почему?
Ответ:
Потому что компилятор не может нам гарантировать, что объекты, объявленные в try
-блоке, были созданы.
Ведь могло быть сгенерировано исключение. Тогда после места, где было сгенерировано исключение, оставшиеся действия не будут выполнены, а значит возможна ситуация, когда объект не будет создан. Следовательно и работать с ним нельзя.
Вернемся к примеру с грузовиком, чтобы объяснить все вышесказанное.
Объездная здесь — это catch
блок, реакция на исключительную ситуацию. Если добавить еще несколько объездных дорог, несколько catch
блоков, то водитель выберет наиболее подходящий путь, наиболее подходящий и удобный catch
блок, что объясняет важность расположения этих блоков.
Транзакционность на этом примере объясняется тем, что если до этого водитель где-то оплатил проезд по мосту, то деньги ему автоматически не вернутся, необходимо будет написать в поддержку или куда-то пожаловаться на управляющую компанию.
Делегирование
Выше было разобрано то, как обрабатывать исключения. Однако, иногда возникают ситуации, когда в нет конкретного понимания того, как обрабатывать возникшее исключение. В таком случае имеет смысл делегировать задачу обработки исключения коду, который вызвал ваш метод, так как вызывающий код чаще всего обладает более обширными сведениями об источнике проблемы или об операции, которая сейчас выполняется.
Делегирование исключения производится с помощью ключевого слова throws
, которое добавляется после сигнатуры метода.
Пример:
// Код написан только для ознакомительной цели, не стоит с него брать пример! String readLine(String path) throws IOException { BufferedReader br = new BufferedReader(...); String line = br.readLine(); return line; }
Таким образом обеспечивается передача объявленного исключения в место вызова метода. И то, как на него реагировать уже становится заботой вызывающего этот метод.
Поэтому реагировать и писать обработчики на те исключения, которые мы делегировали, внутри метода уже не надо.
Механизм throws
введен для проброса проверяемых исключений.
Разумеется, с помощью throws
можно описывать делегирование как проверяемых, так и непроверяемых исключений.
Однако перечислять непроверяемые не стоит, такие исключения не контролируются в compile time
.
Перечисление непроверяемых исключений бессмысленно, так как это примерно то же самое, что перечислять все, что может с вами случиться на улице.
Теперь пришла пора рассмотреть методы обработки исключительных ситуаций.
Методы и практики работы с исключительными ситуацими
Главное и основное правило при работе с исключениями звучит так:
На исключения надо либо реагировать, либо делегировать, но ни в коем случае не игнорировать.
Определить когда надо реагировать, а когда делегировать проще простого. Задайте вопрос: «Знаю ли я как реагировать на это исключение?».
Если ответ «да, знаю», то реагируйте, пишите обработчик и код, отвечающий за эту реакцию, если не знаете что делать с исключением, то делегируйте вызывающему коду.
Собственные исключения
Выше мы уже затронули то, что исключения это те же классы и объекты.
И иногда удобно выстроить свою иерархию исключений, заточенных под конкретную задачу. Дабы более гибко обрабатывать и реагировать на те исключительные ситуации, которые специфичны решаемой задаче.
Например, пусть есть некоторый справочник:
class Catalog { Person findPerson(String name); }
В данном случае нам надо обработать ситуации, когда name
является null
, когда в каталоге нет пользователя с таким именем.
Если генерировать на все ситуации java.lang.Exception
, то обработка ошибок будет крайне неудобной.
Более того, хотелось бы явно выделить ошибку, связанную с тем, что пользователя такого не существует.
Очевидно, что стандартное исключение для этого случая не существует, а значит вполне логично создать свое.
class PersonNotFoundException extends RuntimeException { private String name; // some code }
Обратите внимание, что имя Person
, по которому в каталоге не смогли его найти, выделено в свойство класса name
.
Теперь при использовании этого метода проще реагировать на различные ситуации, такие как null
вместо имени, а проблему с отсутствием Person
в каталоге можно отдельно вынести в свой catch
блок.
Реагирование через re-throw
Часто бывает необходимо перехватить исключение, сделать запись о том, что случилось (в файл лога, например) и делегировать его вызывающему коду.
Как уже было сказано выше, в рамках конструкции try/catch/finally
можно сгенерировать другое исключение.
Такой подход называется re-throw
.
Исключение перехватывается в catch
блоке, совершаются необходимые действия, например, запись в лог или создание нового, более конкретного для контекста задачи, исключения и повторная генерация исключения.
Как это выглядит на практике:
try { Reader readerConf = .... readerConf.readConfig(); } catch(IOException ex) { System.err.println("Log exception: " + ex); throw new ConfigException(ex); }
Во время чтения конфигурационного файла произошло исключение java.io.IOException
, в catch
блоке оно было перехвачено, сделана запись в консоль о проблеме, после чего было создано новое, более конкретное, исключение ConfigException
, с указанием причины (перехваченное исключение, ссылка на которое ex
) и оно было проброшено дальше.
По итогу, из метода с приведенным кодом, в случае ошибки чтения конфигурации, будет выброшено ConfigException
.
Для чего мы здесь так поступили?
Это полезно для более гибкой обработки исключений.
В примере выше чтение конфигурации генерирует слишком общее исключение, так как java.io.IOException
это довольно общее исключение, но проблема в примере выше понятна: работа с этим конфигурационным файлом невозможна.
Значит и сообщить лучше именно как о том, что это не абстрактный java.io.IOException
, а именно ConfigException
. При этом, так как перехваченное исключение было передано новому в конструкторе, т.е. указалась причина возникновения (cause) ConfigException
, то при выводе на консоль или обработке в вызывающем коде будет понятно почему ConfigException
был создан.
Также, можно было добавить еще и текстовое описание к сгенерированному ConfigException
, более подробно описывающее произошедшую ситуацию.
Еще одной важной областью применения re-throw
бывает преобразование проверяемых исключений в непроверяемые.
В Java 8
даже добавили исключение java.io.UncheckedIOException
, которое предназначено как раз для того, чтобы сделать java.io.IOException
непроверяемым, обернуть в unchecked
обертку.
Пример:
try { Reader readerConf = .... readerConf.readConfig(); } catch(IOException ex) { System.err.println("Log exception: " + ex); throw new UncheckedIOException(ex); }
Не забывайте указывать причину возникновения исключения
В предыдущем пункте мы создали собственное исключение, которому указали причину: перехваченное исключение, java.io.IOException
.
Чтобы понять как это работает, давайте рассмотрим наиболее важные поля класса java.lang.Throwable
:
public class Throwable implements Serializable { /** * Specific details about the Throwable. For example, for * {@code FileNotFoundException}, this contains the name of * the file that could not be found. * * @serial */ private String detailMessage; // ... /** * The throwable that caused this throwable to get thrown, or null if this * throwable was not caused by another throwable, or if the causative * throwable is unknown. If this field is equal to this throwable itself, * it indicates that the cause of this throwable has not yet been * initialized. * * @serial * @since 1.4 */ private Throwable cause = this; // ... }
Все исключения, будь то java.lang.RuntimeException
, либо java.lang.Exception
имеют необходимые конструкторы для инициализации этих полей.
При создании собственного исключения не пренебрегайте этими конструкторами!
Поле cause
используются для указания родительского исключения, причины. Например, выше мы перехватили java.io.IOException
, прокинув свое исключение вместо него. Но причиной того, что наш код выкинул ConfigException
было именно исключение java.io.IOException
. И эту причину нельзя игнорировать.
Представьте, что код, использующий ваш метод также перехватил ConfigException
, пробросив какое-то своё исключение, а это исключение снова кто-то перехватил и пробросил свое. Получается, что истинная причина будет просто потеряна! Однако, если каждый будет указывать cause
, истинного виновника возникновения исключения, то вы всегда сможете обнаружить по этому стеку виновника.
Для получения причины возникновения исключения существует метод getCause.
public class ExceptionExample { public Config readConfig() throws ConfigException { // (1) try { Reader readerConf = ....; readerConf.readConfig(); } catch (IOException ex) { System.err.println("Log exception: " + ex); throw new ConfigException(ex); // (2) } } public void run() { try { Config config = readConfig(); // (3) } catch (ConfigException e) { Throwable t = e.getCause(); // (4) } } }
В коде выше:
- В строке (1) объявлен метод
readConfig
, который может выброситьConfigException
. - В строке (2) создаётся исключение
ConfigException
, в конструктор которого передаетсяIOException
— причина возникновения. readConfig
вызывается в (3) строке кода.- А в (4) вызван метод
getCause
который и вернёт причину возникновенияConfigException
—IOException
.
Сохранение исключения
Исключения необязательно генерировать, пробрасывать и так далее.
Выше уже упоминалось, что исключение — это Java
-объект. А значит, его вполне можно присвоить переменной или свойству класса, передать по ссылке в метод и т.д.
class Reader { // A holder of the last IOException encountered private IOException lastException; // some code public void read() { try { Reader readerConf = .... readerConf.readConfig(); } catch(IOException ex) { System.err.println("Log exception: " + ex); lastException = ex; } } }
Генерация исключения это довольно дорогостоящая операция. Кроме того, исключения ломают поток выполнения программы. Чтобы не ломать поток выполнения, но при этом иметь возможность в дальнейшем отреагировать на исключительную ситуацию можно присвоить ее свойству класса или переменой.
Подобный прием использован в java.util.Scanner
, где генерируемое исключение чтения потока сохраняется в свойство класса lastException
.
Еще одним способом применения сохранения исключения может являться ситуация, когда надо сделать N операций, какие-то из них могут быть не выполнены и будет сгенерировано исключение, но реагировать на эти исключения будут позже, скопом.
Например, идет запись в базу данных тысячу строк построчно.
Из них 100 записей происходит с ошибкой.
Эти исключения складываются в список, а после этот список передается специальному методу, который по каждой ситуации из списка как-то отреагирует.
Т.е пока делаете операцию, копите ошибки, а потом уже реагируете.
Это похоже на то, как опрашивают 1000 человек, а негативные отзывы/голоса записывают, после чего реагируют на них. Согласитесь, было бы глупо после каждого негативного отзыва осуществлять реакцию, а потом снова возвращаться к толпе и продолжать опрос.
class Example { private List<Exception> exceptions; // some code public void parse(String s) { try { // do smth } catch(Exception ex) { exceptions.add(ex); } } private void handleExceptions() { for(Exception e : exceptions) { System.err.println("Log exception: " + e); } } }
Логирование
Когда логировать исключение?
В большинстве случаев лучше всего логировать исключение в месте его обработки. Это связано с тем, что именно в данном месте кода достаточно информации для описания возникшей проблемы — реакции на исключение. Кроме этого, одно и то же исключение при вызове одного и того же метода можно перехватывать в разных местах программы.
Также, исключение может быть частью ожидаемого поведения. В этом случае нет необходимости его логировать.
Поэтому не стоит преждевременно логировать исключение, например:
/** * Parse date from string to java.util.Date. * @param date as string * @return Date object. */ public static Date from(String date) { try { DateFormat format = new SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH); return format.parse(date); } catch (ParseException e) { logger.error("Can't parse ") throw e; } }
Здесь ParseException
является частью ожидаемой работы, в ситуациях, когда строка содержит невалидные данные.
Раз происходит делегирование исключения выше (с помощью throw
), то и там, где его будут обрабатывать и лучше всего логировать, а эта запись в лог будет избыточной. Хотя бы потому, что в месте обработки исключения его тоже залогируют!
Подробнее о логировании.
Чего нельзя делать при обработке исключений
-
Старайтесь не игнорировать исключения.
В частности, никогда не пишите подобный код:
try { Reader readerConf = .... readerConf.readConfig(); } catch(IOException e) { e.printStackTrace(); }
-
Не следует писать ‘универсальные’ блоки обработки исключений.
Ведь очень трудно представить себе метод, который одинаково реагировал бы на все возникающие проблемы.
Также программный код может измениться, а ‘универсальный’ обработчик исключений будет продолжать обрабатывать новые типы исключений одинаково.
Поэтому таких ситуаций лучше не допускать.
-
Старайтесь не преобразовывать более конкретные исключения в более общие.
В частности, например, не следует
java.io.IOException
преобразовывать вjava.lang.Exception
или вjava.lang.Throwable
.Чем с более конкретными исключениями идет работа, тем проще реагировать и принимать решения об их обработке.
-
Старайтесь не злоупотреблять исключениями.
Если исключение можно не допустить, например, дополнительной проверкой, то лучше так и сделать.
Например, можно обезопасить себя от
java.lang.NullPointerException
простой проверкой:if(ref != null) { // some code }
Try-with-resources или try-с-ресурсами
Как уже говорилось выше про finally
блок, код в нем выполняется в любом случае, что делает его отличным кандидатом на место по освобождению ресурсов, учитывая нетранзакционность блока try
.
Чаще всего за закрытие ресурса будет отвечать код, наподобие этого:
try { // code } finally { resource.close(); }
Освобождение ресурса (например, освобождение файлового дескриптора) — это поведение.
А за поведение в
Java
отвечают интерфейсы.
Это наталкивает на мысль, что нужен некоторый общий интерфейс, который бы реализовывали все классы, для которых необходимо выполнить какой-то код по освобождению ресурсов, т.е выполнить ‘закрытие’ в finally
блоке и еще удобнее, если бы этот однообразный finally
блок не нужно было писать каждый раз.
Поэтому, начиная с Java 7
, была введена конструкция try-with-resources
или TWR
.
Для этого объявили специальный интерфейс java.lang.AutoCloseable
, у которого один метод:
void close() throws Exception;
Все классы, которые будут использоваться так, как было описано выше, должны реализовать или java.lang.Closable
, или java.lang.AutoCloseable
.
В качестве примера, напишем код чтения содержимого файла и представим две реализации этой задачи: используя и не используя try-with-resources
.
Без использования try-with-resources
(пример ниже плох и служит только для демонстрации объема необходимого кода):
BufferedReader br = null; try { br = new BufferedReader(new FileReader(path)); // read from file } catch (IOException e) { // catch and do smth } finally { try { if (br != null) { br.close(); } } catch (IOException ex) { // catch and do smth } }
А теперь то же самое, но в Java 7+
:
try (FileReader fr = new FileReader(path); BufferedReader br = new BufferedReader(fr)) { // read from file } catch (IOException e) { // catch and do smth }
По возможности пользуйтесь только try-with-resources
.
Помните, что без реализации
java.lang.Closable
илиjava.lang.AutoCloseable
ваш класс не будет работать сtry-with-resources
так, как показано выше.
Вопрос:
Получается, что используя TWR
мы не пишем код для закрытия ресурсов, но при их закрытии может же тоже быть исключение! Что произойдет?
Ответ:
Точно так же, как и без TWR
, исключение выбросится так, будто оно было в finally
-блоке.
Помните, что TWR
, грубо говоря, просто добавляет вам блок кода вида:
finally { resource.close(); }
Вопрос:
Является ли безопасной конструкция следующего вида?
try (BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("a")))) { }
Ответ:
Не совсем, если конструктор OutputStreamWriter
или BufferedWriter
выбросит исключение, то FileOutputStream
закрыт не будет.
Пример, демонстрирующий это:
public class Main { public static void main(String[] args) throws Exception { try (ThrowingAutoCloseable throwingAutoCloseable = new ThrowingAutoCloseable(new PrintingAutoCloseable())) { // (1) } } private static class ThrowingAutoCloseable implements AutoCloseable { // (2) private final AutoCloseable other; public ThrowingAutoCloseable(AutoCloseable other) { this.other = other; throw new IllegalStateException("I always throw"); // (3) } @Override public void close() throws Exception { try { other.close(); // (4) } finally { System.out.println("ThrowingAutoCloseable is closed"); } } } private static class PrintingAutoCloseable implements AutoCloseable { // (5) public PrintingAutoCloseable() { System.out.println("PrintingAutoCloseable created"); // (6) } @Override public void close() { System.out.println("PrintingAutoCloseable is closed"); // (7) } } }
- В строке (1) происходит заворачивание одного ресурса в другой, аналогично
new BufferedWriter(new OutputStreamWriter(new FileOutputStream("a")))
. ThrowingAutoCloseable
(2) — такойAutoCloseable
, который всегда бросает исключение (3), в (4) производится попытка закрыть полученный в конструктореAutoCloseable
.PrintingAutoCloseable
(5) —AutoCloseable
, который печатает сообщения о своём создании (6) и закрытии (7).
В результате выполнения этой программы вывод будет примерно следующим:
PrintingAutoCloseable created
Exception in thread "main" java.lang.IllegalStateException: I always throw
at ru.misc.Main$ThrowingAutoCloseable.<init>(Main.java:19)
at ru.misc.Main.main(Main.java:9)
Как видно, PrintingAutoCloseable
закрыт не был!
Вопрос:
В каком порядке закрываются ресурсы, объявленные в try-with-resources?
Ответ:
В обратном.
Пример:
public class Main { public static void main(String[] args) throws Exception { try (PrintingAutoCloseable printingAutoCloseable1 = new PrintingAutoCloseable("1"); PrintingAutoCloseable printingAutoCloseable2 = new PrintingAutoCloseable("2"); PrintingAutoCloseable printingAutoCloseable3 = new PrintingAutoCloseable("3")) { } } private static class PrintingAutoCloseable implements AutoCloseable { private final String id; public PrintingAutoCloseable(String id) { this.id = id; } @Override public void close() { System.out.println("Closed " + id); } } }
Вывод:
Closed 3
Closed 2
Closed 1
Общие советы
Избегайте генерации исключений, если их можно избежать простой проверкой
Как уже было сказано выше, исключения ломают поток выполнения программы. Если же на сгенерированное исключение не найдется обработчика, не будет подходящего catch
блока, то программа и вовсе будет завершена. Кроме того, генерация исключения это довольно дорогостоящая операция.
Помните, что если исключение можно не допустить, то лучше так и сделать.
Отсюда следует первый совет: не брезгуйте дополнительными проверками.
- Не ловите
IllegalArgumentException
,NullPointerException
,ArrayIndexOutOfBoundsException
и подобные.
Потому что эти ошибки — это явная отсылка к тому, что где-то недостает проверки.
Обращение по индексу за пределами массива,NullPointerException
, все эти исключения — это ошибка разработчика. - Вводите дополнительные проверки на данные, дабы избежать возникновения непроверяемых исключения
Например, запретите вводить в поле возраста не числовые значения, проверяйте ссылки на null
перед обращением и т.д.
Предпочитайте Optional
, если отсутствие значения — не исключительная ситуация
При написании API
к каким-то хранилищам или коллекциям очень часто на отсутствие элемента генерируется исключение, как например в разделе собственные исключения.
class Catalog { Person findPerson(String name); }
Но и в этом случае генерации исключения можно избежать, если воспользоваться java.util.Optional
:
Optional<Person> findPerson(String name);
Класс java.util.Optional
был добавлен в Java 8
и предназначен как раз для подобных ситуаций, когда возвращаемого значения может не быть. В зависимости от задачи и контекста можно как генерировать исключение, как это сделано в примере с PersonNotFoundException
, так и изменить сигнатуру метода, воспользовавшись java.util.Optional
.
Отсюда следует второй совет: думайте над API
ваших классов, исключений можно избежать воспользовавшись другим подходом.
Заранее обдумывайте контракты методов
Важным моментом, который нельзя не упомянуть, является то, что если в методе объявляется, что он может сгенерировать исключение (с помощью throws
), то при переопределении такого метода нельзя указать более общее исключение в качестве выбрасываемого.
class Person { void hello() throws RuntimeException { // some code } } // Compile Error class PPerson extends Person { @Override void hello() throws Exception { // some code } }
Если было явно указано, что метод может сгенерировать java.lang.RuntimeException
, то нельзя объявить более общее бросаемое исключение при переопределении. Но можно указать потомка:
// IllegalArgumentException - потомок RuntimeException! class PPerson extends Person { @Override void hello() throws IllegalArgumentException { // some code } }
Что, в целом логично.
Если объявляется, что метод может сгенерировать java.lang.RuntimeException
, а он выбрасывает java.io.IOException
, то это было бы как минимум странно.
Это объясняется и с помощью полимофризма. Пусть есть интерфейс, в котором объявлен метод, генерирующий исключение. Если полиморфно работать с объектом через общий интерфейс, то разработчик обязан обработать исключение, объявленное в интерфейсе, а если одна из реализаций интерфейса генерирует более общее исключение, то это нарушает полиморфизм. Поэтому такой код даже не скомпилируется.
При этом при переопределении можно вообще не объявлять бросаемые исключения, таким образом сообщив, что все проблемы будут решены в методе:
class PPerson extends Person { @Override void hello() { // some code } }
Отсюда следует третий совет: необходимо думать о тех исключениях, которые делегирует метод, если класс может участвовать в наследовании.
Предпочитайте исключения кодам ошибок и boolean
флагам-признакам успеха
- Исключения более информативны: они позволяют передать сообщение с описанием ошибки
- Исключение практически невозможно проигнорировать
- Исключение может быть обработано кодом, находящимся выше по стеку, а
boolean
-флаг или код ошибки необходимо обрабатывать здесь и сейчас
Исключения и статические блоки
Еще интересно поговорить про то, что происходит, если исключение возникает в статическом блоке.
Так вот, такие исключения оборачиваются в java.lang.ExceptionInInitializerError
:
public class ExceptionHandling { static { throwRuntimeException(); } private static void throwRuntimeException() { throw new NullPointerException(); } public static void main(String[] args) { System.out.println("Hello World"); } }
Результатом будет падение со следующим стектрейсом:
java.lang.ExceptionInInitializerError Caused by: java.lang.NullPointerException at exception.test.ExceptionHandling.throwRuntimeException(ExceptionHandling.java:13) at exception.test.ExceptionHandling. (ExceptionHandling.java:8)
Многопоточность и исключения
Код в Java
потоке выполняется в методе со следующей сигнатурой:
Что делает невозможным пробрасывание проверяемых исключений, т.е разработчик должен обрабатывать все проверяемые исключения внутри метода run
.
Непроверяемые исключения обрабатывать необязательно, однако необработанное исключение, выброшенное из run
, завершит работу потока.
Например:
public class ExceptionHandling4 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread() { @Override public void run() { throw new RuntimeException("Testing unhandled exception processing."); } }; t.start(); } }
Результатом выполнения этого кода будет то, что возникшее исключение прервет поток исполнения (interrupt thread):
Exception in thread “Thread-0” java.lang.RuntimeException: Testing unhandled exception processing. at exception.test. ExceptionHandling4$1.run(ExceptionHandling4.java:27)
При использовании нескольких потоков бывают ситуации, когда надо знать, как поток завершился, из-за какого именно исключения. И, разумеется, отреагировать на это.
В таких ситуациях рекомендуется использовать Thread.UncaughtExceptionHandler
.
t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { System.out.println("Handled uncaught exception in thread :" + t + " Exception : " + e); } });
И вывод уже будет:
Handled uncaught exception in thread :Thread[Thread-0,5,main] Exception : java.lang.RuntimeException: Testing unhandled exception processing.
Необработанное исключение RuntimeException("Testing unhandled exception processing.")
, убившее поток, было перехвачено специальным зарегистрированным обработчиком.
Проверяемые исключения и их необходимость
В большинстве языков программирования, таких как C#
, Scala
, Groovy
, Python
и т.д., нет такого разделения, как в Java
, на проверяемые и непроверяемые исключения.
Почему оно введено в Java
было разобрано выше, а вот почему проверяемые исключения недолюбливают разработчики?
Основных причин две, это причины с: версионированием и масштабируемостью.
Представим, что вы, как разработчик библиотеки, объявили некоторый условный метод foo
, бросающий исключения A
, B
и C
:
void foo() throws A, B, C;
В следующей версии библиотеки в метод foo
добавили функциональности и теперь он бросает еще новое исключение D
:
void foo() throws A, B, C, D;
В таком случае новая версия библиотеки сломает код тех, кто ей пользуется. Это сравнимо с тем, что добавляется новый метод в интерфейс.
И с одной стороны, это правильно, так как в новой версии добавляется еще одно исключение и те, кто использует библиотеку должны отреагировать на все новые исключения. С другой стороны, чаще всего такие исключения будут также проброшены дальше. Все дело в том, что случаев, когда можно обработать специфический тип исключения, например тот же D
или A
в примере выше, и сделать в обработчике что-то интеллектуальное, можно пересчитать по пальцам одной руки.
Проблема с масштабируемостью начинается тогда, когда происходит вызов не одного, а нескольких API
, каждый из которых также несет с собой проверяемые исключения. Представьте, что помимо foo
, бросающего A
, B
, C
и D
, в методе hello
вызывается еще и bar
, который также бросает E
и T
исключения. Как сказано выше, как реагировать чаще всего непонятно, поэтому эти исключения делегируются вызывающему коду, из-за чего объявление метода hello
выглядит совсем уж угрожающе:
void hello() throws A, B, C, D, E, T { try { foo(); bar(); } finally { // clear resources if needed } }
Все это настолько раздражающе, что чаще всего разработчики просто объявляют наиболее общее исключение в throws
:
void hello() throws Exception { try { foo(); bar(); } finally { // clear resources if needed } }
А в таком случае это все равно, что сказать «метод может выбросить исключение» — это настолько общие и абстрактные слова, что смысла в throws Exception
практически нет.
Также есть еще одна проблема с проверяемыми исключениями. Это то, что с проверяемыми исключениями крайне неудобно работать в lambda
-ах и stream
-ах:
// compilation error Lists.newArrayList("a", "asg").stream().map(e -> {throw new Exception();});
Так как с Java 8
использование lambda
и stream
-ов распространенная практика, то накладываемые ограничения вызовут дополнительные трудности при использовании проверяемых исключений.
Поэтому многие разработчики недолюбливают проверяемые исключения, например, оборачивая их в непроверяемые аналоги с помощью re-throw
.
Мое мнение таково: на проверяемых исключениях очень хорошо учиться. Компилятор и язык сами подсказывают вам, что нельзя игнорировать исключения и требуют от вас реакции. Опять же, логическое разделение на проверяемые и непроверяемые помогает в понимании исключений, в понимании того, как и на что реагировать. В промышленной же разработке это становится уже больше раздражающим фактором.
В своей работе я стараюсь чаще использовать непроверяемые исключения, а проверяемые оборачивать в unchecked
аналоги, как, например, java.io.IOException
и java.io.UncheckedIOException
.
Заключение
Иерархия исключений в Java
.
Исключения делятся на два типа: непроверяемые(unchecked
) и проверяемые(checked
). Проверяемые исключения — это исключения, которые проверяются на этапе компиляции, мы обязаны на них отреагировать.
Проверяемые исключения в Java
используются тогда, когда разработчик никак не может предотвратить их возникновение. Причину возникновения java.lang.RuntimeException
можно проверить и устранить заранее, например, проверить ссылку на null
перед вызовом метода, на объекте по ссылке. А вот с причинами проверяемых исключений так сделать не получится, так как ошибка при чтении файла может возникнуть непосредственно в момент чтения, потому что другая программа его удалила. Соответственно, при чтении файла требуется обрабатывать java.io.IOException
, который является потомком java.lang.Exception
.
Допускается создание собственных исключений, признак проверяемости или непроверяемости наследуется от родителя. Исключения — это такие же классы, со своим поведением и состоянием, поэтому при наследовании вполне допускается добавить дополнительное поведение или свойства классу.
Обработка исключений происходит с помощью конструкции try/catch/finally
. Один блок try
может иметь несколько catch
блоков. В таком случае будет выполняться первый подходящий блок.
Помните, что try
блок не транзакционен, все ресурсы, занятые в try
ДО исключения остаются в памяти. Их надо освобождать и очищать вручную.
Если вы используете Java
версии 7 и выше, то отдавайте предпочтение конструкции try-with-resources
.
Основное правило:
На исключения можно реагировать, их обработку можно делегировать, но ни в коем случае нельзя их игнорировать.
Определить когда надо реагировать, а когда делегировать проще простого. Задайте вопрос: «Знаю ли я как реагировать на это исключение?».
Если ответ «да, знаю», то реагируйте, пишите обработчик и код, отвечающий за эту реакцию, если не знаете что делать с исключением, то делегируйте вызывающему коду.
Помните, что перехват java.lang.Error
стоит делать только если вы точно знаете, что делаете. Восстановление после таких ошибок не всегда возможно и почти всегда нетривиально.
Не забывайте, что большинство ошибок java.lang.RuntimeException
и его потомков можно избежать.
Не бойтесь создавать собственные исключения, так как это позволит писать более гибкие обработчики, а значит более точно реагировать на проблемы.
Представьте себе, что существуют пять причин, по которым может быть выброшено исключение, и во всех пяти случаях бросается
java.lang.Exception
. Вы же спятите разбираться, чем именно это исключение вызвано.(c) Евгений Матюшкин.
Помните, что исключения ломают поток выполнения программы, поэтому чем раньше вы обработаете возникшую проблему, тем лучше. Отсюда же следует совет, что лучше не разбрасываться исключениями, так как помимо того, что это ломает поток выполнения, это еще и дорогостоящая операция.
Постарайтесь не создавать ‘универсальных’ обработчиков, так как это чревато трудноуловимыми ошибками.
Если исключение можно не генерировать, то лучше так и сделать. Не пренебрегайте проверками.
Старайтесь продумывать то, как вы будете реагировать на исключения, не игнорировать их, использовать только try-с-ресурсами
.
Помните:
In Java you can ignore exceptions, but you have to willfully do it. You can’t accidentally say, «I don’t care.» You have to explicitly say, «I don’t care.»
(c) James Gosling.
Для закрепления материала рекомендую ознакомиться с ссылками ниже и этим материалом.
Полезные ссылки
- Книга С. Стелтинг ‘Java без сбоев: обработка исключений, тестирование, отладка’
- Oracle Java Tutorials
- Лекция Технострим Исключения
- Лекция OTUS Исключения в Java
- Лекция Ивана Пономарёва по исключениям
- Заметка Евгения Матюшкина про Исключения
- Failure and Exceptions by James Gosling
- The Trouble with Checked Exceptions by Bill Venners with Bruce Eckel
- Никто не умеет обрабатывать ошибки
- Исключения и обобщенные типы в Java
- Вопросы для закрепления