-
Notifications
You must be signed in to change notification settings - Fork 3
Exceptions
###Введение Часто на собеседованиях спрашивают про Exceptions и их обработку. Поэтому решил написать про это.
В нашем приложении иногда(а возможно и часто) происходят какие-то ошибки. Что же с ними делать? Можно их либо игнорировать, либо возвращать некий код ошибки, а после уже расшифровывать его, либо, как в Java, использовать механизм Exceptions.
Exception - это прежде всего объект, хранящий в себе какие то данные, место где произошла ошибка, причину и т.д
Очень распространенная штука это - Exceptions, но что это такое?
На пальцах - Exception это сигнал о нестандартной ситуации.
Например - мы не можем закрыть файл или удалить что-то, не хватает памяти и т.д
Ниже мы видим иерархию исключений, корень - java.lang.Throwable
и два класса - java.lang.Exception
и java.lang.Error
Ну а java.lang.Exception
это super class для java.lang.RuntimeException
java.lang.Throwable
и java.lang.Exception
это так называемые checked exceptions(Проверяемые), java.lang.RuntimeException
и java.lang.Error
это unchecked.
Checked exceptions проверяются компилятором в compile time. И их мы обязаны обработать. В то время как RE(Runtime Exceptions) мы не обязаны явно перехватывать и обрабатывать, ну на то они и runtime:)
Мы также можем перехватывать исключения. И перехватывать мы можем все виды исключений.
Но тогда зачем нам столько видов исключений? Давайте подумаем - какая разница между Error и Exception?
Error - это более серьезная ситуация, нежели Exception. Т.е если происходит что-то такое, что мы либо не можем исправить, либо это крайне сложно починить, например, у нас закончилась память или мы вызываем несуществующий метод - это вот Error
. Т.е непросто исключительная ситуация - а прямо рядом с паникой:)
Но если мы не можем закрыть файл, мы делаем какой-нибудь запрос, не можем строку к число преобразовать - это ситуации, после которых мы можем продолжить работать, мы можем перехватить это, обработать(например - попросить пользователя ввести еще раз число) - это java.lang.Exception
.
Т.е разница - в логическом разделении.
Теперь посмотрим глубже на Exceptions - java.lang.Exception
and java.lang.RuntimeException
. RuntimeException - это исключения падающие в Runtime, т.е когда мы уже работаем с нашим приложением.
Надо понимать, что Exceprions - это class, т.е мы туда можем положить свою логику, методы и переменные, если надо. Однако кидать исключения - довольно дорогая операция, поэтому не надо просто так разбрасываться ими.
Когда что кидать?
-
java.lang.Exception Это ситуации, которые нам не подконтрольны, т.е не смогли закрыть файл, не смогли дессериализовать класс - мы ничего не можем сделать, но можем отреагировать. Опять же мы обязаны такие исключения обрабатывать. Либо прокидывать вверх с помощью throws, в сигнатуру метода. Метод может декларировать сколько угодно исключений.
-
java.lang.RuntimeException А вот тут уже - это наши ошибки, ошибки разработчика скорее. Т.е обращаемся к null, делим на ноль где-то и прочее. Мы можем эти ошибки перехватывать, а можем пропускать. Ловить такие ошибки постоянно - не совсем правильно наверное, так как мы можем так и не понять причину ошибок и падений.
-
java.lang.Error Критические ошибки, после которых мы не можем или с трудом можем продолжать работу. Как и все остальные - мы можем такие ошибки ловить, но зачем? Ловить их можно только в случае, если мы можем или знаем как нам поступить в таких ситуациях.
Кидать мы можем тоже любые исключения.
Ловить и обрабатывать ошибки - try/catch/finally.
Сразу надо сказать, что try-catch блок - не транзакционный, т.е мы создаем объекты в try, работаем и тут хоп! - ошибка, так вот объекты так и останутся в памяти, если мы их не почистим в finally или catch блоке.
Также, мы не можем работать с объектами из try блока в других блоках - почему? Потому что компилятор не может нам гарантировать, что эти объекты создались, а вдруг у вас там кинулся Exception? Тогда после исключения строчки кода не выполняются и объекты мы не создали, а значит и работать с ними не можем.
Еще один момент, это так называемый re-throw. Это когда вы перехватываете exception в catch-block, формируете там еще один и выбрасываете уже его, возможно(почти обязательно) вкладывая предыдущий в этот. Т.е:
try {
Reader readerConf = ....
readerConf.readConfig();
} catch(IOException ex) {
//logging error
throw new ComponentException(ex)
}
Что мы сделали?Мы перехватили ошибку чтения, обернули ее в новую ошибку и выкинули наверх. Для чего это нужно? Это полезно для гибкой обработки ошибок, т.е неважно что стало с нашим конфигом(нет файла, нет какой-то строки) - мы формируем ошибку, что мы не можем работать с этим компонентом программы.
Расположение catch blocks - важно. Пусть у нас метод бросает IOException и Exception, мы пишем что-то типа такого:
try {
method();
} catch (Exception e) {//do some logic 1}
catch (IOException e) {//do some logic 2}
И видим, что IOException обработка недостижима, мы более широкий фильтр "отлова" установили выше.
Это похоже на то, как мы используем сито. Есть более широкие, есть уже. Так вот, если установить выше всех узкое сито - до низу почти ничего не дойдет, ведь мы все перехватим выше. Принцип ясен.
Отсюда понятно, что если нам надо перехватить прямо вот все, то надо ловить Throwable:
try {
method();
} catch (Throwable t) {//do some logic 1}
Отметим, что в таком подходе мы ловим и RE, и Error!
Ловим RE:
try {
String numberAsString = "one";
Double res = Double.valueOf(numberAsString);
} catch (RuntimeException re) {
System.out.println("Error while convert string to double!");
}
Ловим Error:
try {
throw new Error();
} catch (RuntimeException re) {
System.out.println("RE");
} catch (Error error) {
System.out.println("ERROR");
}
}
В целом - что мы ставим в catch - все, что ниже по иерархии мы и ловим. Все вроде бы ясно и понятно. Теперь вот еще что.
Полезно иногда иметь свою иерархию Exceptions. Например, у нас есть method2() и он кидает целых 3 разных Exception-а. Имея свою иерархию мы просто пишем 3 catch block-а на каждое исключение и обрабатываем как нам надо. Без этого у нас был бы только один catch block с перехватом Exception, где мы уже будем понимать что мы вообще перехватили и как это обрабатывать.
Идем дальше.
А что если наше исключение прерывает поток(interrupt thread) - все просто используем Thread.UncaughtExceptionHandler
.
И еще раз! Аккуратнее с обработкой исключений.
public static void main(String[] args){
try {
try {
throw new Exception("0");
} finally {
if (true) {
throw new IOException("1");
}
System.err.println("2");
}
} catch (IOException ex) {
System.err.println(ex.getMessage());
} catch (Exception ex) {
System.err.println("3");
System.err.println(ex.getMessage());
}
}
Видим на выходе "1"!
####Вывод
- Используем обрабатываемые исключения в случае, когда мы понимаем, что тут может быть ошибка.
- Не обрабатываемое исключение - если это наша ошибка.
- Полезно иметь свою иерархию исключений.
- Используйте finally, если работаете с ресурсами и try-with-resources
- finally блок отрабатывает всегда.
- Кидаем исключения с помощью
throw
- Поднять исключение выше -
throws
- Все проверяемые исключения обязаны быть отловлены!
- try-catch block не транзакционный - все объекты, которые создали в try ДО исключения остаются в памяти.