Время на прочтение
23 мин
Количество просмотров 424K
Это первая часть статьи, посвященной такому языковому механизму Java как исключения (вторая (checked/unchecked) вот). Она имеет вводный характер и рассчитана на начинающих разработчиков или тех, кто только приступает к изучению языка.
Также я веду курс «Scala for Java Developers» на платформе для онлайн-образования udemy.com (аналог Coursera/EdX).
1. Ключевые слова: try, catch, finally, throw, throws
2. Почему используем System.err, а не System.out
3. Компилятор требует вернуть результат (или требует молчать)
4. Нелокальная передача управления (nonlocal control transfer)
5. try + catch (catch — полиморфен)
6. try + catch + catch + …
7. try + finally
8. try + catch + finally
9. Вложенные try + catch + finally
1. Ключевые слова: try, catch, finally, throw, throws
Механизм исключительных ситуаций в Java поддерживается пятью ключевыми словами
- try
- catch
- finally
- throw
- throws
«Магия» (т.е. некоторое поведение никак не отраженное в исходном коде и потому неповторяемое пользователем) исключений #1 заключается в том, что catch, throw, throws можно использовать исключительно с java.lang.Throwable или его потомками.
throws:
Годится
public class App {
public static void main(String[] args) throws Throwable {}
}
Не годится
public class App {
public static void main(String[] args) throws String {}
}
>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'
catch:
Годится
public class App {
public static void main(String[] args) {
try {
} catch (Throwable t) {}
}
}
Не годится
public class App {
public static void main(String[] args) {
try {
} catch (String s) {}
}
}
>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'
throw:
Годится
public class App {
public static void main(String[] args) {
// Error - потомок Throwable
throw new Error();
}
}
Не годится
public class App {
public static void main(String[] args) {
throw new String("Hello!");
}
}
>> COMPILATION ERROR: Incompatible types: required 'java.lang.Throwable', found: 'java.lang.String'
Кроме того, throw требуется не-null аргумент, иначе NullPointerException в момент выполнения
public class App {
public static void main(String[] args) {
throw null;
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.NullPointerException
throw и new — это две независимых операции. В следующем коде мы независимо создаем объект исключения и «бросаем» его
public class App {
public static void main(String[] args) {
Error ref = new Error(); // создаем экземпляр
throw ref; // "бросаем" его
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
Однако, попробуйте проанализировать вот это
public class App {
public static void main(String[] args) {
f(null);
}
public static void f(NullPointerException e) {
try {
throw e;
} catch (NullPointerException npe) {
f(npe);
}
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.StackOverflowError
2. Почему используем System.err, а не System.out
System.out — buffered-поток вывода, а System.err — нет. Таким образом вывод может быть как таким
public class App {
public static void main(String[] args) {
System.out.println("sout");
throw new Error();
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
>> sout
Так и вот таким (err обогнало out при выводе в консоль)
public class App {
public static void main(String[] args) {
System.out.println("sout");
throw new Error();
}
}
>> sout
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
Давайте это нарисуем
буфер сообщений
+----------------+
+->| msg2 msg1 msg0 | --> out
/ +----------------+ \
/ +-> +--------+
ВАШЕ ПРИЛОЖЕНИЕ | КОНСОЛЬ|
\ +-> +--------+
\ /
+------------------------> err
нет буфера, сразу печатаем
когда Вы пишете в System.err — ваше сообщение тут же выводится на консоль, но когда пишете в System.out, то оно может на какое-то время быть буферизированно. Stacktrace необработанного исключение выводится через System.err, что позволяет им обгонять «обычные» сообщения.
3. Компилятор требует вернуть результат (или требует молчать)
Если в объявлении метода сказано, что он возвращает НЕ void, то компилятор зорко следит, что бы мы вернули экземпляр требуемого типа или экземпляр типа, который можно неявно привести к требуемому
public class App {
public double sqr(double arg) { // надо double
return arg * arg; // double * double - это double
}
}
public class App {
public double sqr(double arg) { // надо double
int k = 1; // есть int
return k; // можно неявно преобразовать int в double
}
}
// на самом деле, компилятор сгенерирует байт-код для следующих исходников
public class App {
public double sqr(double arg) { // надо double
int k = 1; // есть int
return (double) k; // явное преобразование int в double
}
}
вот так не пройдет (другой тип)
public class App {
public static double sqr(double arg) {
return "hello!";
}
}
>> COMPILATION ERROR: Incompatible types. Required: double. Found: java.lang.String
Вот так не выйдет — нет возврата
public class App {
public static double sqr(double arg) {
}
}
>> COMPILATION ERROR: Missing return statement
и вот так не пройдет (компилятор не может удостовериться, что возврат будет)
public class App {
public static double sqr(double arg) {
if (System.currentTimeMillis() % 2 == 0) {
return arg * arg; // если currentTimeMillis() - четное число, то все ОК
}
// а если нечетное, что нам возвращать?
}
}
>> COMPILATION ERROR: Missing return statement
Компилятор отслеживает, что бы мы что-то вернули, так как иначе непонятно, что должна была бы напечатать данная программа
public class App {
public static void main(String[] args) {
double d = sqr(10.0); // ну, и чему равно d?
System.out.println(d);
}
public static double sqr(double arg) {
// nothing
}
}
>> COMPILATION ERROR: Missing return statement
Из-забавного, можно ничего не возвращать, а «повесить метод»
public class App {
public static double sqr(double arg) {
while (true); // Удивительно, но КОМПИЛИРУЕТСЯ!
}
}
Тут в d никогда ничего не будет присвоено, так как метод sqr повисает
public class App {
public static void main(String[] args) {
double d = sqr(10.0); // sqr - навсегда "повиснет", и
System.out.println(d); // d - НИКОГДА НИЧЕГО НЕ БУДЕТ ПРИСВОЕНО!
}
public static double sqr(double arg) {
while (true); // Вот тут мы на века "повисли"
}
}
Компилятор пропустит «вилку» (таки берем в квадрат ИЛИ висим)
public class App {
public static double sqr(double arg) {
if (System.currentTimeMillis() % 2 == 0) {
return arg * arg; // ну ладно, вот твой double
} else {
while (true); // а тут "виснем" навсегда
}
}
}
Но механизм исключений позволяет НИЧЕГО НЕ ВОЗВРАЩАТЬ!
public class App {
public static double sqr(double arg) {
throw new RuntimeException();
}
}
Итак, у нас есть ТРИ варианта для компилятора
public class App {
public static double sqr(double arg) {// согласно объявлению метода ты должен вернуть double
long time = System.currentTimeMillis();
if (time % 2 == 0) {
return arg * arg; // ок, вот твой double
} else if (time % 2 == 1) { {
while (true); // не, я решил "повиснуть"
} else {
throw new RuntimeException(); // или бросить исключение
}
}
}
Но КАКОЙ ЖЕ double вернет функция, бросающая RuntimeException?
А НИКАКОЙ!
public class App {
public static void main(String[] args) {
// sqr - "сломается" (из него "выскочит" исключение),
double d = sqr(10.0); // выполнение метода main() прервется в этой строчке и
// d - НИКОГДА НИЧЕГО НЕ БУДЕТ ПРИСВОЕНО!
System.out.println(d); // и печатать нам ничего не придется!
}
public static double sqr(double arg) {
throw new RuntimeException(); // "бросаем" исключение
}
}
>> RUNTIME ERROR: Exception in thread "main" java.lang.RuntimeException
Подытожим: бросаемое исключение — это дополнительный возвращаемый тип. Если ваш метод объявил, что возвращает double, но у вас нет double — можете бросить исключение. Если ваш метод объявил, что ничего не возвращает (void), но у вам таки есть что сказать — можете бросить исключение.
Давайте рассмотрим некоторый пример из практики.
Задача: реализовать функцию, вычисляющую площадь прямоугольника
public static int area(int width, int height) {...}
важно, что задание звучит именно так, в терминах предметной области — «вычислить площадь прямоугольника», а не в терминах решения «перемножить два числа»:
public static int area(int width, int height) {
return width * height; // тут просто перемножаем
}
Вопрос: что делать, если мы обнаружили, что хотя бы один из аргументов — отрицательное число?
Если просто умножить, то мы пропустили ошибочные данные дальше. Что еще хуже, возможно, мы «исправили ситуацию» — сказали что площадь прямоугольника с двумя отрицательными сторонами -10 и -20 = 200.
Мы не можем ничего не вернуть
public static int area(int width, int height) {
if (width < 0 || height < 0) {
// у вас плохие аргументы, извините
} else {
return width * height;
}
}
>> COMPILATION ERROR: Missing return statement
Можно, конечно, отписаться в консоль, но кто ее будет читать и как определить где была поломка. При чем, вычисление то продолжится с неправильными данными
public static int area(int width, int height) {
if (width < 0 || height < 0) {
System.out.println("Bad ...");
}
return width * height;
}
Можно вернуть специальное значение, показывающее, что что-то не так (error code), но кто гарантирует, что его прочитают, а не просто воспользуются им?
public static int area(int width, int height) {
if (width < 0 || height < 0) {
return -1; // специальное "неправильное" значение площади
}
return width * height;
}
Можем, конечно, целиком остановить виртуальную машину
public static int area(int width, int height) {
if (width < 0 || height < 0) {
System.exit(0);
}
return width * height;
}
Но «правильный путь» таков: если обнаружили возможное некорректное поведение, то
1. Вычисления остановить, сгенерировать сообщение-поломку, которое трудно игнорировать, предоставить пользователю информацию о причине, предоставить пользователю возможность все починить (загрузить белье назад и повторно нажать кнопку старт)
public static int area(int width, int height) {
if (width < 0 || height < 0) {
throw new IllegalArgumentException("Negative sizes: w = " + width + ", h = " + height);
}
return width * height;
}
4. Нелокальная передача управления (nonlocal control transfer)
Механизм исключительных ситуация (исключений) — это механизм НЕЛОКАЛЬНОЙ ПЕРЕДАЧИ УПРАВЛЕНИЯ.
Что под этим имеется в виду?
Программа, в ходе своего выполнения (точнее исполнения инструкций в рамках отдельного потока), оперирует стеком («стопкой») фреймов. Передача управления осуществляется либо в рамках одного фрейма
public class App {
public static void main(String[] args) {
// Пример: ОПЕРАТОР ПОСЛЕДОВАТЕЛЬНОСТИ
int x = 42; // первый шаг
int y = x * x; // второй шаг
x = x * y; // третий шаг
...
}
}
public class App {
public static void main(String[] args) {
// Пример: ОПЕРАТОР ВЕТВЛЕНИЯ
if (args.length > 2) { первый шаг
// второй шаг или тут
...
} else {
// или тут
...
}
// третий шаг
...
}
}
public class App {
public static void main(String[] args) {
// Пример: ОПЕРАТОР ЦИКЛА do..while
int x = 1;
do {
...
} while (x++ < 10);
...
}
}
и другие операторы.
Либо передача управления происходит в «стопке» фреймов между СОСЕДНИМИ фреймами
- вызов метода: создаем новый фрейм, помещаем его на верхушку стека и переходим в него
- выход из метода: возвращаемся к предыдущему фрейму (через return или просто кончились инструкции в методе)
return — выходим из ОДНОГО фрейма (из фрейма #4(метод h()))
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
f(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println("#1.out"); // вернулись
} // выходим из текущего фрейма, кончились инструкции
public static void f() {
System.err.println(". #2.in");
g(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". #2.out"); //вернулись
} // выходим из текущего фрейма, кончились инструкции
public static void g() {
System.err.println(". . #3.in");
h(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". . #3.out"); // вернулись
} // выходим из текущего фрейма, кончились инструкции
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.RETURN");
return; // выходим из текущего фрейма по 'return'
}
System.err.println(". . . #4.out"); // ПРОПУСКАЕМ
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.RETURN
>> . . #3.out
>> . #2.out
>> #1.out
throw — выходим из ВСЕХ фреймов
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
f(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println("#1.out"); // ПРОПУСТИЛИ!
}
public static void f() {
System.err.println(". #2.in");
g(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". #2.out"); // ПРОПУСТИЛИ!
}
public static void g() {
System.err.println(". . #3.in");
h(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". . #3.out"); // ПРОПУСТИЛИ!
}
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.THROW");
throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
}
System.err.println(". . . #4.out"); // ПРОПУСТИЛИ!
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.THROW
>> RUNTIME ERROR: Exception in thread "main" java.lang.Error
При помощи catch мы можем остановить летящее исключение (причина, по которой мы автоматически покидаем фреймы).
Останавливаем через 3 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g()) + фрейм #2(метод f())
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
try {
f(); // создаем фрейм, помещаем в стек, передаем в него управление
} catch (Error e) { // "перехватили" "летящее" исключение
System.err.println("#1.CATCH"); // и работаем
}
System.err.println("#1.out"); // работаем дальше
}
public static void f() {
System.err.println(". #2.in");
g(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". #2.out"); // ПРОПУСТИЛИ!
}
public static void g() {
System.err.println(". . #3.in");
h(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". . #3.out"); // ПРОПУСТИЛИ!
}
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.THROW");
throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
}
System.err.println(". . . #4.out"); // ПРОПУСТИЛИ!
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.THROW
>> #1.CATCH
>> #1.out
Обратите внимание, стандартный сценарий работы был восстановлен в методе main() (фрейм #1)
Останавливаем через 2 фрейма, пролетаем фрейм #4(метод h()) + пролетаем фрейм #3(метод g())
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
f(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println("#1.out"); // вернулись и работаем
}
public static void f() {
System.err.println(". #2.in");
try {
g(); // создаем фрейм, помещаем в стек, передаем в него управление
} catch (Error e) { // "перехватили" "летящее" исключение
System.err.println(". #2.CATCH"); // и работаем
}
System.err.println(". #2.out"); // работаем дальше
}
public static void g() {
System.err.println(". . #3.in");
h(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". . #3.out"); // ПРОПУСТИЛИ!
}
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.THROW");
throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
}
System.err.println(". . . #4.out"); // ПРОПУСТИЛИ!
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.THROW
>> . #2.CATCH
>> . #2.out
>> #1.out
Останавливаем через 1 фрейм (фактически аналог return, просто покинули фрейм «другим образом»)
public class App {
public static void main(String[] args) {
System.err.println("#1.in");
f(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println("#1.out"); // вернулись и работаем
}
public static void f() {
System.err.println(". #2.in");
g(); // создаем фрейм, помещаем в стек, передаем в него управление
System.err.println(". #2.out"); // вернулись и работаем
}
public static void g() {
System.err.println(". . #3.in");
try {
h(); // создаем фрейм, помещаем в стек, передаем в него управление
} catch (Error e) { // "перехватили" "летящее" исключение
System.err.println(". . #3.CATCH"); // и работаем
}
System.err.println(". . #3.out"); // работаем дальше
}
public static void h() {
System.err.println(". . . #4.in");
if (true) {
System.err.println(". . . #4.THROW");
throw new Error(); // выходим со всей пачки фреймов ("раскрутка стека") по 'throw'
}
System.err.println(". . . #4.out"); // ПРОПУСТИЛИ!
}
}
>> #1.in
>> . #2.in
>> . . #3.in
>> . . . #4.in
>> . . . #4.THROW
>> . . #3.CATCH
>> . . #3.out
>> . #2.out
>> #1.out
Итак, давайте сведем все на одну картинку
// ---Используем RETURN--- // ---Используем THROW---
// Выход из 1-го фрейма // Выход из ВСЕХ (из 4) фреймов
#1.in #1.in
. #2.in . #2.in
. . #3.in . . #3.in
. . . #4.in . . . #4.in
. . . #4.RETURN . . . #4.THROW
. . #3.out RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
. #2.out
#1.out
// ---Используем THROW+CATCH---
// Выход из 3-х фреймов // Выход из 2-х фреймов // Выход из 1-го фрейма
#1.in #1.in #1.in
. #2.in . #2.in . #2.in
. . #3.in . . #3.in . . #3.in
. . . #4.in . . . #4.in . . . #4.in
. . . #4.THROW . . . #4.THROW . . . #4.THROW
#1.CATCH . #2.CATCH . . #3.CATCH
#1.out . #2.out . . #3.out
#1.out . #2.out
#1.out
5. try + catch (catch — полиморфен)
Напомним иерархию исключений
Object
|
Throwable
/ \
Error Exception
|
RuntimeException
То, что исключения являются объектами важно для нас в двух моментах
1. Они образуют иерархию с корнем java.lang.Throwable (java.lang.Object — предок java.lang.Throwable, но Object — уже не исключение)
2. Они могут иметь поля и методы (в этой статье это не будем использовать)
По первому пункту: catch — полиморфная конструкция, т.е. catch по типу Parent перехватывает летящие экземпляры любого типа, который является Parent-ом (т.е. экземпляры непосредственно Parent-а или любого потомка Parent-а)
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (Exception e) { // catch по Exception ПЕРЕХВАТЫВАЕТ RuntimeException
System.err.print(" 2");
}
System.err.println(" 3");
}
}
>> 0 2 3
Даже так: в блоке catch мы будем иметь ссылку типа Exception на объект типа RuntimeException
public class App {
public static void main(String[] args) {
try {
throw new RuntimeException();
} catch (Exception e) {
if (e instanceof RuntimeException) {
RuntimeException re = (RuntimeException) e;
System.err.print("Это RuntimeException на самом деле!!!");
} else {
System.err.print("В каком смысле не RuntimeException???");
}
}
}
}
>> Это RuntimeException на самом деле!!!
catch по потомку не может поймать предка
public class App {
public static void main(String[] args) throws Exception { // пока игнорируйте 'throws'
try {
System.err.print(" 0");
if (true) {throw new Exception();}
System.err.print(" 1");
} catch (RuntimeException e) {
System.err.print(" 2");
}
System.err.print(" 3");
}
}
>> 0
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Exception
catch по одному «брату» не может поймать другого «брата» (Error и Exception не находятся в отношении предок-потомок, они из параллельных веток наследования от Throwable)
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new Error();}
System.err.print(" 1");
} catch (Exception e) {
System.err.print(" 2");
}
System.err.print(" 3");
}
}
>> 0
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
По предыдущим примерам — надеюсь вы обратили внимание, что если исключение перехвачено, то JVM выполняет операторы идущие ПОСЛЕ последних скобок try+catch.
Но если не перехвачено, то мы
1. не заходим в блок catch
2. покидаем фрейм метода с летящим исключением
А что будет, если мы зашли в catch, и потом бросили исключение ИЗ catch?
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (RuntimeException e) { // перехватили RuntimeException
System.err.print(" 2");
if (true) {throw new Error();} // но бросили Error
}
System.err.println(" 3"); // пропускаем - уже летит Error
}
}
>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
В таком случае выполнение метода тоже прерывается (не печатаем «3»). Новое исключение не имеет никакого отношения к try-catch
Мы можем даже кинуть тот объект, что у нас есть «на руках»
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (RuntimeException e) { // перехватили RuntimeException
System.err.print(" 2");
if (true) {throw e;} // и бросили ВТОРОЙ раз ЕГО ЖЕ
}
System.err.println(" 3"); // пропускаем - опять летит RuntimeException
}
}
>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.RuntimeException
И мы не попадем в другие секции catch, если они есть
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (RuntimeException e) { // перехватили RuntimeException
System.err.print(" 2");
if (true) {throw new Error();} // и бросили новый Error
} catch (Error e) { // хотя есть cath по Error "ниже", но мы в него не попадаем
System.err.print(" 3");
}
System.err.println(" 4");
}
}
>> 0 2
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
Обратите внимание, мы не напечатали «3», хотя у нас летит Error а «ниже» расположен catch по Error. Но важный момент в том, что catch имеет отношение исключительно к try-секции, но не к другим catch-секциям.
Как покажем ниже — можно строить вложенные конструкции, но вот пример, «исправляющий» эту ситуацию
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch (RuntimeException e) { // перехватили RuntimeException
System.err.print(" 2.1");
try {
System.err.print(" 2.2");
if (true) {throw new Error();} // и бросили новый Error
System.err.print(" 2.3");
} catch (Throwable t) { // перехватили Error
System.err.print(" 2.4");
}
System.err.print(" 2.5");
} catch (Error e) { // хотя есть cath по Error "ниже", но мы в него не попадаем
System.err.print(" 3");
}
System.err.println(" 4");
}
}
>> 0 2.1 2.2 2.4 2.5 4
6. try + catch + catch + …
Как вы видели, мы можем расположить несколько catch после одного try.
Но есть такое правило — нельзя ставить потомка после предка! (RuntimeException после Exception)
public class App {
public static void main(String[] args) {
try {
} catch (Exception e) {
} catch (RuntimeException e) {
}
}
}
>> COMPILATION ERROR: Exception 'java.lang.RuntimeException' has alredy been caught
Ставить брата после брата — можно (RuntimeException после Error)
public class App {
public static void main(String[] args) {
try {
} catch (Error e) {
} catch (RuntimeException e) {
}
}
}
Как происходит выбор «правильного» catch? Да очень просто — JVM идет сверху-вниз до тех пор, пока не найдет такой catch что в нем указано ваше исключение или его предок — туда и заходит. Ниже — не идет.
public class App {
public static void main(String[] args) {
try {
throw new Exception();
} catch (RuntimeException e) {
System.err.println("catch RuntimeException");
} catch (Exception e) {
System.err.println("catch Exception");
} catch (Throwable e) {
System.err.println("catch Throwable");
}
System.err.println("next statement");
}
}
>> catch Exception
>> next statement
Выбор catch осуществляется в runtime (а не в compile-time), значит учитывается не тип ССЫЛКИ (Throwable), а тип ССЫЛАЕМОГО (Exception)
public class App {
public static void main(String[] args) {
try {
Throwable t = new Exception(); // ссылка типа Throwable указывает на объект типа Exception
throw t;
} catch (RuntimeException e) {
System.err.println("catch RuntimeException");
} catch (Exception e) {
System.err.println("catch Exception");
} catch (Throwable e) {
System.err.println("catch Throwable");
}
System.err.println("next statement");
}
}
>> catch Exception
>> next statement
7. try + finally
finally-секция получает управление, если try-блок завершился успешно
public class App {
public static void main(String[] args) {
try {
System.err.println("try");
} finally {
System.err.println("finally");
}
}
}
>> try
>> finally
finally-секция получает управление, даже если try-блок завершился исключением
public class App {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
System.err.println("finally");
}
}
}
>> finally
>> Exception in thread "main" java.lang.RuntimeException
finally-секция получает управление, даже если try-блок завершился директивой выхода из метода
public class App {
public static void main(String[] args) {
try {
return;
} finally {
System.err.println("finally");
}
}
}
>> finally
finally-секция НЕ вызывается только если мы «прибили» JVM
public class App {
public static void main(String[] args) {
try {
System.exit(42);
} finally {
System.err.println("finally");
}
}
}
>> Process finished with exit code 42
System.exit(42) и Runtime.getRuntime().exit(42) — это синонимы
public class App {
public static void main(String[] args) {
try {
Runtime.getRuntime().exit(42);
} finally {
System.err.println("finally");
}
}
}
>> Process finished with exit code 42
И при Runtime.getRuntime().halt(42) — тоже не успевает зайти в finally
public class App {
public static void main(String[] args) {
try {
Runtime.getRuntime().halt(42);
} finally {
System.err.println("finally");
}
}
}
>> Process finished with exit code 42
exit() vs halt()
javadoc: java.lang.Runtime#halt(int status)
… Unlike the exit method, this method does not cause shutdown hooks to be started and does not run uninvoked finalizers if finalization-on-exit has been enabled. If the shutdown sequence has already been initiated then this method does not wait for any running shutdown hooks or finalizers to finish their work.
Однако finally-секция не может «починить» try-блок завершившийся исключение (заметьте, «more» — не выводится в консоль)
public class App {
public static void main(String[] args) {
try {
System.err.println("try");
if (true) {throw new RuntimeException();}
} finally {
System.err.println("finally");
}
System.err.println("more");
}
}
>> try
>> finally
>> Exception in thread "main" java.lang.RuntimeException
Трюк с «if (true) {…}» требуется, так как иначе компилятор обнаруживает недостижимый код (последняя строка) и отказывается его компилировать
public class App {
public static void main(String[] args) {
try {
System.err.println("try");
throw new RuntimeException();
} finally {
System.err.println("finally");
}
System.err.println("more");
}
}
>> COMPILER ERROR: Unrechable statement
И finally-секция не может «предотвратить» выход из метода, если try-блок вызвал return («more» — не выводится в консоль)
public class App {
public static void main(String[] args) {
try {
System.err.println("try");
if (true) {return;}
} finally {
System.err.println("finally");
}
System.err.println("more");
}
}
>> try
>> finally
Однако finally-секция может «перебить» throw/return при помощи другого throw/return
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
try {
return 0;
} finally {
return 1;
}
}
}
>> 1
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
try {
throw new RuntimeException();
} finally {
return 1;
}
}
}
>> 1
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
try {
return 0;
} finally {
throw new RuntimeException();
}
}
}
>> Exception in thread "main" java.lang.RuntimeException
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
try {
throw new Error();
} finally {
throw new RuntimeException();
}
}
}
>> Exception in thread "main" java.lang.RuntimeException
finally-секция может быть использована для завершающего действия, которое гарантированно будет вызвано (даже если было брошено исключение или автор использовал return) по окончании работы
// open some resource
try {
// use resource
} finally {
// close resource
}
Например для освобождения захваченной блокировки
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// some code
} finally {
lock.unlock();
}
Или для закрытия открытого файлового потока
InputStream input = new FileInputStream("...");
try {
// some code
} finally {
input.close();
}
Специально для этих целей в Java 7 появилась конструкция try-with-resources, ее мы изучим позже.
Вообще говоря, в finally-секция нельзя стандартно узнать было ли исключение.
Конечно, можно постараться написать свой «велосипед»
public class App {
public static void main(String[] args) {
System.err.println(f());
}
public static int f() {
long rnd = System.currenttimeMillis();
boolean finished = false;
try {
if (rnd % 3 == 0) {
throw new Error();
} else if (rnd % 3 == 1) {
throw new RuntimeException();
} else {
// nothing
}
finished = true;
} finally {
if (finished) {
// не было исключений
} else {
// было исключение, но какое?
}
}
}
}
Не рекомендуемые практики
— return из finally-секции (можем затереть исключение из try-блока)
— действия в finally-секции, которые могут бросить исключение (можем затереть исключение из try-блока)
8. try + catch + finally
Нет исключения
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
// nothing
System.err.print(" 1");
} catch(Error e) {
System.err.print(" 2");
} finally {
System.err.print(" 3");
}
System.err.print(" 4");
}
}
>> 0 1 3 4
Не заходим в catch, заходим в finally, продолжаем после оператора
Есть исключение и есть подходящий catch
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new Error();}
System.err.print(" 1");
} catch(Error e) {
System.err.print(" 2");
} finally {
System.err.print(" 3");
}
System.err.print(" 4");
}
}
>> 0 2 3 4
Заходим в catch, заходим в finally, продолжаем после оператора
Есть исключение но нет подходящего catch
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
if (true) {throw new RuntimeException();}
System.err.print(" 1");
} catch(Error e) {
System.err.print(" 2");
} finally {
System.err.print(" 3");
}
System.err.print(" 4");
}
}
>> 0 3
>> RUNTIME ERROR: Exception in thread "main" java.lang.RuntimeException
Не заходим в catch, заходим в finally, не продолжаем после оператора — вылетаем с неперехваченным исключением
9. Вложенные try + catch + finally
Операторы обычно допускают неограниченное вложение.
Пример с if
public class App {
public static void main(String[] args) {
if (args.length > 1) {
if (args.length > 2) {
if (args.length > 3) {
...
}
}
}
}
}
Пример с for
public class App {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; i++) {
for (int k = 0; k < 10; k++) {
...
}
}
}
}
}
Суть в том, что try-cacth-finally тоже допускает неограниченное вложение.
Например вот так
public class App {
public static void main(String[] args) {
try {
try {
try {
...
} catch (Exception e) {
} finally {}
} catch (Exception e) {
} finally {}
} catch (Exception e) {
} finally {}
}
}
Или даже вот так
public class App {
public static void main(String[] args) {
try {
try {
...
} catch (Exception e) {
...
} finally {
...
}
} catch (Exception e) {
try {
...
} catch (Exception e) {
...
} finally {
...
}
} finally {
try {
...
} catch (Exception e) {
...
} finally {
...
}
}
}
}
Ну что же, давайте исследуем как это работает.
Вложенный try-catch-finally без исключения
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
try {
System.err.print(" 1");
// НИЧЕГО
System.err.print(" 2");
} catch (RuntimeException e) {
System.err.print(" 3"); // НЕ заходим - нет исключения
} finally {
System.err.print(" 4"); // заходим всегда
}
System.err.print(" 5"); // заходим - выполнение в норме
} catch (Exception e) {
System.err.print(" 6"); // НЕ заходим - нет исключения
} finally {
System.err.print(" 7"); // заходим всегда
}
System.err.print(" 8"); // заходим - выполнение в норме
}
}
>> 0 1 2 4 5 7 8
Мы НЕ заходим в обе catch-секции (нет исключения), заходим в обе finally-секции и выполняем обе строки ПОСЛЕ finally.
Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНУТРЕННИЙ catch
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
try {
System.err.print(" 1");
if (true) {throw new RuntimeException();}
System.err.print(" 2");
} catch (RuntimeException e) {
System.err.print(" 3"); // ЗАХОДИМ - есть исключение
} finally {
System.err.print(" 4"); // заходим всегда
}
System.err.print(" 5"); // заходим - выполнение УЖЕ в норме
} catch (Exception e) {
System.err.print(" 6"); // не заходим - нет исключения, УЖЕ перехвачено
} finally {
System.err.print(" 7"); // заходим всегда
}
System.err.print(" 8"); // заходим - выполнение УЖЕ в норме
}
}
>> 0 1 3 4 5 7 8
Мы заходим в ПЕРВУЮ catch-секцию (печатаем «3»), но НЕ заходим во ВТОРУЮ catch-секцию (НЕ печатаем «6», так как исключение УЖЕ перехвачено первым catch), заходим в обе finally-секции (печатаем «4» и «7»), в обоих случаях выполняем код после finally (печатаем «5»и «8», так как исключение остановлено еще первым catch).
Вложенный try-catch-finally с исключением, которое ПЕРЕХВАТИТ ВНЕШНИЙ catch
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
try {
System.err.print(" 1");
if (true) {throw new Exception();}
System.err.print(" 2");
} catch (RuntimeException e) {
System.err.print(" 3"); // НЕ заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
} finally {
System.err.print(" 4"); // заходим всегда
}
System.err.print(" 5"); // не заходим - выполнение НЕ в норме
} catch (Exception e) {
System.err.print(" 6"); // ЗАХОДИМ - есть подходящее исключение
} finally {
System.err.print(" 7"); // заходим всегда
}
System.err.print(" 8"); // заходим - выполнение УЖЕ в норме
}
}
>> 0 1 4 6 7 8
Мы НЕ заходим в ПЕРВУЮ catch-секцию (не печатаем «3»), но заходим в ВТОРУЮ catch-секцию (печатаем «6»), заходим в обе finally-секции (печатаем «4» и «7»), в ПЕРВОМ случае НЕ выполняем код ПОСЛЕ finally (не печатаем «5», так как исключение НЕ остановлено), во ВТОРОМ случае выполняем код после finally (печатаем «8», так как исключение остановлено).
Вложенный try-catch-finally с исключением, которое НИКТО НЕ ПЕРЕХВАТИТ
public class App {
public static void main(String[] args) {
try {
System.err.print(" 0");
try {
System.err.print(" 1");
if (true) {throw new Error();}
System.err.print(" 2");
} catch (RuntimeException e) {
System.err.print(" 3"); // НЕ заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
} finally {
System.err.print(" 4"); // заходим всегда
}
System.err.print(" 5"); // НЕ заходим - выполнение НЕ в норме
} catch (Exception e) {
System.err.print(" 6"); // не заходим - есть исключение, но НЕПОДХОДЯЩЕГО ТИПА
} finally {
System.err.print(" 7"); // заходим всегда
}
System.err.print(" 8"); // не заходим - выполнение НЕ в норме
}
}
>> 0 1 4 7
>> RUNTIME EXCEPTION: Exception in thread "main" java.lang.Error
Мы НЕ заходим в ОБЕ catch-секции (не печатаем «3» и «6»), заходим в обе finally-секции (печатаем «4» и «7») и в обоих случаях НЕ выполняем код ПОСЛЕ finally (не печатаем «5» и «8», так как исключение НЕ остановлено), выполнение метода прерывается по исключению.
Контакты
Я занимаюсь онлайн обучением Java (вот курсы программирования) и публикую часть учебных материалов в рамках переработки курса Java Core. Видеозаписи лекций в аудитории Вы можете увидеть на youtube-канале, возможно, видео канала лучше систематизировано в этой статье.
Мой метод обучения состоит в том, что я
- показываю различные варианты применения
- строю усложняющуюся последовательность примеров по каждому варианту
- объясняю логику двигавшую авторами (по мере возможности)
- даю большое количество тестов (50-100) всесторонне проверяющее понимание и демонстрирующих различные комбинации
- даю лабораторные для самостоятельной работы
Данная статье следует пунктам #1 (различные варианты) и #2(последовательность примеров по каждому варианту).
skype: GolovachCourses
email: GolovachCourses@gmail.com
Исключение (exception) — это ненормальная ситуация (термин «исключение» здесь следует понимать как «исключительная ситуация»), возникающая во время выполнения программного кода. Иными словами, исключение — это ошибка, возникающая во время выполнения программы (в runtime).
Исключение — это способ системы Java (в частности, JVM — виртуальной машины Java) сообщить вашей программе, что в коде произошла ошибка. К примеру, это может быть деление на ноль, попытка обратиться к массиву по несуществующему индексу, очень распространенная ошибка нулевого указателя (NullPointerException
) — когда вы обращаетесь к ссылочной переменной, у которой значение равно null и так далее.
В любом случае, с формальной точки зрения, Java не может продолжать выполнение программы.
Обработка исключений (exception handling) — название объектно-ориентированной техники, которая пытается разрешить эти ошибки.
Программа в Java может сгенерировать различные исключения, например:
-
программа может пытаться прочитать файл из диска, но файл не существует;
-
программа может попытаться записать файл на диск, но диск заполнен или не отформатирован;
-
программа может попросить пользователя ввести данные, но пользователь ввел данные неверного типа;
-
программа может попытаться осуществить деление на ноль;
-
программа может попытаться обратиться к массиву по несуществующему индексу.
Используя подсистему обработки исключений Java, можно управлять реакцией программы на появление ошибок во время выполнения. Средства обработки исключений в том или ином виде имеются практически во всех современных языках программирования. В Java подобные инструменты отличаются большей гибкостью, понятнее и удобнее в применении по сравнению с большинством других языков программирования.
Преимущество обработки исключений заключается в том, что она предусматривает автоматическую реакцию на многие ошибки, избавляя от необходимости писать вручную соответствующий код.
В Java все исключения представлены отдельными классами. Все классы исключений являются потомками класса Throwable
. Так, если в программе возникнет исключительная ситуация, будет сгенерирован объект класса, соответствующего определенному типу исключения. У класса Throwable
имеются два непосредственных подкласса: Exception
и Error
.
Исключения типа Error
относятся к ошибкам, возникающим в виртуальной машине Java, а не в прикладной программе. Контролировать такие исключения невозможно, поэтому реакция на них в приложении, как правило, не предусматривается. В связи с этим исключения данного типа не будут рассматриваться в книге.
Ошибки, связанные с работой программы, представлены отдельными подклассами, производными от класса Exception
. В частности, к этой категории относятся ошибки деления на нуль, выхода за пределы массива и обращения к файлам. Подобные ошибки следует обрабатывать в самой программе. Важным подклассом, производным от Exception
, является класс RuntimeException
, который служит для представления различных видов ошибок, часто возникающих во время выполнения программ.
Каждой исключительной ситуации поставлен в соответствие некоторый класс. Если подходящего класса не существует, то он может быть создан разработчиком.
Так как в Java ВСЁ ЯВЛЯЕТСЯ ОБЪЕКТОМ, то исключение тоже является объектом некоторого класса, который описывает исключительную ситуацию, возникающую в определенной части программного кода.
«Обработка исключений» работает следующим образом:
-
когда возникает исключительная ситуация, JVM генерирует (говорят, что JVM ВЫБРАСЫВАЕТ исключение, для описания этого процесса используется ключевое слово
throw
) объект исключения и передает его в метод, в котором произошло исключение; -
вы можете перехватить исключение (используется ключевое слово
catch
), чтобы его каким-то образом обработать. Для этого, необходимо определить специальный блок кода, который называется обработчиком исключений, этот блок будет выполнен при возникновении исключения, код должен содержать реакцию на исключительную ситуацию; -
таким образом, если возникнет ошибка, все необходимые действия по ее обработке выполнит обработчик исключений.
Если вы не предусмотрите обработчик исключений, то исключение будет перехвачено стандартным обработчиком Java. Стандартный обработчик прекратит выполнение программы и выведет сообщение об ошибке.
Рассмотрим пример исключения и реакцию стандартного обработчика Java.
public static void main(String[] args) {
System.out.println(5 / 0);
Мы видим, что стандартный обработчик вывел в консоль сообщение об ошибке. Давайте разберемся с содержимым этого сообщения:
«C:\Program Files\Java\jdk1.8.0_60\bin\java»…
Exception in thread «main» java.lang.ArithmeticException: / by zero
at ua.opu.Main.main(Main.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Process finished with exit code 1
Exception in thread «main» java.lang.ArithmeticException: / by zero
сообщает нам тип исключения, а именно класс ArithmeticException
(про классы исключений мы будем говорить позже), после чего сообщает, какая именно ошибка произошла. В нашем случае это деление на ноль.
at ua.opu.Main.main(Main.java:6)
в каком классе, методе и строке произошло исключение. Используя эту информацию, мы можем найти ту строчку кода, которая привела к исключительной ситуации, и предпринять какие-то действия. Строки
at ua.opu.Main.main(Main.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
называются «трассировкой стека» (stack tracing). О каком стеке идет речь? Речь идет о стеке вызовов (call stack). Соответственно, эти строки означают последовательность вызванных методов, начиная от метода, в котором произошло исключение, заканчивая самым первым вызванным методом.
Для вызова методов в программе используется инструкция «call». Когда вы вызываете метод в программе, важно сохранить адрес следующей инструкции, чтобы, когда вызванный метод отработал, программа продолжила работу со следующей инструкции. Этот адрес нужно где-то хранить в памяти. Также перед вызовом необходимо сохранить аргументы функции, которые тоже необходимо где-то хранить.
Вся эта информация хранится в специальной структуре – стеке вызовов. Каждая запись в стеке вызовов называется кадром или фреймом (stack frame).
Таким образом, зная, какая строка привела к возникновению исключения, вы можете изменить код либо предусмотреть обработчик событий.
Как уже было сказано выше, исключение это объект некоторого класса. В Java существует разветвленная иерархия классов исключений.
В Java, класс исключения служит для описания типа исключения. Например, класс NullPointerException
описывает исключение нулевого указателя, а FileNotFoundException
означает исключение, когда файл, с которым пытается работать приложение, не найден. Рассмотрим иерархию классов исключений:
На самом верхнем уровне расположен класс Throwable
, который является базовым для всех исключений (как мы помним, JVM «выбрасывает» исключение», поэтому класс Throwable
означает – то, что может «выбросить» JVM).
От класса Throwable
наследуются классы Error
и Exception
. Среди подклассов Exception
отдельно выделен класс RuntimeException
, который играет важную роль в иерархии исключений.
В Java существует некоторая неопределенность насчет того – существует ли два или три вида исключений.
Если делить исключения на два вида, то это:
-
1.
контролируемые исключения (checked exceptions) – подклассы класса
Exception
, кроме подклассаRuntimeException
и его производных; -
2.
неконтролируемые исключения (unchecked exceptions) – класс
Error
с подклассами, а также классRuntimeException
и его производные;
В некоторых источниках класс Error
и его подклассы выделяют в отдельный вид исключений — ошибки (errors).
Далее мы видим класс Error
. Классы этой ветки составляют вид исключений, который можно обозначить как «ошибки» (errors). Ошибки представляют собой серьезные проблемы, которые не следует пытаться обработать в собственной программе, поскольку они связаны с проблемами уровня JVM.
На самом деле, вы конечно можете предпринять некоторые действия при возникновении ошибок, например, вывести сообщение для пользователя в удобном формате, выслать трассировку стека себе на почту, чтобы понять – что вообще произошло.
Но, по факту, вы ничего не можете предпринять в вашей программе, чтобы эту ошибку исправить, и ваша программа, как правило, при возникновении такой ошибки дальше работать не может.
В качестве примеров «ошибок» можно привести: переполнение стека вызова (класс StackOverflowError
); нехватка памяти в куче (класс OutOfMemoryError
), вследствие чего JVM не может выделить память под новый объект и сборщик мусора не помогает; ошибка виртуальной машины, вследствие которой она не может работать дальше (класс VirtualMachineError
) и так далее.
Несмотря на то, что в нашей программе мы никак не можем помочь этой проблеме, и приложение не может работать дальше (ну как может работать приложение, если стек вызовов переполнен или JVM не может дальше выполнять код?!); знание природы этих ошибок поможет вам предпринять некоторые действия, чтобы избежать этих ошибок в дальнейшем. Например, ошибки типа StackOverflowError
и OutOfMemoryError
могут быть следствием вашего некорректного кода.
Например, попробуем спровоцировать ошибку StackOverflowError
public static void main(String[] args) {
public static void methodA() {
private static void methodB() {
Получим такое сообщение об ошибке
Exception in thread «main» java.lang.StackOverflowError
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
at com.company.Main.methodB(Main.java:14)
at com.company.Main.methodA(Main.java:10)
Ошибка OutOfMemoryError
может быть вызвана тем, что ваш код, вследствие ошибки при программировании, создает очень большое количество массивных объектов, которые очень быстро заполняют кучу и свободного места не остается.
Exception in thread «main» java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3511)
at java.base/java.util.Arrays.copyOf(Arrays.java:3480)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:244)
at java.base/java.util.ArrayList.add(ArrayList.java:454)
at java.base/java.util.ArrayList.add(ArrayList.java:467)
at com.company.Main.main(Main.java:13)
Process finished with exit code 1
Ошибка VirtualMachineError
может означать, что следует переустановить библиотеки Java.
В любом случае, следует относиться к типу Error
не как к неизбежному злу и «воле богов», а просто как к сигналу к тому, что в вашем приложении что-то не так, или что-то не так с программным или аппаратным обеспечением, которое вы используете.
Класс Exception
описывает исключения, связанные непосредственно с работой программы. Такого рода исключения «решаемы» и их грамотная обработка позволит программе работать дальше в нормальном режиме.
В классе Exception описаны исключения двух видов: контролируемые исключения (checked exceptions) и неконтролируемые исключения (unchecked exceptions).
Неконтролируемые исключения содержатся в подклассе RuntimeException
и его наследниках. Контролируемые исключения содержатся в остальных подклассах Exception
.
В чем разница между контролируемыми и неконтролируемыми исключениями, мы узнаем позже, а теперь рассмотрим вопрос – а как же именно нам обрабатывать исключения?
Обработка исключений в методе может выполняться двумя способами:
-
1.
с помощью связки
try-catch
; -
2.
с помощью ключевого слова
throws
в сигнатуре метода.
Рассмотрим оба метода поподробнее:
Способ 1. Связка try-catch
Этот способ кратко можно описать следующим образом.
Код, который теоретически может вызвать исключение, записывается в блоке try{}
. Сразу за блоком try
идет блок код catch{}
, в котором содержится код, который будет выполнен в случае генерации исключения. В блоке finally{}
содержится код, который будет выполнен в любом случае – произошло ли исключение или нет.
Теперь разберемся с этим способом более подробно. Рассмотрим следующий пример – программу, которая складывает два числа, введенные пользователем из консоли
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println(«Введите первое число: «);
String firstNumber = scanner.nextLine();
System.out.println(«Введите второе число: «);
String secondNumber = scanner.nextLine();
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
System.out.println(«Результат: « + (a + b));
Первое, что нам нужно определить – и что является главным при работе с исключениями, КАКАЯ ИНСТРУКЦИЯ МОЖЕТ ПРИВЕСТИ К ВОЗНИКНОВЕНИЮ ИСКЛЮЧЕНИЯ?
То есть, мы должны понять – где потенциально у нас может возникнуть исключение? Понятно, что речь идет не об операции сложения и не об операции чтения данных из консоли. Потенциально опасными строчками кода здесь являются строчки
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
в которых происходит преобразование ввода пользователя в целое число (метод parseInt()
преобразует цифры в строке в число).
Почему здесь может возникнуть исключение? Потому что пользователь может ввести не число, а просто какой-то текст и тогда непонятно – что записывать в переменную a
или b
. И да, действительно, если пользователь введет некорректное значение, возникнет исключение в методе Integer.parseInt()
.
Итак, что мы можем сделать. «Опасный код» нужно поместить в блок try{}
Обратите внимание на синтаксис блока try
. В самом простом случае это просто ключевое слово try
, после которого идут парные фигурные скобки. Внутри этих скобок и заключается «опасный» код, который может вызвать исключение. Сразу после блока try должен идти блок catch()
.
a = Integer.parseInt(firstNumber);
b = Integer.parseInt(secondNumber);
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println(«Одно или оба значения некорректны!»);
System.out.println(«Результат: « + (a + b));
Обратите внимание на синтаксис блока catch
. После ключевого слова, в скобках описывается аргумент с именем e
типа NumberFormatException
.
Когда произойдет исключение, то система Java прервет выполнение инструкций в блоке try
и передаст управление блоку catch
и запишет в этот аргумент объект исключения, который сгенерировала Java-машина.
То есть, как только в блоке try
возникнет исключение, то дальше инструкции в блоке try выполняться не будут! А сразу же начнут выполняться действия в блоке catch
.
Обработчик исключения находится в блоке catch
, в котором мы можем отреагировать на возникновение исключения. Также, в этом блоке нам будет доступен объект исключения, от которого мы можем получить дополнительные сведения об исключении.
Блок catch
сработает только в том случае, если указанный в скобках тип объекта исключения будет суперклассом или будет того же типа, что и объект исключения, который сгенерировала Java.
Например, если в нашем примере мы напишем код, который потенциально может выбросить исключение типа IOException
, но не изменим блок catch
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println(«Одно или оба значения некорректны!»);
тогда обработчик не будет вызван и исключение будет обработано стандартным обработчиком Java.
Способ 2. Использование ключевого слова throws
Второй способ позволяет передать обязанность обработки исключения тому методу, который вызывает данный метод (а тот, в свою очередь может передать эту обязанность выше и т.д.).
Изменим наш пример и выделим в отдельный метод код, который будет запрашивать у пользователя число и возвращать его как результат работы метода
public static void main(String[] args) {
int a = getNumberFromConsole(«Введите первое число»);
int b = getNumberFromConsole(«Введите второе число»);
System.out.println(«Результат: « + (a + b));
public static int getNumberFromConsole(String message) {
Scanner scanner = new Scanner(System.in);
System.out.print(message + «: «);
String s = scanner.nextLine();
return Integer.parseInt(s);
Мы понимаем, что в данном методе может произойти исключение, но мы не хотим или не можем его обработать. Причины могут быть разными, например:
-
1.
обработка исключений может происходить централизованно однотипным способом (например, показ окошка с сообщением и с определенным текстом);
-
2.
это не входит в нашу компетенцию как программиста – обработкой исключений занимается другой программист;
-
3.
мы пишем только некоторую часть программы и непонятно – как будет обрабатывать исключение другой программист, который потом будет использовать наш код (например, мы пишем просто какую-то библиотеку, которая производит вычисления, и как будет выглядеть обработка – это не наше дело).
В любом случае, мы знаем, что в этом коде может быть исключение, но мы не хотим его обрабатывать, а хотим просто предупредить другой метод, который будет вызывать наш код, что выполнение кода может привести к исключению. В этом случае, используется ключевое слово throws
, которое указывается в сигнатуре метода
public static int getNumberFromConsole(String message) throws NumberFormatException {
Scanner scanner = new Scanner(System.in);
System.out.print(message + «: «);
String s = scanner.nextLine();
return Integer.parseInt(s);
Обратите внимание на расположение сигнатуру метода. Мы привыкли, что при объявлении метода сразу после скобок входных аргументов мы открываем фигурную скобку и записываем тело метода. Здесь же, после входных аргументов, мы пишем ключевое слово throws
и потом указываем тип исключения, которое может быть сгенерировано в нашем методе. Если метод может выбрасывать несколько типов исключений, они записываются через запятую
public static void foo() throws NumberFormatException, ArithmeticException, IOException {
Тогда, в методе main мы должны написать примерно следующее
public static void main(String[] args) {
a = getNumberFromConsole(«Введите первое число»);
b = getNumberFromConsole(«Введите второе число»);
} catch (NumberFormatException e) {
// сохранить текст ошибки в лог
System.out.println(«Одно или оба значения некорректны!»);
System.out.println(«Результат: « + (a + b));
Основное преимущество этого подхода – мы передаем обязанность по обработке исключений другому, вышестоящему методу.
Отличия между контролируемыми и неконтролируемыми исключениями
Отличия между контролируемыми и неконтролируемыми исключениями
Если вы вызываете метод, который выбрасывает checked исключение, то вы ОБЯЗАНЫ предусмотреть обработку возможного исключения, то есть связку try-catch
.
Яркий пример checked исключения – класс IOException
и его подклассы.
Рассмотрим пример – попробуем прочитать файл и построчно вывести его содержимое на экран консоли:
public static void main(String[] args) {
Path p = Paths.get(«c:\\temp\\file.txt»);
BufferedReader reader = Files.newBufferedReader(p);
while ((line = reader.readLine()) != null) {
System.out.println(line);
Как мы видим, компилятор не хочет компилировать наш код. Чем же он недоволен? У нас в коде происходит вызов двух методов – статического метода Files.newBufferedReader()
и обычного метода BufferedReader.readLine()
.
Если посмотреть на сигнатуры этих методов то можно увидеть, что оба этих метода выбрасывают исключения типа IOException
. Этот тип исключения относится к checked-исключению и поэтому, если вы вызываете эти методы, компилятор ТРЕБУЕТ от вас предусмотреть блок catch
, либо в самом вашем методе указать throws IOException
и, таким образом, передать обязанность обрабатывать исключение другому методу, который будет вызывать ваш.
Таким образом, «оборачиваем» наш код в блок try
и пишем блок catch
.
public static void main(String[] args) {
Path p = Paths.get(«c:\\temp\\file.txt»);
BufferedReader reader = Files.newBufferedReader(p);
while ((line = reader.readLine()) != null) {
System.out.println(line);
} catch (IOException e) {
System.out.println(«Ошибка при чтении файла!»);
Еще один способ — указать в сигнатуре метода, что он выбрасывает исключение типа IOException
и переложить обязанность обработать ошибку в вызывающем коде
public static void main(String[] args) {
Path p = Paths.get(«c:\\temp\\file.txt»);
} catch (IOException e) {
System.out.println(«Ошибка при чтении файла!»);
public static void printFile(Path p) throws IOException {
BufferedReader reader = Files.newBufferedReader(p);
while ((line = reader.readLine()) != null) {
System.out.println(line);
Eсли метод выбрасывает checked-исключение, то проверка на наличие catch-блока происходит на этапе компиляции. И вы обязаны предусмотреть обработку исключения для checked-исключения.
Что касается unchecked-исключения, то обязательной обработки исключения нет – вы можете оставить подобные ситуации без обработки.
Зачем необходимо наличие двух видов исключений?
Зачем необходимо наличие двух видов исключений?
В большинстве языков существует всего лишь один тип исключений – unchecked. Некоторые языки, например, C#, в свое время отказались от checked-исключений.
Во-первых, мы не можем сделать все исключения checked, т.к. очень многие операции могут генерировать исключения, и если каждый такой участок кода «оборачивать» в блок try-catch
, то код получится слишком громоздким и нечитабельным.
С другой стороны, зачем нужно делать некоторые типы исключений checked? Почему просто не сделать все исключения unchecked и оставить решения об обработке исключений целиком на совести программиста?
В официальной документации написано, что unchecked-исключения – это те исключения, от которых программа «не может восстановиться», тогда как checked-исключения позволяют откатить некоторую операцию и повторить ее снова.
На самом деле, если вы посмотрите на различные типы unchecked-исключений, то вы увидите, что большинство их связаны с ошибками самого программиста. Выход за пределы массива, исключение нулевого указателя, деление на ноль – большинство из подобного рода исключений целиком лежат на совести программистов. Тогда мы можем сказать, что лучше программист пишет более хороший код, чем везде вставляет проверки на исключения.
Контролируемые исключения, как правило, представляют те ошибки, которые возникают не из-за программиста и предусмотреть которые программист не может. Например, это отсутствующие файлы, работа с сокетами, подключение к базе данных, сетевые соединения, некорректный пользовательский ввод.
Вы можете написать идеальный код, но потом вы отдадите приложение пользователю, а он введет название файла, которого нет или напишет неправильный IP для сокет-соединения. Таким образом, мы заранее должны быть готовыми к неверным действиям пользователя или к программным или аппаратным проблемам на его стороне и в обязательном порядке предусмотреть обработку возможных исключений.
Дополнительно об исключениях
Дополнительно об исключениях
Рассмотрим детально различные возможности механизма исключений, которые позволяют программисту максимально эффективно противодействовать исключениям:
Java позволяет вам для одного блока try
предусмотреть несколько блоков catch
, каждый из которых должен обрабатывать свой тип исключения
public static void foo() {
} catch (ArithmeticException e) {
// обработка арифметического исключения
} catch (IndexOutOfBoundsException e) {
// обработка выхода за пределы коллекции
} catch (IllegalArgumentException e) {
// обработка некорректного аргумента
Важно помнить, что Java обрабатывает исключения последовательно. Java просматривает блок catch сверху вниз и выполняет первый подходящий блок, который может обработать данное исключение.
Так как вы можете указать как точный класс, так и суперкласс, то если первым блоком будет блок для суперкласса – выполнится он. Например, исключение FileNotFoundException
является подклассом IOException
. И поэтому если вы первым поставите блок с IOException
– он будет вызываться для всех подтипов исключений, в том числе и для FileNotFoundException
и блок c FileNotFoundException
никогда не выполнится.
public static void main(String[] args) {
Path p = Paths.get(«c:\\temp\\file.txt»);
} catch (IOException e) {
System.out.println(«Ошибка при чтении файла!»);
} catch (FileNotFoundException e) {
// данный блок никогда не будет вызван
public static void printFile(Path p) throws IOException {
BufferedReader reader = Files.newBufferedReader(p);
while ((line = reader.readLine()) != null) {
System.out.println(line);
Один блок для обработки нескольких типов исключений
Один блок для обработки нескольких типов исключений
Начиная с версии Java 7, вы можете использовать один блок catch
для обработки исключений нескольких, не связанных друг с другом типов. Приведем пример
public static void foo() {
} catch (ArithmeticException | IllegalArgumentException | IndexOutOfBoundsException e) {
// три типа исключений обрабатываются одинаково
Как мы видим, один блок catch используется для обработки и типа IOException
и NullPointerException
и NumberFormaException
.
Вы можете использовать вложенные блоки try
, которые могут помещаться в других блоках try
. После вложенного блока try
обязательно идет блок catch
public static void foo() {
} catch (IllegalArgumentException e) {
// обработка вложенного блока try
} catch (ArithmeticException e) {
Выбрасывание исключения с помощью ключевого слова throw
С помощью ключевого слова throw
вы можете преднамеренно «выбросить» определенный тип исключения.
public static void foo(int a) {
throw new IllegalArgumentException(«Аргумент не может быть отрицательным!»);
Кроме блока try
и catch
существует специальный блок finally
. Его отличительная особенность – он гарантированно отработает, вне зависимости от того, будет выброшено исключение в блоке try
или нет. Как правило, блок finally
используется для того, чтобы выполнить некоторые «завершающие» операции, которые могли быть инициированы в блоке try
.
public static void foo(int a) {
FileOutputStream fout = null;
File file = new File(«file.txt»);
fout = new FileOutputStream(file);
} catch (IOException e) {
// обработка исключения при записи в файл
} catch (IOException e) {
При любом развитии события в блоке try
, код в блоке finally
отработает в любом случае.
Блок finally
отработает, даже если в try-catch
присутствует оператор return
.
Как правило, блок finally
используется, когда мы в блоке try
работаем с ресурсами (файлы, базы данных, сокеты и т.д.), когда по окончании блока try-catch
мы освобождаем ресурсы. Например, допустим, в процессе работы программы возникло исключение, требующее ее преждевременного закрытия. Но в программе открыт файл или установлено сетевое соединение, а, следовательно, файл нужно закрыть, а соединение – разорвать. Для этого удобно использовать блок finally
.
Блок try-with-resources
является модификацией блока try
. Данный блок позволяет автоматически закрывать ресурс после окончания работы блока try
и является удобной альтернативой блоку finally
.
public static void foo() {
Path p = Paths.get(«c:\\temp\\file.txt»);
try (BufferedReader reader = Files.newBufferedReader(p)) {
while ((line = reader.readLine()) != null)
System.out.println(line);
} catch (IOException e) {
Внутри скобок блока try
объявляется один или несколько ресурсов, которые после отработки блока try-catch
будут автоматически освобождены. Для этого объект ресурса должен реализовывать интерфейс java.lang.AutoCloseable
.
Создание собственных подклассов исключений
Создание собственных подклассов исключений
Встроенные в Java исключения позволяют обрабатывать большинство распространенных ошибок. Тем не менее, вы можете создавать и обрабатывать собственные типы исключений. Для того, чтобы создать класс собственного исключения, достаточно определить как его произвольный от Exception
или от RuntimeException
(в зависимости от того, хотите ли вы использовать checked или unchecked – исключения).
Насчет создания рекомендуется придерживаться двух правил:
-
1.
определитесь, исключения какого типа вы хотите использовать для собственных исключений (checked или unchecked) и старайтесь создавать исключения только этого типа;
-
2.
старайтесь максимально использовать стандартные типы исключений и создавать свои типы только в том случае, если существующие типы исключений не отражают суть того исключения, которое вы хотите добавить.
Плохие практики при обработке исключений
Плохие практики при обработке исключений
Ниже представлены действия по обработке ошибок, которые характерны для плохого программиста. Ни в коем случае не рекомендуется их повторять!
-
1.
Указание в блоке catch объекта исключения типа Exception. Существует очень большой соблазн при создании блока
catch
указать тип исключенияException
и, таким образом, перехватывать все исключения, которые относятся к этому классу (а это все исключения, кроме системных ошибок). Делать так крайне не рекомендуется, т.к. вместо того чтобы решать проблему с исключениями, мы фактически игнорируем ее и просто реализуем некоторую «заглушку», чтобы приложение продолжило работу дальше. Кроме того, каждый тип исключения должен быть обработан своим определенным образом. -
2.
Помещение в блок
try
всего тела метода. Следующий плохой прием используется, когда программист не хочет разбираться с кодом, который вызывает исключение и просто, опять же, реализует «заглушку». Этот прием очень «хорошо» сочетается с первым приемом. В блокtry
должен помещаться только тот код, который потенциально может вызвать исключение, а не всё подряд, т.к. лень обрабатывать исключения нормально. -
3.
Игнорирование исключения. Следующий плохой прием состоит в том, что мы просто игнорируем исключение и оставляем блок
catch
пустым. Программа должна реагировать на исключения и должна информировать пользователя и разработчика о том, что что-то пошло не так. Безусловно, исключение это не повод тут же закрывать приложение, а попытаться повторить то действие, которое привело к исключению (например, повторно указать название файла, попытаться открыть базу данных через время и т.д.). В любом случае, когда приложение в ответ на ошибку никак не реагирует – не выдает сообщение, но и не делает того, чего от нее ожидали – это самый плохой вариант.
#База знаний
-
0
Разбираемся, что такое исключения, зачем они нужны и как с ними работать.
vlada_maestro / shutterstock
Хлебом не корми — дай кому-нибудь про Java рассказать.
Из этой статьи вы узнаете:
- что такое исключения (Exceptions);
- как они возникают и чем отличаются от ошибок (Errors);
- зачем нужна конструкция try-catch;
- как разобраться в полученном исключении
- и как вызвать исключение самому.
Код вашей программы исправно компилируется и запускается, только вот вместо желанного результата вы видите непонятный текст. Строчки его будто кричат на вас, аж побагровели.
За примером далеко ходить не надо: сделаем то, что нам запрещали ещё в школе, — поделим на ноль.
public static void main(String[] args) {
hereWillBeTrouble(42, 0);
}
public static void hereWillBeTrouble(int a, int b) {
int oops = a / b;
System.out.println(oops);
}
А получим вот что:
Это и есть исключение.
«Исключение» — сокращение от слов «исключительный случай». Это ситуация, в которой программа не может продолжить работу или её работа становится бессмысленной. Причём речь не только о нештатных ситуациях — исключения бывают и намеренными, такие разработчик вызывает сам.
Это интересно. Исключения в Java появились уже в первой версии языка. А вот в языках, где их нет, вместо них возвращают коды ошибок.
У всех классов исключений есть общий класс-предок Throwable, от него наследуются классы Error и Exception, базовые для всех прочих.
Error — это критические условия, в которых работа программы должна быть завершена. Например, когда при выполнении программы закончилась память, произошёл сбой в системе или виртуальной машине. Не будем задерживаться на этой ветке, поскольку документация Java говорит:
Error is the superclass of all the exceptions from which ordinary programs are not ordinarily expected to recover.
Что в переводе означает: ошибки (Error) — это такие исключительные ситуации, в которых восстанавливать работу программы не предполагается.
То есть это проблемы, которые нельзя (недопустимо) исправлять на ходу. Всё, что нам остаётся, — извиниться перед пользователем и впредь писать программы, где возникнет меньше подобных ситуаций. Например, не допускать такой глубокой рекурсии, как в коде ниже:
static void notGood() {
System.out.println("Только не снова!");
notGood();
}
При работе этого метода у нас возникнет ошибка: Exception in thread «main» java.lang.StackOverflowError — стек вызовов переполнился, так как мы не указали условие выхода из рекурсии.
А теперь об Exception. Эти исключительные ситуации возникают, если разработчик допустил невыполнимую операцию, не предусмотрел особые случаи в бизнес-логике программы (или сообщает о них с помощью исключений).
1. Невыполнимая операция
Мир не рухнул, как в случае с Error, просто Java не знает, что делать дальше. Как раз из этого разряда деление на ноль в начале статьи: и правда, какое значение тогда присвоить переменной oops?
Убедитесь сами, что исключение класса ArithmeticException наследуется как раз от Exception.
Стоит запомнить. В IntelliJ IDEA, чтобы увидеть положение класса в иерархии, выберите его и нажмите Ctrl + H (или на пункт Type Hierarchy в меню Navigate).
Другая частая ситуация — обращение к несуществующему элементу массива. Например, у нас в нём десять элементов, а мы пытаемся обратиться к одиннадцатому.
2. Особый случай в бизнес-логике программы
Классика. Программируем задачу о перевозке волка, козы и капусты через реку: в лодке может быть только два пассажира, но волка с козой и козу с капустой нельзя оставлять на берегу вместе. Это и есть особый случай в бизнес-логике, который нельзя нарушать.
Или пользователь вводит дату начала некоторого периода и дату его окончания. Вторая дата не может быть раньше первой.
Или, допустим, у нас есть метод, который читает файл. Сам метод написан верно. Пользователь передал в него корректный путь. Только вот у этого работника нет права читать этот файл (его роль и права обусловлены предметной областью). Что же тогда методу возвращать? Вернуть-то нечего, ведь метод не отработал. Самое очевидное решение — выдать исключение.
В дерево исключений мы ещё углубимся, а сейчас посмотрим, что и как с ними делают.
Простейший вариант — ничего; возникает исключение — программа просто прекращает работать.
Чтобы убедиться в этом, выполним код:
public static void main(String[] args) {
hereWillBeTrouble(42, 0);
}
public static void hereWillBeTrouble(int a, int b) {
System.out.println("Всё, что было до...");
int oops = a / b;
System.out.println(oops);
System.out.println("Всё, что будет после...");
}
Так и есть: до деления на ноль код выполнялся, а после — нет.
Это интересно: когда возникает исключение, программисты выдают что-то вроде «код [вы]бросил исключение» или «код кинул исключение». А глагол таков потому, что все исключения — наследники класса Throwable, что значит «бросаемый» / «который можно бросить».
Второе, что можно делать с исключениями, — это их обрабатывать.
Для этого нужно заключить кусок кода, который может вызвать исключение, в конструкцию try-catch.
Как это работает: если в блоке try возникает исключение, которое указано в блоке catch, то исполнение блока try прервётся и выполнится код из блока catch.
Например:
public static void main(String[] args) {
hereWillBeTrouble();
}
private static void hereWillBeTrouble(int a, int b) {
int oops;
try {
System.out.println("Всё, что было до...");
oops = a / b;
System.out.println(oops);
System.out.println("Всё, что будет после...");
} catch (ArithmeticException e) {
System.out.println("Говорили же не делить на ноль!");
oops = 0;
}
System.out.println("Метод отработал");
}
Разберём этот код.
Если блок try кинет исключение ArithmeticException, то управление перехватит блок catch, который выведет строку «Говорили же не делить на ноль!», а значение oops станет равным 0.
После этого программа продолжит работать как ни в чём не бывало: выполнится код после блока try-catch, который сообщит: «Метод отработал».
Проверьте сами: запустите код выше. Вызовите метод hereWillBeTrouble с любыми значениями аргументов кроме нулевого b. Если в блоке try не возникнет исключений, то его код выполнится целиком, а в блок catch мы даже не попадём.
Есть ещё и третий вариант — пробросить исключение наверх. Но об этом в следующей статье.
Вернёмся к первой картинке. Посмотрим, что нам сказала Java, когда произошло исключение:
Начинаем разбирать сверху вниз:
— это указание на поток, в котором произошло исключение. В нашей простой однопоточной программе это поток main.
— какое исключение брошено. У нас это ArithmeticException. А java.lang.ArithmeticException — полное название класса вместе с пакетом, в котором он размещается.
— весточка, которую принесло исключение. Дело в том, что одно и то же исключение нередко возникает по разным причинам. И тут мы видим стандартное пояснение «/ by zero» — из-за деления на ноль.
— это самое интересное: стектрейс.
Стектрейс (Stack trace) — это упорядоченный список методов, сквозь которые исключение пронырнуло.
У нас оно возникло в методе hereWillBeTrouble на 8-й строке в классе Main (номер строки и класс указаны в скобках синим). А этот метод, в свою очередь, вызван методом main на 3-й строке класса Main.
Стектрейсы могут быть довольно длинными — из десятков методов, которые вызывают друг друга по цепочке. И они здорово помогают расследовать неожиданно кинутое исключение.
Советую закреплять теорию на практике. Поэтому вернитесь в блок про Error и вызовите метод notGood — увидите любопытный стектрейс.
Всё это время мы имели дело с исключением, которое бросает Java-машина — при делении на ноль. Но как вызвать исключение самим?
Раз исключение — это объект класса, то программисту всего-то и нужно, что создать объект с нужным классом исключения и бросить его с помощью оператора throw.
public static void main(String[] args) {
hereWillBeTrouble(42, 0);
}
private static void hereWillBeTrouble(int a, int b) {
if (b == 0) {
throw new ArithmeticException("ты опять делишь на ноль?");
}
int oops = a / b;
System.out.println(oops);
}
При создании большинства исключений первым параметром в конструктор можно передать сообщение — мы как раз сделали так выше.
А получим мы то же самое, что и в самом первом примере, только вместо стандартной фразы «/by zero» теперь выдаётся наш вопрос-пояснение «ты опять делишь на ноль?»:
В следующей статье мы углубимся в иерархию исключений Java, узнаем про их разделение на checked и unchecked, а также о том, что ещё интересного можно с ними делать.
Как зарабатывать больше с помощью нейросетей?
Бесплатный вебинар: 15 экспертов, 7 топ-нейросетей. Научитесь использовать ИИ в своей работе и увеличьте доход.
Узнать больше
В нашей жизни нередко возникают ситуации, которые мы не планировали. К примеру, пошли вы утром умываться и с досадой обнаружили, что отключили воду. Вышли на улицу, сели в машину, а она не заводится. Позвонили другу, а он недоступен. И так далее и тому подобное… В большинстве случаев человек без труда справится с подобными проблемами. А вот как с непредвиденными ситуациями справляется Java, мы сейчас и поговорим.
Что называют исключением. Исключения в мире программирования
В программировании исключением называют возникновение ошибки (ошибок) и различных непредвиденных ситуаций в процессе выполнения программы. Исключения могут появляться как в итоге неправильных действий юзера, так и из-за потери сетевого соединения с сервером, отсутствии нужного ресурса на диске и т. п. Также среди причин исключений — ошибки программирования либо неверное использование API.
При этом в отличие от «человеческого мира», программное приложение должно чётко понимать, как поступать в подобной ситуации. И вот как раз для этого в Java и существует механизм исключений (exception).
Используемые ключевые слова
При обработке исключений в Java применяются следующие ключевые слова:
— try – служит для определения блока кода, в котором может произойти исключение;
— catch – необходим для определения блока кода, где происходит обработка исключения;
— finally – применяется для определения блока кода, являющегося необязательным, однако при его наличии он выполняется в любом случае вне зависимости от результата выполнения блока try.
Вышеперечисленные ключевые слова необходимы для создания в коде ряда специальных обрабатывающих конструкций: try{}finally{}, try{}catch, try{}catch{}finally.
Кроме того:
1. Для возбуждения исключения используем throw.
2. Для предупреждения в сигнатуре методов о том, что метод может выбросить исключение, применяем throws.
Давайте на примере посмотрим, как используются ключевые слова в Java-программе:
//метод считывает строку с клавиатуры public String input() throws MyException {//предупреждаем с помощью throws, // что метод может выбросить исключение MyException BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); String s = null; //в блок try заключаем код, в котором может произойти исключение, в данном // случае компилятор нам подсказывает, что метод readLine() класса // BufferedReader может выбросить исключение ввода/вывода try { s = reader.readLine(); // в блок catch заключаем код по обработке исключения IOException } catch (IOException e) { System.out.println(e.getMessage()); // в блоке finally закрываем поток чтения } finally { // при закрытии потока тоже возможно исключение, например, если он не был открыт, поэтому “оборачиваем” код в блок try try { reader.close(); // пишем обработку исключения при закрытии потока чтения } catch (IOException e) { System.out.println(e.getMessage()); } } if (s.equals("")) { // мы решили, что пустая строка может нарушить в дальнейшем работу нашей программы, например, на результате этого метода нам надо вызывать метод substring(1,2), поэтому мы вынуждены прервать выполнение программы с генерацией своего типа исключения MyException с помощью throw throw new MyException("String can not be empty!"); } return s; }Зачем нам механизм исключений?
Для понимания опять приведём пример из обычного мира. Представьте, что на какой-нибудь автодороге имеется участок с аварийным мостом, на котором ограничена грузоподъёмность. И если по такому мосту проедет грузовик со слишком большой массой, мост разрушится, а в момент этого ЧП ситуация для шофёра станет, мягко говоря, исключительной. И вот, дабы такого не произошло, дорожные службы заранее устанавливают на дороге соответствующие предупреждающие знаки. И тогда водитель, посмотрев на знак, сравнит массу своего авто со значением разрешённой грузоподъёмности и примет соответствующее решение, например, поедет по другой дороге.
То есть мы видим, что из-за правильных действий дорожной службы шоферы крупногабаритных транспортных средств:
1) получили возможность заранее изменить свой путь;
2) были предупреждены об опасности;
3) были предупреждены о невозможности проезжать по мосту при определённых условиях.Вот как наш жизненный пример соотносится с применением исключения на Java:
Исходя из вышесказанного, мы можем назвать одну из причин применения исключений в Java. Заключается она в возможности предупреждения исключительной ситуации для её последующего разрешения и продолжения работы программы. То есть механизм исключений позволит защитить написанный код от неверного применения пользователем путём валидации входящих данных.
Что же, давайте ещё раз побудем дорожной службой. Чтобы установить знак, мы ведь должны знать места, где водителей ТС могут ждать различные неприятности. Это первое. Далее, нам ведь надо заготовить и установить знаки. Это второе. И, наконец, надо предусмотреть маршруты объезда, позволяющие избежать опасности.
В общем, механизм исключений в Java работает схожим образом. На стадии разработки программы мы выполняем «ограждение» опасных участков кода в отношении наших исключений, используя блок try{}. Чтобы предусмотреть запасные пути, применяем блок catch{}. Код, выполняемый в программе при любом исходе, пишем в блоке finally{}.
Иногда бывает, что мы не можем предусмотреть «запасной аэродром» либо специально желаем предоставить право его выбора юзеру. Но всё равно мы должны как минимум предупредить пользователя об опасности. Иначе он превратится в разъярённого шофёра, который ехал долго, не встретил ни одного предупреждающего знака и в итоге добрался до аварийного моста, проехать по которому не представляется возможным.
Что касается программирования на Java, то мы, когда пишем свои классы и методы, далеко не всегда можем предвидеть контекст их применения другими программистами в своих программах, а значит, не можем со стопроцентной вероятностью предвидеть правильный путь для разрешения исключительных ситуаций. Но предупредить коллег о возможной исключительной ситуации мы всё-таки должны, и это не что иное, как правило хорошего тона.
Выполнить это правило в Java нам как раз и помогает механизм исключений с помощью throws. Выбрасывая исключение, мы, по сути, объявляем общее поведение нашего метода и предоставляем пользователю метода право написания кода по обработке исключения.
Предупреждаем о неприятностях
Если мы не планируем обрабатывать исключение в собственном методе, но желаем предупредить пользователей метода о возможной исключительной ситуации, мы используем, как это уже было упомянуто, ключевое слово throws. В сигнатуре метода оно означает, что при некоторых обстоятельствах метод может выбросить исключение. Это предупреждение становится частью интерфейса метода и даёт право пользователю на создание своего варианта реализации обработчика исключения.
После упоминания ключевого слова throws мы указываем тип исключения. Как правило, речь идёт о наследниках класса Exception Java. Но так как Java — это объектно-ориентированный язык программирования, все исключения представляют собой объекты.
Иерархия исключений в Java
Когда возникают ошибки при выполнении программы, исполняющая среда Java Virtual Machine обеспечивает создание объекта нужного типа, используя иерархию исключений Java — речь идёт о множестве возможных исключительных ситуаций, которые унаследованы от класса Throwable — общего предка. При этом исключительные ситуации, которые возникают в программе, делят на 2 группы:
1. Ситуации, при которых восстановление нормальной дальнейшей работы невозможно.
2. Ситуации с возможностью восстановления.К первой группе можно отнести случаи, при которых возникают исключения, которые унаследованы из класса Error. Это ошибки, возникающие во время выполнения программы при сбое работы Java Virtual Machine, переполнении памяти либо сбое системы. Как правило, такие ошибки говорят о серьёзных проблемах, устранение которых программными средствами невозможно. Данный вид исключений в Java относят к неконтролируемым исключениям на стадии компиляции (unchecked). К этой же группе относятся и исключения-наследники класса Exception, генерируемые Java Virtual Machine в процессе выполнения программы — RuntimeException. Данные исключения тоже считаются unchecked на стадии компиляции, а значит, написание кода по их обработке необязательно.
Что касается второй группы, то к ней относят ситуации, которые можно предвидеть ещё на стадии написания приложения, поэтому для них код обработки должен быть написан. Это контролируемые исключения (checked). И в большинстве случаев Java-разработчики работают именно с этими исключениями, выполняя их обработку.
Создание исключения
В процессе исполнения программы исключение генерируется Java Virtual Machine либо вручную посредством оператора throw. В таком случае в памяти происходит создание объекта исключения, выполнение основного кода прерывается, а встроенный в JVM обработчик исключений пробует найти способ обработать это самое исключение.
Обработка исключения
Обработка исключений в Java подразумевает создание блоков кода и производится в программе посредством конструкций try{}finally{}, try{}catch, try{}catch{}finally.
В процессе возбуждения исключения в try обработчик исключения ищется в блоке catch, который следует за try. При этом если в catch присутствует обработчик данного вида исключения, происходит передача управления ему. Если же нет, JVM осуществляет поиск обработчика данного типа исключения, используя для этого цепочку вызова методов. И так происходит до тех пор, пока не находится подходящий catch. После того, как блок catch выполнится, управление переходит в необязательный блок finally. Если подходящий блок catch найден не будет, Java Virtual Machine остановит выполнение программы, выведя стек вызовов методов под названием stack trace. Причём перед этим выполнится код блока finally при наличии такового.
Рассмотрим практический пример обработки исключений:
public class Print { void print(String s) { if (s == null) { throw new NullPointerException("Exception: s is null!"); } System.out.println("Inside method print: " + s); } public static void main(String[] args) { Print print = new Print(); List list= Arrays.asList("first step", null, "second step"); for (String s:list) { try { print.print(s); } catch (NullPointerException e) { System.out.println(e.getMessage()); System.out.println("Exception was processed. Program continues"); } finally { System.out.println("Inside bloсk finally"); } System.out.println("Go program...."); System.out.println("-----------------"); } } }А теперь глянем на результаты работы метода main:
Inside method print: first step Inside bloсk finally Go program.... ----------------- Exception: s is null! Exception was processed. Program continues Inside bloсk finally Go program.... ----------------- Inside method print: second step Inside bloсk finally Go program.... -----------------Блок finally чаще всего используют, чтобы закрыть открытые в try потоки либо освободить ресурсы. Но при написании программы уследить за закрытием всех ресурсов возможно не всегда. Чтобы облегчить жизнь разработчикам Java, была предложена конструкция try-with-resources, автоматически закрывающая ресурсы, открытые в try. Используя try-with-resources, мы можем переписать наш первый пример следующим образом:
public String input() throws MyException { String s = null; try(BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))){ s = reader.readLine(); } catch (IOException e) { System.out.println(e.getMessage()); } if (s.equals("")){ throw new MyException ("String can not be empty!"); } return s; }А благодаря появившимся возможностям Java начиная с седьмой версии, мы можем ещё и объединять в одном блоке перехват разнотипных исключений, делая код компактнее и читабельнее:
public String input() { String s = null; try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { s = reader.readLine(); if (s.equals("")) { throw new MyException("String can not be empty!"); } } catch (IOException | MyException e) { System.out.println(e.getMessage()); } return s; }Итоги
Итак, применение исключений в Java повышает отказоустойчивость программы благодаря использованию запасных путей. Кроме того, появляется возможность отделить код обработки исключительных ситуаций от логики основного кода за счёт блоков catch и переложить обработку исключений на пользователя кода посредством throws.
Основные вопросы об исключениях в Java
1.Что такое проверяемые и непроверяемые исключения?
Если говорить коротко, то первые должны быть явно пойманы в теле метода либо объявлены в секции throws метода. Вторые вызываются проблемами, которые не могут быть решены. Например, это нулевой указатель или деление на ноль. Проверяемые исключения очень важны, ведь от других программистов, использующих ваш API, вы ожидаете, что они знают, как обращаться с исключениями. К примеру, наиболее часто встречаемое проверяемое исключение — IOException, непроверяемое — RuntimeException.
2.Почему переменные, определённые в try, нельзя использовать в catch либо finally?
Давайте посмотрим на нижеследующий код. Обратите внимание, что строку s, которая объявлена в блоке try, нельзя применять в блоке catch. То есть данный код не скомпилируется.try { File file = new File("path"); FileInputStream fis = new FileInputStream(file); String s = "inside"; } catch (FileNotFoundException e) { e.printStackTrace(); System.out.println(s); }А всё потому, что неизвестно, где конкретно в try могло быть вызвано исключение. Вполне вероятно, что оно было вызвано до объявления объекта.
3.Почему Integer.parseInt(null) и Double.parseDouble(null) вызывают разные исключения?
Это проблема JDK. Так как они были разработаны разными людьми, то заморачиваться вам над этим не стоит:Integer.parseInt(null); // вызывает java.lang.NumberFormatException: null Double.parseDouble(null); // вызывает java.lang.NullPointerException4.Каковы основные runtime exceptions в Java?
Вот лишь некоторые из них:IllegalArgumentException ArrayIndexOutOfBoundsExceptionИх можно задействовать в операторе if, если условие не выполняется:
if (obj == null) { throw new IllegalArgumentException("obj не может быть равно null");5.Возможно ли поймать в одном блоке catch несколько исключений?
Вполне. Пока классы данных исключений можно отследить вверх по иерархии наследования классов до одного и того же суперкласса, возможно применение только этого суперкласса.
6.Способен ли конструктор вызывать исключения?
Способен, ведь конструктор — это лишь особый вид метода.class FileReader{ public FileInputStream fis = null; public FileReader() throws IOException{ File dir = new File(".");//get current directory File fin = new File(dir.getCanonicalPath() + File.separator + "not-existing-file.txt"); fis = new FileInputStream(fin); } }7.Возможен ли вызов исключений в final?
В принципе, можете сделать таким образом:public static void main(String[] args) { File file1 = new File("path1"); File file2 = new File("path2"); try { FileInputStream fis = new FileInputStream(file1); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { try { FileInputStream fis = new FileInputStream(file2); } catch (FileNotFoundException e) { e.printStackTrace(); } } }Но если желаете сохранить читабельность, объявите вложенный блок try-catch в качестве нового метода и вставьте вызов данного метода в блок finally.
finally. public static void main(String[] args) { File file1 = new File("path1"); File file2 = new File("path2"); try { FileInputStream fis = new FileInputStream(file1); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { methodThrowException(); } }
Исключение в Java представляет проблему, которая возникает в ходе выполнения программы. В случае возникновения в Java исключения (exception), или исключительного события, имеет место прекращение нормального течения программы, и программа/приложение завершаются в аварийном режиме, что не является рекомендованным, и, как следствие, подобные случаи требуют в Java обработку исключений.
Причины возникновения исключения
Существует множество причин, которые могут повлечь за собой возникновение исключения. Далее рассмотрен ряд подобных сценариев, в контексте которых может произойти исключение:
- Пользователь ввел недопустимые данные.
- Файл, который необходимо открыть, не найден.
- Соединение с сетью потеряно в процессе передачи данных либо JVM исчерпала имеющийся объем памяти.
Некоторые из данных исключений вызваны пользовательской ошибкой, другие – программной ошибкой, в некоторых случаях, причиной тому может послужить сбой в материальных ресурсах.
Исходя из приведенных сведений, мы можем обозначить три типа исключений. Знание данных типов позволит вам в дальнейшем разрешать проблемные ситуации, связанные с исключениями. Ниже рассмотрим список исключений в Java с примерами.
- Контролируемые исключения – контролируемое исключение представляет собой вид исключения, которое происходит на стадии компиляции, их также именуют исключениями периода компиляции. Обозначенные исключения не следует игнорировать в ходе компиляции, они требуют должного обращения (разрешения) со стороны программиста.
К примеру, если вы используете класс FileReader в вашей программе для считывания данных из файла, в случае, если указанный в конструкторе файл не существует, происходит FileNotFoundException, и компилятор подсказывает программисту обработку данного исключения.
Пример 1
import java.io.File;
import java.io.FileReader;
public class Test {
public static void main(String args[]) {
File f = new File("D://java/file.txt");
FileReader fr = new FileReader(f);
}
}
При попытке компиляции обозначенной выше программы будут выведены следующие исключения:
C:\>javac Test.java
Test.java:8: error: unreported exception FileNotFoundException; must be caught or declared to be thrown
FileReader fr = new FileReader(f);
^
1 error
Примечание. В виду того, что методы read() и close() класса FileReader вызывают IOException, компилятор может уведомить вас об обработке IOException, совместно с FileNotFoundException.
- Неконтролируемые исключения – неконтролируемое исключение представляет собой исключение, которое происходит во время выполнения. Они также носят название исключения на этапе выполнения. Данная категория может включать погрешности программирования, такие как логические ошибки либо неверный способ использования API. Исключения на этапе выполнения игнорируются в ходе компиляции.
К примеру, если вами в вашей программе был объявлен массив из 5 элементов, попытка вызова 6-го элемента массива повлечет за собой возникновение ArrayIndexOutOfBoundsExceptionexception.
Пример 2
public class Test {
public static void main(String args[]) {
int array[] = {1, 2, 3};
System.out.println(array[4]);
}
}
При компиляции и выполнении обозначенной выше программы будет получено следующее исключение:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at Exceptions.Test.main(Test.java:8)
- Ошибки – не являются исключениями, однако представляют проблемы, которые возникают независимо от пользователя либо программиста. Ошибки в вашем коде обычно игнорируются в виду того, что в редких случаях их обработка окажется результативной. К примеру, ошибка возникнет при переполнении стека. На этапе компиляции они также игнорируются.
Иерархия исключений
Все классы исключений в Java представляют подтипы класса java.lang.Exception. Класс исключений является подклассом класса Throwable. Помимо класса исключений существует также подкласс ошибок, образовавшихся из класса Throwable.
Ошибки представляют аварийное состояние вследствие значительных сбоев, которые не обрабатываются программами Java. Генерирование ошибок предназначено для отображения ошибок, выявленных средой выполнения. Примеры: JVM исчерпал имеющийся объем памяти. Обычно, программы не могут восстановить неполадки, вызванные ошибками.
Класс исключений делится на два основных подкласса: класс IOException и класс RuntimeException.
По ссылке представлен перечень наиболее распространенных контролируемых (checked) и неконтролируемых (unchecked) встроенных исключений в Java.
Методы исключений
Далее представлен список важных методов, доступных в классе Throwable.
№ | Метод и описание |
1 | public String getMessage() Возврат подробного сообщения о произошедшем исключении. Инициализация данного сообщения производится в конструкторе Throwable. |
2 | public Throwable getCause() Возврат причины исключения, представленной объектом Throwable. |
3 | public String toString() Возврат имени класса, соединенного с результатом getMessage(). |
4 | public void printStackTrace() Выведение результата toString() совместно с трассировкой стека в System.err, поток вывода ошибок. |
5 | public StackTraceElement [] getStackTrace() Возврат массива, содержащего каждый элемент в трассировке стека. Элемент с номером 0 представляет вершину стека вызовов, последний элемент массива отображает метод на дне стека вызовов. |
6 | public Throwable fillInStackTrace() Заполняет трассировку стека данного объекта Throwable текущей трассировкой стека, дополняя какую-либо предшествующую информацию в трассировке стека. |
Обработка исключений – try и catch
Метод производит обработку исключения при использовании ключевых слов try и catch.
Описание
Блок try/catch размещается в начале и конце кода, который может сгенерировать исключение. Код в составе блока try/catch является защищенным кодом, синтаксис использования try/catch выглядит следующим образом:
try {
// Защищенный код
}catch(НазваниеИсключения e1) {
// Блок catch
}
Код, предрасположенный к исключениям, размещается в блоке try. В случае возникновения исключения, обработка данного исключения будет производиться соответствующим блоком catch. За каждым блоком try должен немедленно следовать блок catch либо блок finally.
Оператор catch включает объявление типа исключения, которое предстоит обработать. При возникновении исключения в защищенном коде, блок catch (либо блоки), следующий за try, будет проверен. В случае, если тип произошедшего исключения представлен в блоке catch, исключение передается в блок catch аналогично тому, как аргумент передается в параметр метода.
Пример
Ниже представлен массив с заявленными двумя элементами. Попытка кода получить доступ к третьему элементу массива повлечет за собой генерацию исключения.
import java.io.*;
public class Test {
public static void main(String args[]) {
try {
int array[] = new int[2];
System.out.println("Доступ к третьему элементу:" + array[3]);
}catch(ArrayIndexOutOfBoundsException e) {
System.out.println("Исключение:" + e);
}
System.out.println("Вне блока");
}
}
Вследствие этого будет получен следующий результат:
Исключение:java.lang.ArrayIndexOutOfBoundsException: 3
Вне блока
Многократные блоки catch
За блоком try могут следовать несколько блоков catch. Синтаксис многократных блоков catch выглядит следующим образом:
try {
// Защищенный код
}catch(ИсключениеТип1 e1) {
// Блок catch
}catch(ИсключениеТип2 e2) {
// Блок catch
}catch(ИсключениеТип3 e3) {
// Блок catch
}
Представленные выше операторы демонстрируют три блока catch, однако, после однократного try количество данных используемых блоков может быть произвольным. В случае возникновения исключения в защищенном коде, исключение выводится в первый блок catch в списке. Если тип данных генерируемого исключения совпадает с ИсключениеТип1, он перехватывается в указанной области. В обратном случае, исключение переходит ко второму оператору catch. Это продолжается до тех пор, пока не будет произведен перехват исключения, либо оно не пройдет через все операторы, в случае чего выполнение текущего метода будет прекращено, и исключение будет перенесено к предшествующему методу в стеке вызовов.
Пример
Далее представлен сегмент кода, демонстрирующий использование многократных операторов try/catch.
try {
file = new FileInputStream(fileName);
x = (byte) file.read();
}catch(IOException e1) {
e1.printStackTrace();
return -1;
}catch(FileNotFoundException e2) // Недействительно! {
e2.printStackTrace();
return -1;
}
Перехват многотипных исключений
В среде Java 7, Вы можете произвести обработку более чем одного исключения при использовании одного блока catch, данное свойство упрощает код. Ниже представлена модель реализации:
catch (IOException|FileNotFoundException ex) {
logger.log(ex);
throw ex;
Ключевые слова throws/throw
В случае если метод не может осуществить обработку контролируемого исключения, производится соответствующее уведомление при использовании ключевого слова throws в Java. Ключевое слово throws появляется в конце сигнатуры метода.
При использовании ключевого слова throw вы можете произвести обработку вновь выявленного исключения либо исключения, которое было только что перехвачено.
Следует внимательно различать ключевые слова throw и throws в Java, так как throws используется для отложенной обработки контролируемого исключения, а throw, в свою очередь, используется для вызова заданного исключения.
Представленный ниже метод отображает, что им генерируется RemoteException:
Пример 1
import java.rmi.RemoteException;
public class Test {
public void deposit(double amount) throws RemoteException {
// Реализация метода
throw new RemoteException();
}
// Остаток определения класса
}
Метод также может объявить о том, что им генерируется более чем одно исключение, в случае чего исключения представляются в виде перечня, отделенные друг от друга запятыми. К примеру, следующий метод оповещает о том, что им генерируются RemoteException и InsufficientFundsException:
Пример 2
import java.rmi.RemoteException;
public class Test {
public void withdraw(double amount) throws RemoteException,
InsufficientFundsException {
// Реализация метода
}
// Остаток определения класса
}
Блок finally
В Java finally следует за блоком try либо блоком catch. Блок finally в коде выполняется всегда независимо от наличия исключения.
Использование блока finally позволяет запустить какой-либо оператор, предназначенный для очистки, не зависимо от того, что происходит в защищенном коде.
Блок finally в Java появляется по окончании блоков catch, его синтаксис выглядит следующим образом:
Синтаксис
try {
// Защищенный код
}catch(ИсключениеТип1 e1) {
// Блок catch
}catch(ИсключениеТип2 e2) {
// Блок catch
}catch(ИсключениеТип3 e3) {
// Блок catch
}finally {
// Блок finally всегда выполняется.
}
Пример
public class Test {
public static void main(String args[]) {
int array[] = new int[2];
try {
System.out.println("Доступ к третьему элементу:" + array[3]);
}catch(ArrayIndexOutOfBoundsException e) {
System.out.println("Исключение:" + e);
}finally {
array[0] = 6;
System.out.println("Значение первого элемента: " + array[0]);
System.out.println("Оператор finally выполнен.");
}
}
}
Вследствие этого будет получен следующий результат:
Исключение:java.lang.ArrayIndexOutOfBoundsException: 3
Значение первого элемента: 6
Оператор finally выполнен.
Следует помнить, что:
- Выражение catch не может существовать без оператора try.
- При наличии блока try/catch, выражение finally не является обязательным.
- Блок try не может существовать при отсутствии выражения catch либо выражения finally.
- Существование какого-либо кода в промежутке между блоками try, catch, finally является невозможным.
Конструкция try-with-resources
В норме, при использовании различных видов ресурсов, таких как потоки, соединения и др., нам предстоит закрыть их непосредственно при использовании блока finally. В программе, представленной ниже, нами производится считывание данных из файла при использовании FileReader, после чего он закрывается блоком finally.
Пример 1
import java.io.FileReader;
import java.io.File;
import java.io.IOException;
public class Test {
public static void main(String args[]) {
FileReader fr = null;
try {
File f = new File("file.txt");
fr = new FileReader(f);
char [] array = new char[10];
fr.read(array); // чтение содержимого массива
for(char c : array)
System.out.print(c); // вывод символов на экран, один за одним
}catch(IOException e1) {
e1.printStackTrace();
}finally {
try {
fr.close();
}catch(IOException e2) {
e2.printStackTrace();
}
}
}
}
Конструкция try-with-resources, также именуемая как автоматическое управление ресурсами, представляет новый механизм обработки исключений, который был представлен в 7-ой версии Java, осуществляя автоматическое закрытие всех ресурсов, используемых в рамках блока try catch.
Чтобы воспользоваться данным оператором, вам всего лишь нужно разместить заданные ресурсы в круглых скобках, после чего созданный ресурс будет автоматически закрыт по окончании блока. Ниже представлен синтаксис конструкции try-with-resources.
Синтаксис
try(FileReader fr = new FileReader("Путь к файлу")) {
// использование ресурса
}catch() {
// тело catch
}
}
Программа ниже производит считывание данных в файле, используя конструкцию try-with-resources.
Пример 2
import java.io.FileReader;
import java.io.IOException;
public class Test {
public static void main(String args[]) {
try(FileReader fr = new FileReader("E://Soft/NetBeans 8.2/Projects/test/test/file.txt")) {
char [] array = new char[10];
fr.read(array); // чтение содержимого массива
for(char c : array)
System.out.print(c); // вывод символов на экран, один за одним
}catch(IOException e) {
e.printStackTrace();
}
}
}
При работе с конструкцией try-with-resources следует принимать во внимание следующие нюансы:
- С целью использования конструкции try-with-resources следует реализовать интерфейс AutoCloseable, после чего соответствующий метод close() будет вызван автоматически во время выполнения.
- В конструкции try-with-resources возможно указание одного и более классов.
- При указании нескольких классов в блоке try конструкции try-with-resources, закрытие данных классов будет производиться в обратном порядке.
- За исключением внесения ресурсов в скобки, все элементы являются равными аналогично нормальному блоку try/catch в составе блока try.
- Ресурсы, внесенные в try, конкретизируются до запуска блока try.
- Ресурсы непосредственно в составе блока try указываются как окончательные.
Создание своих собственных исключений
Вы можете создать свои собственные исключения в среде Java. При записи собственных классов исключений следует принимать во внимание следующие аспекты:
- Все исключения должны быть дочерними элементами Throwable.
- Если вы планируете произвести запись контролируемого исключения с автоматическим использованием за счет правила обработки или объявления, вам следует расширить класс Exception.
- Если вы хотите произвести запись исключения на этапе выполнения, вам следует расширить класс RuntimeException.
Вы можете определить собственный класс исключений, как показано ниже:
class MyException extends Exception {
}
Вам лишь необходимо расширить предопределенный класс Exception с целью создания собственного исключения. Данная категория относится к контролируемым исключениям. Следующий класс InsufficientFundsException исключительных ситуаций, определяемых пользователем, расширяет класс Exception, делая его контролируемым исключением. Класс исключений, подобно всем остальным классам, содержит используемые области и методы.
Пример
// Название файла InsufficientFundsException.java
import java.io.*;
public class InsufficientFundsException extends Exception {
private double amount;
public InsufficientFundsException(double amount) {
this.amount = amount;
}
public double getAmount() {
return amount;
}
}
С целью демонстрации наших исключений, определяемых пользователем, следующий класс Checking содержит метод withdraw(), генерирующий InsufficientFundsException.
// Название файла Checking.java
import java.io.*;
public class Checking {
private int number;
private double balance;
public Checking(int number) {
this.number = number;
}
public void deposit(double amount) {
balance += amount;
}
public void withdraw(double amount) throws InsufficientFundsException {
if(amount <= balance) {
balance -= amount;
}else {
double needs = amount - balance;
throw new InsufficientFundsException(needs);
}
}
public double getBalance() {
return balance;
}
public int getNumber() {
return number;
}
}
Следующая программа Bank демонстрирует вызов методов deposit() и withdraw() класса Checking.
// Название файла Bank.java
public class Bank {
public static void main(String [] args) {
Checking c = new Checking(101);
System.out.println("Депозит $300...");
c.deposit(300.00);
try {
System.out.println("\nСнятие $100...");
c.withdraw(100.00);
System.out.println("\nСнятие $400...");
c.withdraw(400.00);
}catch(InsufficientFundsException e) {
System.out.println("Извините, но у Вас $" + e.getAmount());
e.printStackTrace();
}
}
}
Скомпилируйте все три выше обозначенные файла и произведите запуск Bank. Вследствие этого будет получен следующий результат:
Депозит $300...
Снятие $100...
Снятие $400...
Извините, но у Вас $200.0
InsufficientFundsException
at Checking.withdraw(Checking.java:25)
at Bank.main(Bank.java:13)
Общие исключения
В Java можно выделить две категории исключений и ошибок.
- Исключения JVM – данная группа представлена исключениями/ошибками, которые вызываются непосредственно и логически со стороны JVM. Примеры: NullPointerException, ArrayIndexOutOfBoundsException, ClassCastException.
- Программные исключения – данные исключения вызываются непосредственно приложением либо программистами API. Примеры: IllegalArgumentException, IllegalStateException.