Описание классов ошибок java

Классы исключений

Последнее обновление: 23.04.2018

Базовым классом для всех исключений является класс Throwable. От него уже наследуются два класса: Error
и Exception. Все остальные классы являются производными от этих двух классов.

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

Собственно исключения наследуются от класса Exception. Среди этих исключений следует выделить класс RuntimeException.
RuntimeException является базовым классом для так называемой группы непроверяемых исключений (unchecked exceptions) —
компилятор не проверяет факт обработки таких исключений и их можно не указывать вместе с оператором throws в объявлении метода.
Такие исключения являются следствием ошибок разработчика, например, неверное преобразование типов или выход за пределы массива.

Некоторые из классов непроверяемых исключений:

  • ArithmeticException: исключение, возникающее при делении на ноль

  • IndexOutOfBoundException: индекс вне границ массива

  • IllegalArgumentException: использование неверного аргумента при вызове метода

  • NullPointerException: использование пустой ссылки

  • NumberFormatException: ошибка преобразования строки в число

Все остальные классы, образованные от класса Exception, называются проверяемыми исключениями (checked exceptions).

Некоторые из классов проверяемых исключений:

  • CloneNotSupportedException: класс, для объекта которого вызывается клонирование, не реализует интерфейс Cloneable

  • InterruptedException: поток прерван другим потоком

  • ClassNotFoundException: невозможно найти класс

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

public Person clone() throws CloneNotSupportedException{
    
    Person p = (Person) super.clone();
    return p;
}

В итоге получается следующая иерархия исключений:

Иерархия классов исключений в Java

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

  • Метод getMessage() возвращает сообщение об исключении

  • Метод getStackTrace() возвращает массив, содержащий трассировку стека исключения

  • Метод printStackTrace() отображает трассировку стека

Например:

try{
    int x = 6/0;
}
catch(Exception ex){
        
    ex.printStackTrace();
}

% Throwable

Класс: java.lang.Throwable

Описание

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

Объект-исключение содержит в себе текстовое сообщение, говорящее о причине ошибки, трассировку стека вызовов на момент
создания, а также, возможно, причину (cause) — исключение более низкого уровня, завёрнутое в данное исключение.

Иерархия исключений

Все исключения в Java делятся на две большие и неравные группы. Меньшая группа наследует от класса Error и обозначает
серьёзные низкоуровневые ошибки, после которых продолжение выполнения программы обычно бессмысленно. Большая группа
наследует от класса Exception и отвечает за обычные нештатные ситуации, которые могут возникнуть в ходе выполнения
программы.

Теоретически язык Java не запрещает определить свой подкласс класса Throwable, не производный ни от Error, ни от
Exception. Компилятор будет рассматривать такую неведому зверушку как обычное проверяемое исключение (см. ниже), но
на практике так никто не поступает, да и необходимости в этом нет.

Проверяемые и непроверяемые исключения

Классы Error и RuntimeException занимают особое положение в иерархии исключений. Эти классы, а также все производные
от них, являются непроверяемыми исключениями (unchecked exceptions). Все остальные исключения являются проверяемыми
(checked exceptions). На диаграмме классов выше непроверяемые исключения выделены голубым, а проверяемые
— зелёным.

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

void throwsIOException() throws IOException {
	if (MoonPhase.getCurrent() == MoonPhase.FULL) {
		// непроверяемое исключение; можно обработать, но компилятор не заставляет
		throw new IllegalStateException();
	} else {
		// проверяемое исключение
		throw new IOException();
	}
}

void handlesIOException() {
	try {
		throwsIOException();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

В стандартной библиотеке есть одно нарушение этого правила. Статический метод Class.newInstance передаёт во внешний
код любые исключения, в том числе проверяемые, что позволяет обойти проверки времени компиляции:

public class Thrower {
    private Thrower () throws IOException {
        throw new IOException();
    }

    public static void main(String[] args) {
        try {
            Thrower t = Thrower.class.newInstance();
        } catch (IllegalAccessException | InstantiationException e) {
            // Ну хотя бы эти нужно обрабатывать, но уже поздно
        }
    }
}

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

Ошибки (Error)

Исключения, производные от класса Error, являются непроверяемыми и обладают объединяющими свойствами:

  1. Они сигнализируют о серьёзных ошибках низкого уровня, после которых восстановление обычно невозможно.
  2. Многие из них (в частности, OutOfMemoryError, LinkageError и ThreadDeath) могут возникнуть практически в любом
    месте программы, поэтому настраиваться на них заранее обычно бессмысленно. Кроме того, место, где выбрасывается
    исключение, обычно не связано с местом логического возникновения нижележащей проблемы.

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

Вот некоторые наиболее распространённые исключения типа Error:

StackOverflowError
: Переполнение стека вызовов. Обычно это указывает на бесконечную рекурсию.

OutOfMemoryError
: Переполнение кучи, причём сборщик мусора уже попытался её почистить и беспомощно развёл руками. Обычно это указывает
на утечки памяти, вызванные хранением ссылок на множество ненужных объектов в корневых переменных.

LinkageError
: Базовый класс для различных ошибок при загрузке классов, необходимых для работы выполняемого кода. Сюда относятся в
том числе NoClassDefFoundError и ExceptionInInitializerError.

NoClassDefFoundError
: JVM не может найти класс, к которому пытается обратиться код. Это может случиться, если код был скомпилирован с
зависимостью от какого-то класса или библиотеки, но запущен при отсутствии этого класса/библиотеки в classpath.

ExceptionInInitializerError
: При инициализации статического поля класса или внутри блока static выбросилось исключение, к которому можно
доступиться через метод getCause.

ThreadDeath
: В многопоточной программе чужой поток нагло и бесцеремонно завершил работу нашего потока методом Thread.stop. Если
поток завершается по этому исключению, по умолчанию сообщение об ошибке подавляется и не пишется в консоль.

AssertionError
: Нарушено базовое условие, которое в корректно работающей программе должно выполняться всегда. Исключения этого класса
бросает инструкция assert при запуске JVM с параметром -ea (enable assertions), а также библиотека JUnit при провале
тестов.

Как правило, не стоит выбрасывать исключения типа Error инструкцией throw, кроме исключения AssertionError. Его
принято выбрасывать как «невозможное» исключение, чтобы «заткнуть» выбросом исключения пути выполнения, которые заведомо
никогда не будут выполнены, но компилятор об этом не знает. Часто это приходится делать при использовании перечислимых
типов в инструкции switch:

public enum Stoplight { RED, YELLOW, GREEN }
public enum CarState { STOPPED, STOPPING, MOVING }

public class Car {
	public CarState approachIntersection(Intersection intersection) {
		switch (intersection.getStoplight()) {
		case RED:
			stop();
			return CarState.STOPPING;
		case YELLOW:
			if (stopIfPossible()) {
				return CarState.STOPPING;
			} else {
				return CarState.MOVING;
			}
		case GREEN:
			keepMoving();
			return CarState.MOVING;
		default:
			// не может случиться
			throw new AssertionError();
		}
	}
	
	public CarState handleStoplightSwitch(Stoplight stoplight) {
		switch (stoplight) {
		case RED:
			return CarState.STOPPED;
		case YELLOW:
			prepareToMove();
			return CarState.STOPPED;
		case GREEN:
			startMoving();
			return CarState.MOVING;
		default:
			// не может случиться
			throw new AssertionError();
		}
	}
}

В нашем случае у светофора всего три возможных состояния, но компилятор тем не менее требует, чтобы блок switch
обработал невозможную ситуацию default. С помощью throw new AssertionError мы затыкаем компилятор и говорим
сопровождающему код, что эта строка кода заведомо не выполнится.

Ещё один частый use case для AssertionError — блок catch для исключения, невозможного по определению.
Например, стандартные классы URLEncoder и URLDecoder имеют методы, принимающие два параметра: перекодируемую строку
и строку с именем кодировки.

String url = "https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA";
String decodedUrl = URLDecoder.decode(url, "UTF-8");

Этот код просто так не скомпилируется, потому что метод URLDecoder.decode объявлен как
throws UnsupportedEncodingException. Однако кодировка UTF-8 гарантированно поддерживается в любой реализации JVM,
поэтому на самом деле это исключение никогда не выбросится (но компилятор, увы, об этом не знает). Нам придётся
обернуть этот код в идиому «невозможное исключение» (impossible exception):

try {
	String url = "https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BA";
	String decodedUrl = URLDecoder.decode(url, "UTF-8");
	// https://ru.wikipedia.org/wiki/Стек
} catch (UnsupportedEncodingException e) {
	// не может случиться
	throw new AssertionError(e);
}

Большинство методов, работающих с кодировками, имеют версии, принимающие вместо строки с именем кодировки объект
класса Charset и не бросающие UnsupportedEncodingException. Им можно передавать константы из класса
StandardCharsets:

List<String> fileContents = Files.readAllLines(
		Paths.get("отчёт.txt"), StandardCharsets.UTF_8);
fileContents.forEach(System.out::println);

К сожалению, некоторые старые классы, в том числе URLEncoder и URLDecoder, так и не были обновлены для поддержки
параметров типа Charset.

Исключения времени выполнения (RuntimeException)

Строго говоря, все исключения возникают во время выполнения, но класс RuntimeException занимает особое положение.
Во-первых, RuntimeException и все производные от него исключения являются непроверяемыми. Во-вторых,
многие из этих исключений сигнализируют об ошибках в программном коде и в корректно написанной программе не должны
возникать вообще. Такие исключения полезно выбрасывать в собственных public-методах в качестве предусловий
(preconditions), краткий смысл которых состоит в том, чтобы сказать вызывающему коду: «Ты виноват, такой ситуации вообще
не должно быть».

Вот некоторые самые важные из них:

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

IllegalArgumentException
: Метод был вызван с недопустимым значением параметра. Например, один из конструкторов стандартного класса Color,
принимающий три целых числа RGB, выбрасывает это исключение, если переданные числа не лежат в диапазоне 0–255.

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

IndexOutOfBoundsException
: Попытка обратиться к упорядоченной последовательности элементов (массиву, строке или списку) по индексу, лежащему
вне допустимых значений. Часто происходит при ошибке на единицу, когда в качестве индекса используется length или
size.

NoSuchElementException
: Выбрасывается итератором при попытке прочитать элемент за концом последовательности (когда hasNext() == false). В
корректной реализации итератора это исключение возможно только при ручной работе с итератором и невозможно при
использовании цикла for-each.

UnsupportedOperationException
: Вызываемая операция в принципе запрещена для этого объекта. Например, неизменяемые коллекции выбрасывают это
исключение при попытке изменить их методами set, add или remove.

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

ClassCastException
: Попытка привести объект к несовместимому типу, например, (String) new Object().

Все эти исключения объединяет то, что в корректно написанной программе они не должны выбрасываться. Если одно из этих
исключений ловится в программе, это знак неверной логики кода. Часто отлов такого исключения можно заменить проверкой;
например, вместо отлова ClassCastException использовать оператор instanceof, вместо IndexOutOfBoundsException
— проверку диапазона (index >= 0 && index < size()), а вместо NoSuchElementException — проверку
Iterator.hasNext().

Прочие исключения (Exception)

Исключения, производные от Exception, но не RuntimeException, являются проверяемыми. Их очень много, и сторонние
библиотеки часто добавляют свои собственные классы исключений. Вот лишь несколько примеров:

ReflectiveOperationException
: Базовый класс для исключений, возникающих при работе с механизмом рефлексии. Сюда относятся IllegalAccessException
(попытка доступиться извне к членам класса с доступом private, protected или package-private), NoSuchMethodException
(метод не найден) и InvocationTargetException (оборачивает исключение, выброшенное вызываемым через рефлексию
методом). Отдельно стоит выделить…

ClassNotFoundException
: Не путать с NoClassDefFoundError. Выбрасывается методом Class.forName, если класс с таким именем не найден. Это
исключение, в отличие от NoClassDefFoundError, предназначено для того, чтобы его перехватывали и обрабатывали.

CloneNotSupportedException
: По умолчанию выбрасывается protected-методом Object.clone, если объект не реализует интерфейс Cloneable. По
каким-то непонятным причинам конченые укурки, проектировавшие Java 1.0, сделали это исключение проверяемым, и при
реализации Cloneable-классов это исключение приходится подавлять.

SQLException
: Базовый класс для ошибок при работе с базами данных через JDBC API.

IOException
: Базовый класс для исключений при операциях ввода-вывода, к которым относятся операции, в том числе, с файловой
системой и сетью. У этого класса очень много подклассов, обозначающих конкретные ситуации; например,
ConnectException бросается при неудачной попытке соединения с сервером.

FileSystemException
: Подкласс IOException, являющийся базовым классом для исключений при операциях с файловой системой.

AccessDeniedException
: Попытка обратиться к файлу, на доступ к которому у пользователя нет прав — например, попытка открыть
защищённый системный файл для записи.

NoSuchFileException
: Попытка обратиться к несуществующему файлу.

Класс NoSuchFileException появился в Java 7 и является частью нового файлового API (классы Path и Files). В
старых API, спроектированных для Java 6 и ниже, можно встретить более старое исключение FileNotFoundException,
присутствовавшее ещё в Java 1.0. К сожалению, это исключение не позволяет различить ситуации «файл не найден» и «доступ
запрещён» и выбрасывается старыми API в обоих этих случаях.

Использование проверяемых и непроверяемых исключений

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

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

  1. Вызывающий код не может гарантированно избежать ошибочной ситуации, то есть она находится вне его полного контроля.
  2. Ошибочная ситуация является «стандартной», то есть в любом случае автору вызывающего кода нужно думать о том, что
    произойдёт при возникновении ошибки. Например, при попытке открыть файл может оказаться, что файл недоступен.
  3. Вызывающий код может осмысленно обработать ошибку или даже восстановиться после неё.

Хорошими примерами проверяемых исключений являются исключения при работе с файловой системой, сетью, базами данных,
окнами, другими процессами в системе и т.д. Все эти сущности объединяет то, что это внешние ресурсы, корректную работу
которых вызывающий код не может полностью гарантировать. Плохой пример проверяемого исключения — исключение
CloneNotSupportedException, которое может возникнуть только при логической ошибке в использовании метода clone.
Если бы класс CloneNotSupportedException был написан в наши дни, скорее всего, это исключение было бы непроверяемым.

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

  • Низкоуровневые ошибки самой JVM и загруженных классов (Error).
  • Ошибки в логике программы, необнаружимые на этапе компиляции (RuntimeException).
  • Ошибки, при которых продолжение выполнения кода не имеет смысла. Например, в серверных приложениях это могут быть
    ошибки конфигурации сервера, при которых приложение вообще не может запуститься, или ошибки доступа к основной базе
    данных, с которой работает серверное приложение (здесь само приложение всё равно не сможет восстановиться). Как правило,
    такие исключения тоже наследуют от RuntimeException.

Создание исключения

Объекты исключений, как правило, создаются прямо в инструкции throw:

throw new SomeException(параметры);

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

SomeException()

SomeException(String message)

SomeException(Throwable cause)

SomeException(String message, Throwable cause)

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

try {
	readConfigFile("server-config.xml");
} catch (XMLStreamException e) {
	throw new ServerStartupException("Invalid server configuration format", e);
} catch (IOException e) {
	throw new ServerStartupException("Cannot access server configuration", e);
}

У некоторых очень старых классов исключений, написанных до выхода Java 1.4, может не быть конструкторов с параметром
cause. Для них можно установить причину после создания объекта с помощью метода initCause:

throw new OldException("Message").initCause(e);

Получение информации об исключении

Все исключения поддерживают три базовых операции:

String getMessage()
: Возвращает короткое, в одно-два предложения, сообщение об ошибке, переданное в объект исключения при его создании.

Throwable getCause()
: Возвращает исключение, послужившее причиной данного исключения, или null, если таковое не было задано. У причины,
в свою очередь, тоже может быть причина; с помощью последовательных вызовов getCause можно восстановить причинную
цепь исключений (causal chain).

void printStackTrace()
void printStackTrace(PrintStream s)
void printStackTrace(PrintWriter s)
: Выводит трассировку стека (stack trace) в указанный байтовый или символьный поток (по умолчанию — в
System.err). Варианты с PrintStream и PrintWriter бывают полезны, чтобы записать трассировку стека в файл или
сетевой поток, либо получить её в виде строки с помощью StringWriter.

Если у исключения есть причина, методы printStackTrace вслед за трассировкой стека самого исключения печатают
трассировку стека его причины, затем — причины причины и так далее.

По умолчанию для всех исключений, кроме ThreadDeath, при завершении потока по необработанному исключению трассировка
стека записывается в System.err с помощью printStackTrace. Это относится и к главному потоку при аварийном
завершении метода main.

Как пример, вот такая неправильная программа

public class ExceptionChain {
	private static class BadInitializer {
		private static final char CONSTANT = "Hello".charAt(5);
	}
	
	public static void main(String[] args) {
		System.out.println(BadInitializer.CONSTANT);
	}
}

при выполнении выдаст:

Exception in thread "main" java.lang.ExceptionInInitializerError
	at ExceptionChain.main(ExceptionChain.java:11)
Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 5
	at java.lang.String.charAt(String.java:658)
	at ExceptionChain$BadInitializer.<clinit>(ExceptionChain.java:7)
	... 1 more

Исключение (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 FilesJavajdk1.8.0_60binjava»

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

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

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

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

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

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

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

Process finished with exit code 1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. 1.

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

  2. 2.

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

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

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

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

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

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

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

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

public static void main(String[] args) {

public static void methodA() {

private static void methodB() {

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

Exception in thread «main» java.lang.StackOverflowError

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Process finished with exit code 1

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

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

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

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

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

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

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

  1. 1.

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

  2. 2.

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

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

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

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

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

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

public static void main(String[] args) {

Scanner scanner = new Scanner(System.in);

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

String firstNumber = scanner.nextLine();

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

String secondNumber = scanner.nextLine();

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

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

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

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

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

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

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

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

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

a = Integer.parseInt(firstNumber);

b = Integer.parseInt(secondNumber);

} catch (NumberFormatException e) {

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

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

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

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

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

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

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

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

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

} catch (NumberFormatException e) {

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

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

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

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

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

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

public static void main(String[] args) {

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

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

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

public static int getNumberFromConsole(String message) {

Scanner scanner = new Scanner(System.in);

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

String s = scanner.nextLine();

return Integer.parseInt(s);

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

  1. 1.

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

  2. 2.

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

  3. 3.

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

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

public static int getNumberFromConsole(String message) throws NumberFormatException {

Scanner scanner = new Scanner(System.in);

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

String s = scanner.nextLine();

return Integer.parseInt(s);

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

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

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

public static void main(String[] args) {

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

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

} catch (NumberFormatException e) {

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

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

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

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

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

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

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

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

public static void main(String[] args) {

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

BufferedReader reader = Files.newBufferedReader(p);

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

System.out.println(line);

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

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

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

public static void main(String[] args) {

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

BufferedReader reader = Files.newBufferedReader(p);

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

System.out.println(line);

} catch (IOException e) {

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

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

public static void main(String[] args) {

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

} catch (IOException e) {

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

public static void printFile(Path p) throws IOException {

BufferedReader reader = Files.newBufferedReader(p);

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

System.out.println(line);

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

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

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

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

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

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

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

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

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

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

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

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

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

public static void foo() {

} catch (ArithmeticException e) {

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

} catch (IndexOutOfBoundsException e) {

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

} catch (IllegalArgumentException e) {

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

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

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

public static void main(String[] args) {

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

} catch (IOException e) {

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

} catch (FileNotFoundException e) {

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

public static void printFile(Path p) throws IOException {

BufferedReader reader = Files.newBufferedReader(p);

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

System.out.println(line);

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

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

public static void foo() {

} catch (ArithmeticException | IllegalArgumentException | IndexOutOfBoundsException e) {

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

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

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

public static void foo() {

} catch (IllegalArgumentException e) {

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

} catch (ArithmeticException e) {

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

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

public static void foo(int a) {

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

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

public static void foo(int a) {

FileOutputStream fout = null;

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

fout = new FileOutputStream(file);

} catch (IOException e) {

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

} catch (IOException e) {

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

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

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

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

public static void foo() {

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

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

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

System.out.println(line);

} catch (IOException e) {

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

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

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

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

  1. 1.

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

  2. 2.

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

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

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

  1. 1.

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

  2. 2.

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

  3. 3.

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

Исключения

  • Исключения
    • Введение
    • Иерархия исключений
      • Проверяемые и непроверяемые
      • Иерархия
        • Классификация
        • Error и Exception
    • Работа с исключениями
      • Обработка исключений
        • Правила try/catch/finally
        • Расположение catch блоков
        • Транзакционность
      • Делегирование
      • Методы и практики работы с исключительными ситуацими
        • Собственные исключения
        • Реагирование через re-throw
        • Не забывайте указывать причину возникновения исключения
        • Сохранение исключения
        • Логирование
        • Чего нельзя делать при обработке исключений
      • Try-with-resources или try-с-ресурсами
      • Общие советы
        • Избегайте генерации исключений, если их можно избежать простой проверкой
        • Предпочитайте Optional, если отсутствие значения — не исключительная ситуация
        • Заранее обдумывайте контракты методов
        • Предпочитайте исключения кодам ошибок и boolean флагам-признакам успеха
    • Исключения и статические блоки
    • Многопоточность и исключения
    • Проверяемые исключения и их необходимость
    • Заключение
    • Полезные ссылки

Введение

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

(c) Морис Уилкс.

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

    public List<String> readAll(String path) {
        BufferedReader br = new BufferedReader(new FileReader(path));
        String line;
        List<String> lines = new ArrayList<>();
        while ((line = br.readLine()) != null) {
            lines.add(line);
        }
        
        return lines;
    }

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

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

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

Самая простая реакция — это возвращать boolean — признак успеха или некоторый код ошибки, например, какое-то число.
Пусть, 0 — это код удачного завершения приложения, 1 — это аварийное завершение и т.д.
Мы получаем код возврата и уже на него реагируем.

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

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

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

В-третьих, и это, на мой взгляд, самое главное — это небезопасно, так как подобный способ можно легко проигнорировать.

Lots of newbie’s coming in from the C world complain about exceptions and the fact that they have to put exception handling all over the place—they want to just write their code. But that’s stupid: most C code never checks return codes and so it tends to be very fragile. If you want to build something really robust, you need to pay attention to things that can go wrong, and most folks don’t in the C world because it’s just too damn hard.
One of the design principles behind Java is that I don’t care much about how long it takes to slap together something that kind of works. The real measure is how long it takes to write something solid.

In Java you can ignore exceptions, but you have to willfully do it. You can’t accidentally say, «I don’t care.» You have to explicitly say, «I don’t care.»

(c) James Gosling.

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

Что такое исключение? В некотором смысле можно сказать, что исключение — это некоторое сообщение, уведомляющее о проблеме, незапланированном поведении.

В нашем примере с чтением содержимого файла, источником такого сообщения может являться BufferedReader или FileReader. Сообщению необходим получатель/обработчик, чтобы перехватить его и что-то сделать, как-то отреагировать.

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

Что значит «ломает поток выполнения программы»?

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

Объездная

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

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

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

Иерархия исключений

Ниже приведена иерархия исключений:

Exception Hierarchy

Картинка большая, чтобы лучше запоминалась.

Для начала разберем загадочные подписи checked и unchecked на рисунке.

Проверяемые и непроверяемые

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

Как видно на рисунке, java.lang.Throwable и java.lang.Exception относятся к проверяемым исключениям, в то время как java.lang.RuntimeException и java.lang.Error — это непроверяемые исключения.

Принадлежность к тому или иному типу каждое исключение наследует от родителя.
Это значит, что наследники java.lang.RuntimeException будут unchecked исключениями, а наследники java.lang.Exceptionchecked.

Что это за разделение?

В первую очередь напомним, что Java — это компилируемый язык, а значит, помимо runtime(время выполнения кода), существует ещё и compile-time(то, что происходит во время компиляции).

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

Непроверяемые исключения — это исключения времени выполнения. Компилятор не будет от вас требовать обработки непроверяемых исключений.

В чём же смысл этого разделения на проверяемые и непроверяемые исключения?

Я думаю так: проверяемые исключения в Java — это ситуации, которые разработчик никак не может предотвратить и исключение является одним из вариантов нормальной работы кода.

Например, при чтении файла требуется обрабатывать java.io.FileNotFoundException и java.io.IOException, которые является потомками java.io.Exception.

Потому, что отсутствие файла или ошибка работы с вводом/выводом — это вполне допустимая ситуация при чтении.

С другой стороны, java.lang.RuntimeException — это скорее ошибки разработчика.
Например, java.lang.NullPointerException — это ошибка обращения по null ссылке, данную ситуацию можно предотвратить: проверить ссылку на null перед вызовом.

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

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

Разделение на проверяемые и непроверяемые исключения существует только в Java, в других языках программирования, таких как Scala, Groovy, Kotlin или Python, все исключения непроверяемые.

Это довольно холиварная тема и свои мысли по ней я изложу в конце статьи.

Теперь рассмотрим непосредственно иерархию исключений.

Иерархия

Итак, корнем иерархии является java.lang.Throwable, у которого два наследника: java.lang.Exception и java.lang.Error.
В свою очередь java.lang.Exception является родительским классом для java.lang.RuntimeException.

Занятно, что класс java.lang.Throwable назван так, как обычно называют интерфейсы, что иногда вводит в заблуждение новичков. Однако помните, что это класс! Запомнить это довольно просто, достаточно держать в уме то, что исключения могут содержать состояние (например, информация о возникшей проблеме).

Так как в Java все классы являются наследниками java.lang.Object, то и исключения (будучи тоже классами) наследуют все стандартные методы, такие как equals, hashCode, toString и т.д.

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

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

Классификация

Каждый тип исключения отвечает за свою область ошибок.

  1. java.lang.Exception

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

    Это проверяемые исключения, мы обязаны на такие исключения реагировать, это будет проверено на этапе компиляции.

    Пример: java.io.IOException, java.io.FileNotFoundException.

  2. java.lang.RuntimeException

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

    Это непроверяемые исключения, реагировать на них или нет решает разработчик.

    Пример: java.lang.NullPointerException.

  3. java.lang.Error

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

    Это непроверяемые исключения, реагировать на них или нет решает разработчик.

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

Теперь перейдем к вопросу: в чем же разница между java.lang.Error и java.lang.Exception?

Error и Exception

Все просто. Исключения java.lang.Error — это более серьезная ситуация, нежели java.lang.Exception.
Это серьезные проблемы в работе приложения, которые тяжело исправить, либо вообще неясно, можно ли это сделать.

Это не просто исключительная ситуация — это ситуация, в которой работоспособность всего приложения под угрозой! Например, исключение java.lang.OutOfMemoryError, сигнализирующее о том, что кончается память или java.lang.StackOverflowError – переполнение стека вызовов, которое можно встретить при бесконечной рекурсии.

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

Т.е разница — в логическом разделении.

Поэтому, java.lang.Error и его наследники используются только для критических ситуаций.

Работа с исключениями

Обработка исключений

Корнем иерархии является класс java.lang.Throwable, т.е. что-то «бросаемое».
А раз исключения бросаются, то для обработки мы будем ловить их!

В Java исключения ловят и обрабатывают с помощью конструкции try/catch/finally.

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

В блоках catch перечисляются исключения, на которые решено реагировать. Тут определяются блоки кода, предназначенные для решения возникших проблем. Это и есть объявление тех самых получателей/обработчиков исключений.

Пример:

public class ExceptionHandling {
    public static void main(String[] args) {
        try {
             // код
        } catch(FileNotFoundException fnf) {
            // обработчик на FileNotFoundException
        }
    }
}

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

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

Точно также и в Java, ставя фильтр на java.lang.RuntimeException вы ловите не только java.lang.RuntimeException, но и всех его наследников! Ведь эти потомки — это тоже runtime ошибки!

В блоке finally определяется код, который будет всегда выполнен, независимо от результата выполнения блоков try/catch. Этот блок будет выполняться независимо от того, выполнился или нет блок try до конца, было ли сгенерировано исключение или нет, и было ли оно обработано в блоке catch или нет.

Пример:

public class ExceptionHandling {
    public static void main(String[] args) {
        try {
             // some code
        } catch(FileNotFoundException fnf) {
            // обработчик 1
        } catch(RuntimeException re) {
            // обработчик 2
        } finally {
            System.out.println("Hello from finally block.");
        }
    }
}

В примере выше объявлен try блок с кодом, который потенциально может сгенерировать исключения, после try блока описаны два обработчика исключений, на случай генерации FileNotFoundException и на случай генерации любого RuntimeException.
Объект исключения доступен по ссылке exception.

Правила try/catch/finally

  1. Блок try находится перед блоком catch или finally. При этом должен присутствовать хотя бы один из этих блоков.

  2. Между try, catch и finally не может быть никаких операторов.

  3. Один блок try может иметь несколько catch блоков. В таком случае будет выполняться первый подходящий блок.

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

  4. Блок finally будет выполнен всегда, кроме случая, когда JVM преждевременно завершит работу или будет сгенерировано исключение непосредственно в самом finally блоке.

  5. Допускается использование вложенных конструкций try/catch/finally.

    public class ExceptionHandling {
        public static void main(String[] args) {
            try {
                 try {
                    // some code
                } catch(FileNotFoundException fnf) {
                    // обработчик 1
                }
            } catch(RuntimeException re) {
                // обработчик 2
            } finally {
                System.out.println("Hello from finally block.");
            }
        }
    }

Вопрос:

Каков результат выполнения примера выше, если в блоке try не будет сгенерировано ни одного исключения?

Ответ:

Будет выведено на экран: «Hello from finally block.».

Так как блок finally выполняется всегда.


Вопрос:

Теперь немного видоизменим код, каков результат выполнения будет теперь?

public class ExceptionHandling {
  public static void main(String[] args) {
    try {
         return;
    } finally {
         System.out.println("Hello from finally block");
    }
  }
}

Ответ:

На экран будет выведено: Hello from finally block.


Вопрос:

Плохим тоном считается прямое наследование от java.lang.Throwable.
Это строго не рекомендуется делать, почему?

Ответ:

Наследование от наиболее общего класса, а в данном случае от корневого класса иерархии, усложняет обработку ваших исключений. Проблему надо стараться локализовать, а не делать ее описание/объявление максимально общим. Согласитесь, что java.lang.IllegalArgumentException говорит гораздо больше, чем java.lang.RuntimeException. А значит и реакция на первое исключение будет более точная, чем на второе.


Далее приводится несколько примеров перехвата исключений разных типов:

Обработка java.lang.RuntimeException:

try {
    String numberAsString = "one";
    Double res = Double.valueOf(numberAsString);
} catch (RuntimeException re) {
    System.err.println("Error while convert string to double!");
}

Результатом будет печать на экран: Error while convert string to double!.

Обработка java.lang.Error:

try {
    throw new Error();
} catch (RuntimeException re) {
    System.out.println("RuntimeException");
} catch (Error error) {
    System.out.println("ERROR");
}

Результатом будет печать на экран: ERROR.

Расположение catch блоков

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

Это значит, что порядок расположения catch блоков важен.

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

void method() throws Exception {
    if (new Random((System.currentTimeMillis())).nextBoolean()) {
        throw new Exception();
    } else {
       throw new IOException();
    }
}

Конструкция new Random((System.currentTimeMillis())).nextBoolean() генерирует нам случайное значение false или true.

Для обработки исключений этого метода написан следующий код:

try {
  method();
} catch (Exception e) {
  // Обработчик 1
} catch (IOException e) {
  // Обработчик 2
}

Все ли хорошо с приведенным выше кодом?
Нет, код выше неверен, так как обработчик java.io.IOException в данном случае недостижим. Все дело в том, что первый обработчик, ответсвенный за Exception, перехватит все исключения, а значит не может быть ситуации, когда мы сможем попасть во второй обработчик.

Снова вспомним пример с мукой, приведенный в начале.

Так вот песчинка, которую мы ищем, это и есть наше исключение, а каждый фильтр это catch блок.

Если первым установлен фильтр ловить все, что является Exception и его потомков, то до фильтра ловить все, что является IOException и его потомков ничего не дойдет, так как верхний фильтр уже перехватит все песчинки.

Отсюда следует правило:

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

А что если на два разных исключения предусмотрена одна и та же реакция? Написание двух одинаковых catch блоков не приветствуется, ведь дублирование кода — это зло.

Поэтому допускается объединить два catch блока с помощью |:

try {
    method2();
} catch (IllegalArgumentException | IndexOutOfBoundsException e) {
    // Обработчик
}

Вопрос:

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

Ответ:

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

try {
  method();
} catch (Throwable t) {
  // Обработчик
}

Однако, делать так не рекомендуется, что наталкивает на следующий вопрос.


Вопрос:

Почему перехватывать java.lang.Throwable — плохо?

Ответ:

Дело в том, что написав:

try {
  method();
} catch (Throwable t) {
  // catch all
}

Будут перехвачены абсолютно все исключения: и java.lang.Exception, и java.lang.RuntimeException, и java.lang.Error, и все их потомки.

И как реагировать на все? При этом надо учесть, что обычно на java.lang.Error исключений вообще не ясно как реагировать. А значит, мы можем неверно отреагировать на исключение и вообще потерять данные. А ловить то, что не можешь и не собирался обрабатывать — плохо.

Поэтому перехватывать все исключения — плохая практика.


Вопрос-Тест:

Что будет выведено на экран при запуске данного куска кода?

public static void main(String[] args) {
    try {
        try {
            throw new Exception("0");
        } finally {
            if (true) {
                throw new IOException("1");
            }

            System.err.println("2");
        }
    } catch (IOException ex) {
        System.err.println(ex.getMessage());
    } catch (Exception ex) {
        System.err.println("3");
        System.err.println(ex.getMessage());
    }
}

Ответ:

При выполнении данного кода выведется «1».
Давайте разберем почему.

Мы кидаем исключение во вложенном try блоке: throw new Exception("0");.

После этого поток программы ломается и мы попадаем в finally блок:

if (true) {
    throw new IOException("1");
}

System.err.println("2");

Здесь мы гарантированно зайдем в if и кинем уже новое исключение: throw new IOException("1");.
При этом вся информация о первом исключении будет потеряна! Ведь мы никак не отреагировали на него, а в finally блоке и вовсе ‘перезатерли’ новым исключением.

На try, оборачивающий наш код, настроено два фильтра: первый на IOException, второй на Exception.

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

System.err.println(ex.getMessage());

Именно поэтому выведется 1.


Транзакционность

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

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

Что это значит?

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

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


Вопрос:

Работа с объектами из try блока в других блоках невозможна:

public class ExceptionExample {
    public static void main(String[] args) {
        try {
            String line = "hello";
        } catch (Exception e) {
            System.err.println(e);
        }

        // Compile error
        System.out.println(line); // Cannot resolve symbol `line`
    }
}

Почему?

Ответ:

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

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


Вернемся к примеру с грузовиком, чтобы объяснить все вышесказанное.

Объездная

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

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

Делегирование

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

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

Пример:

// Код написан только для ознакомительной цели, не стоит с него брать пример!
String readLine(String path) throws IOException {
    BufferedReader br = new BufferedReader(...);
    String line = br.readLine();

    return line;
}

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

Механизм throws введен для проброса проверяемых исключений.

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

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

Теперь пришла пора рассмотреть методы обработки исключительных ситуаций.

Методы и практики работы с исключительными ситуацими

Главное и основное правило при работе с исключениями звучит так:

На исключения надо либо реагировать, либо делегировать, но ни в коем случае не игнорировать.

Определить когда надо реагировать, а когда делегировать проще простого. Задайте вопрос: «Знаю ли я как реагировать на это исключение?».

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

Собственные исключения

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

Например, пусть есть некоторый справочник:

class Catalog {
    Person findPerson(String name);
}

В данном случае нам надо обработать ситуации, когда name является null, когда в каталоге нет пользователя с таким именем.

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

class PersonNotFoundException extends RuntimeException {
    private String name;

    // some code
}

Обратите внимание, что имя Person, по которому в каталоге не смогли его найти, выделено в свойство класса name.

Теперь при использовании этого метода проще реагировать на различные ситуации, такие как null вместо имени, а проблему с отсутствием Person в каталоге можно отдельно вынести в свой catch блок.

Реагирование через re-throw

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

Такой подход называется re-throw.

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

Как это выглядит на практике:

try {
    Reader readerConf = ....
    readerConf.readConfig();
} catch(IOException ex) {
    System.err.println("Log exception: " + ex);
    throw new ConfigException(ex);
}

Во время чтения конфигурационного файла произошло исключение java.io.IOException, в catch блоке оно было перехвачено, сделана запись в консоль о проблеме, после чего было создано новое, более конкретное, исключение ConfigException, с указанием причины (перехваченное исключение, ссылка на которое ex) и оно было проброшено дальше.

По итогу, из метода с приведенным кодом, в случае ошибки чтения конфигурации, будет выброшено ConfigException.

Для чего мы здесь так поступили?

Это полезно для более гибкой обработки исключений.
В примере выше чтение конфигурации генерирует слишком общее исключение, так как java.io.IOException это довольно общее исключение, но проблема в примере выше понятна: работа с этим конфигурационным файлом невозможна.

Значит и сообщить лучше именно как о том, что это не абстрактный java.io.IOException, а именно ConfigException. При этом, так как перехваченное исключение было передано новому в конструкторе, т.е. указалась причина возникновения (cause) ConfigException, то при выводе на консоль или обработке в вызывающем коде будет понятно почему ConfigException был создан.

Также, можно было добавить еще и текстовое описание к сгенерированному ConfigException, более подробно описывающее произошедшую ситуацию.

Еще одной важной областью применения re-throw бывает преобразование проверяемых исключений в непроверяемые.
В Java 8 даже добавили исключение java.io.UncheckedIOException, которое предназначено как раз для того, чтобы сделать java.io.IOException непроверяемым, обернуть в unchecked обертку.

Пример:

try {
    Reader readerConf = ....
    readerConf.readConfig();
} catch(IOException ex) {
    System.err.println("Log exception: " + ex);
    throw new UncheckedIOException(ex);
}

Не забывайте указывать причину возникновения исключения

В предыдущем пункте мы создали собственное исключение, которому указали причину: перехваченное исключение, java.io.IOException.

Чтобы понять как это работает, давайте рассмотрим наиболее важные поля класса java.lang.Throwable:

public class Throwable implements Serializable {

    /**
     * Specific details about the Throwable.  For example, for
     * {@code FileNotFoundException}, this contains the name of
     * the file that could not be found.
     *
     * @serial
     */
    private String detailMessage;

    // ...


    /**
     * The throwable that caused this throwable to get thrown, or null if this
     * throwable was not caused by another throwable, or if the causative
     * throwable is unknown.  If this field is equal to this throwable itself,
     * it indicates that the cause of this throwable has not yet been
     * initialized.
     *
     * @serial
     * @since 1.4
     */
    private Throwable cause = this;

    // ...
}

Все исключения, будь то java.lang.RuntimeException, либо java.lang.Exception имеют необходимые конструкторы для инициализации этих полей.

При создании собственного исключения не пренебрегайте этими конструкторами!

Поле cause используются для указания родительского исключения, причины. Например, выше мы перехватили java.io.IOException, прокинув свое исключение вместо него. Но причиной того, что наш код выкинул ConfigException было именно исключение java.io.IOException. И эту причину нельзя игнорировать.

Представьте, что код, использующий ваш метод также перехватил ConfigException, пробросив какое-то своё исключение, а это исключение снова кто-то перехватил и пробросил свое. Получается, что истинная причина будет просто потеряна! Однако, если каждый будет указывать cause, истинного виновника возникновения исключения, то вы всегда сможете обнаружить по этому стеку виновника.

Для получения причины возникновения исключения существует метод getCause.

public class ExceptionExample {
    public Config readConfig() throws ConfigException { // (1)
      try {
        Reader readerConf = ....;
        readerConf.readConfig();
      } catch (IOException ex) {
          System.err.println("Log exception: " + ex);
          throw new ConfigException(ex); // (2)
      }
    }

    public void run() {
        try {
            Config config = readConfig(); // (3)
        } catch (ConfigException e) {
            Throwable t = e.getCause(); // (4)
        }
    }
}

В коде выше:

  1. В строке (1) объявлен метод readConfig, который может выбросить ConfigException.
  2. В строке (2) создаётся исключение ConfigException, в конструктор которого передается IOException — причина возникновения.
  3. readConfig вызывается в (3) строке кода.
  4. А в (4) вызван метод getCause который и вернёт причину возникновения ConfigExceptionIOException.

Сохранение исключения

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

class Reader {
    // A holder of the last IOException encountered
    private IOException lastException;

    // some code
    
    public void read() {
        try {
            Reader readerConf = ....
            readerConf.readConfig();
        } catch(IOException ex) {
            System.err.println("Log exception: " + ex);
            lastException = ex;
        }
    }
}

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

Подобный прием использован в java.util.Scanner, где генерируемое исключение чтения потока сохраняется в свойство класса lastException.

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

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

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

class Example {
    private List<Exception> exceptions;

    // some code
    
    public void parse(String s) {
        try {
            // do smth
        } catch(Exception ex) {
            exceptions.add(ex);
        }
    }

    private void handleExceptions()  {
        for(Exception e : exceptions) {
            System.err.println("Log exception: " + e);
        }
    }
}

Логирование

Когда логировать исключение?

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

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

Поэтому не стоит преждевременно логировать исключение, например:

/**
 * Parse date from string to java.util.Date.
 * @param date as string 
 * @return Date object.
 */
public static Date from(String date) {
    try {
        DateFormat format = new SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH);
        return format.parse(date);
    }  catch (ParseException e) {
        logger.error("Can't parse ")
        throw e;
    }
}

Здесь ParseException является частью ожидаемой работы, в ситуациях, когда строка содержит невалидные данные.
Раз происходит делегирование исключения выше (с помощью throw), то и там, где его будут обрабатывать и лучше всего логировать, а эта запись в лог будет избыточной. Хотя бы потому, что в месте обработки исключения его тоже залогируют!

Подробнее о логировании.

Чего нельзя делать при обработке исключений

  1. Старайтесь не игнорировать исключения.

    В частности, никогда не пишите подобный код:

        try {
            Reader readerConf = ....
            readerConf.readConfig();
        } catch(IOException e) {
            e.printStackTrace();
        }
  2. Не следует писать ‘универсальные’ блоки обработки исключений.

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

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

    Поэтому таких ситуаций лучше не допускать.

  3. Старайтесь не преобразовывать более конкретные исключения в более общие.

    В частности, например, не следует java.io.IOException преобразовывать в java.lang.Exception или в java.lang.Throwable.

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

  4. Старайтесь не злоупотреблять исключениями.

    Если исключение можно не допустить, например, дополнительной проверкой, то лучше так и сделать.

    Например, можно обезопасить себя от java.lang.NullPointerException простой проверкой:

      if(ref != null) {
          // some code
      }

Try-with-resources или try-с-ресурсами

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

Чаще всего за закрытие ресурса будет отвечать код, наподобие этого:

try {
    // code
} finally {
    resource.close();
}

Освобождение ресурса (например, освобождение файлового дескриптора) — это поведение.

А за поведение в Java отвечают интерфейсы.

Это наталкивает на мысль, что нужен некоторый общий интерфейс, который бы реализовывали все классы, для которых необходимо выполнить какой-то код по освобождению ресурсов, т.е выполнить ‘закрытие’ в finally блоке и еще удобнее, если бы этот однообразный finally блок не нужно было писать каждый раз.

Поэтому, начиная с Java 7, была введена конструкция try-with-resources или TWR.

Для этого объявили специальный интерфейс java.lang.AutoCloseable, у которого один метод:

void close() throws Exception;

Все классы, которые будут использоваться так, как было описано выше, должны реализовать или java.lang.Closable, или java.lang.AutoCloseable.

В качестве примера, напишем код чтения содержимого файла и представим две реализации этой задачи: используя и не используя try-with-resources.

Без использования try-with-resources (пример ниже плох и служит только для демонстрации объема необходимого кода):

BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader(path));
    // read from file
} catch (IOException e) {
    // catch and do smth
} finally {
    try {
        if (br != null) {
            br.close();
        }
    } catch (IOException ex) {
        // catch and do smth
    }
}

А теперь то же самое, но в Java 7+:

try (FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr)) {
         // read from file
} catch (IOException e) {
         // catch and do smth
}

По возможности пользуйтесь только try-with-resources.

Помните, что без реализации java.lang.Closable или java.lang.AutoCloseable ваш класс не будет работать с try-with-resources так, как показано выше.


Вопрос:

Получается, что используя TWR мы не пишем код для закрытия ресурсов, но при их закрытии может же тоже быть исключение! Что произойдет?

Ответ:

Точно так же, как и без TWR, исключение выбросится так, будто оно было в finally-блоке.

Помните, что TWR, грубо говоря, просто добавляет вам блок кода вида:

finally {
    resource.close();
}

Вопрос:

Является ли безопасной конструкция следующего вида?

try (BufferedWriter bufferedWriter
        = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("a")))) {
}

Ответ:

Не совсем, если конструктор OutputStreamWriter или BufferedWriter выбросит исключение, то FileOutputStream закрыт не будет.

Пример, демонстрирующий это:

public class Main {
    public static void main(String[] args) throws Exception {
        try (ThrowingAutoCloseable throwingAutoCloseable
                     = new ThrowingAutoCloseable(new PrintingAutoCloseable())) { // (1)
        }
    }

    private static class ThrowingAutoCloseable implements AutoCloseable { // (2)
        private final AutoCloseable other;

        public ThrowingAutoCloseable(AutoCloseable other) {
            this.other = other;
            throw new IllegalStateException("I always throw"); // (3)
        }

        @Override
        public void close() throws Exception {
            try {
                other.close(); // (4)
            } finally {
                System.out.println("ThrowingAutoCloseable is closed");
            }
        }
    }

    private static class PrintingAutoCloseable implements AutoCloseable { // (5)
        public PrintingAutoCloseable() {
            System.out.println("PrintingAutoCloseable created"); // (6)
        }

        @Override
        public void close() {
            System.out.println("PrintingAutoCloseable is closed"); // (7)
        }
    }
}
  1. В строке (1) происходит заворачивание одного ресурса в другой, аналогично new BufferedWriter(new OutputStreamWriter(new FileOutputStream("a"))).
  2. ThrowingAutoCloseable (2) — такой AutoCloseable, который всегда бросает исключение (3), в (4) производится попытка закрыть полученный в конструкторе AutoCloseable.
  3. PrintingAutoCloseable (5) — AutoCloseable, который печатает сообщения о своём создании (6) и закрытии (7).

В результате выполнения этой программы вывод будет примерно следующим:

PrintingAutoCloseable created
Exception in thread "main" java.lang.IllegalStateException: I always throw
    at ru.misc.Main$ThrowingAutoCloseable.<init>(Main.java:19)
    at ru.misc.Main.main(Main.java:9)

Как видно, PrintingAutoCloseable закрыт не был!


Вопрос:

В каком порядке закрываются ресурсы, объявленные в try-with-resources?

Ответ:

В обратном.

Пример:

public class Main {
    public static void main(String[] args) throws Exception {
        try (PrintingAutoCloseable printingAutoCloseable1 = new PrintingAutoCloseable("1");
             PrintingAutoCloseable printingAutoCloseable2 = new PrintingAutoCloseable("2");
             PrintingAutoCloseable printingAutoCloseable3 = new PrintingAutoCloseable("3")) {
        }
    }

    private static class PrintingAutoCloseable implements AutoCloseable {
        private final String id;

        public PrintingAutoCloseable(String id) {
            this.id = id;
        }

        @Override
        public void close() {
            System.out.println("Closed " + id);
        }
    }
}

Вывод:

Closed 3
Closed 2
Closed 1

Общие советы

Избегайте генерации исключений, если их можно избежать простой проверкой

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

Помните, что если исключение можно не допустить, то лучше так и сделать.

Отсюда следует первый совет: не брезгуйте дополнительными проверками.

  1. Не ловите IllegalArgumentException, NullPointerException, ArrayIndexOutOfBoundsException и подобные.
    Потому что эти ошибки — это явная отсылка к тому, что где-то недостает проверки.
    Обращение по индексу за пределами массива, NullPointerException, все эти исключения — это ошибка разработчика.
  2. Вводите дополнительные проверки на данные, дабы избежать возникновения непроверяемых исключения

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

Предпочитайте Optional, если отсутствие значения — не исключительная ситуация

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

class Catalog {
    Person findPerson(String name);
}

Но и в этом случае генерации исключения можно избежать, если воспользоваться java.util.Optional:

Optional<Person> findPerson(String name);

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

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

Заранее обдумывайте контракты методов

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

class Person {
    void hello() throws RuntimeException {
        // some code
    }
}

// Compile Error
class PPerson extends Person {
    @Override
    void hello() throws Exception {
        // some code
    }
}

Если было явно указано, что метод может сгенерировать java.lang.RuntimeException, то нельзя объявить более общее бросаемое исключение при переопределении. Но можно указать потомка:

// IllegalArgumentException - потомок RuntimeException!
class PPerson extends Person {
    @Override
    void hello() throws IllegalArgumentException {
        // some code
    }
}

Что, в целом логично.

Если объявляется, что метод может сгенерировать java.lang.RuntimeException, а он выбрасывает java.io.IOException, то это было бы как минимум странно.

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

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

class PPerson extends Person {
    @Override
    void hello() {
        // some code
    }
}

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

Предпочитайте исключения кодам ошибок и boolean флагам-признакам успеха

  1. Исключения более информативны: они позволяют передать сообщение с описанием ошибки
  2. Исключение практически невозможно проигнорировать
  3. Исключение может быть обработано кодом, находящимся выше по стеку, а boolean-флаг или код ошибки необходимо обрабатывать здесь и сейчас

Исключения и статические блоки

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

Так вот, такие исключения оборачиваются в java.lang.ExceptionInInitializerError:

public class ExceptionHandling {
    static {
        throwRuntimeException();
    }

    private static void throwRuntimeException()  {
        throw new NullPointerException();
    }

    public static void main(String[] args)  {
        System.out.println("Hello World");
    }
}

Результатом будет падение со следующим стектрейсом:

java.lang.ExceptionInInitializerError Caused by: java.lang.NullPointerException at exception.test.ExceptionHandling.throwRuntimeException(ExceptionHandling.java:13) at exception.test.ExceptionHandling. (ExceptionHandling.java:8)

Многопоточность и исключения

Код в Java потоке выполняется в методе со следующей сигнатурой:

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

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

Например:

public class ExceptionHandling4 {
  public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread() {
        @Override
        public void run() {
                throw new RuntimeException("Testing unhandled exception processing.");
         }
    };
    t.start();
  }
}

Результатом выполнения этого кода будет то, что возникшее исключение прервет поток исполнения (interrupt thread):

Exception in threadThread-0java.lang.RuntimeException: Testing unhandled exception processing. at exception.test. ExceptionHandling4$1.run(ExceptionHandling4.java:27)

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

В таких ситуациях рекомендуется использовать Thread.UncaughtExceptionHandler.

t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
          public void uncaughtException(Thread t, Throwable e)   {
             System.out.println("Handled uncaught exception in thread :" + t + " Exception : " + e);
         }
  });

И вывод уже будет:

Handled uncaught exception in thread :Thread[Thread-0,5,main] Exception : java.lang.RuntimeException: Testing unhandled exception processing.

Необработанное исключение RuntimeException("Testing unhandled exception processing."), убившее поток, было перехвачено специальным зарегистрированным обработчиком.

Проверяемые исключения и их необходимость

В большинстве языков программирования, таких как C#, Scala, Groovy, Python и т.д., нет такого разделения, как в Java, на проверяемые и непроверяемые исключения.
Почему оно введено в Java было разобрано выше, а вот почему проверяемые исключения недолюбливают разработчики?

Основных причин две, это причины с: версионированием и масштабируемостью.

Представим, что вы, как разработчик библиотеки, объявили некоторый условный метод foo, бросающий исключения A, B и C:

void foo() throws A, B, C;

В следующей версии библиотеки в метод foo добавили функциональности и теперь он бросает еще новое исключение D:

void foo() throws A, B, C, D;

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

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

Проблема с масштабируемостью начинается тогда, когда происходит вызов не одного, а нескольких API, каждый из которых также несет с собой проверяемые исключения. Представьте, что помимо foo, бросающего A, B, C и D, в методе hello вызывается еще и bar, который также бросает E и T исключения. Как сказано выше, как реагировать чаще всего непонятно, поэтому эти исключения делегируются вызывающему коду, из-за чего объявление метода hello выглядит совсем уж угрожающе:

void hello() throws A, B, C, D, E, T {
    try {
        foo();
        bar();
    } finally {
        // clear resources if needed
    }
}

Все это настолько раздражающе, что чаще всего разработчики просто объявляют наиболее общее исключение в throws:

void hello() throws Exception {
    try {
        foo();
        bar();
    } finally {
        // clear resources if needed
    }
}

А в таком случае это все равно, что сказать «метод может выбросить исключение» — это настолько общие и абстрактные слова, что смысла в throws Exception практически нет.

Также есть еще одна проблема с проверяемыми исключениями. Это то, что с проверяемыми исключениями крайне неудобно работать в lambda-ах и stream-ах:

// compilation error
    Lists.newArrayList("a", "asg").stream().map(e -> {throw new Exception();});

Так как с Java 8 использование lambda и stream-ов распространенная практика, то накладываемые ограничения вызовут дополнительные трудности при использовании проверяемых исключений.

Поэтому многие разработчики недолюбливают проверяемые исключения, например, оборачивая их в непроверяемые аналоги с помощью re-throw.

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

В своей работе я стараюсь чаще использовать непроверяемые исключения, а проверяемые оборачивать в unchecked аналоги, как, например, java.io.IOException и java.io.UncheckedIOException.

Заключение

Иерархия исключений в Java.

Exception Hierarchy

Исключения делятся на два типа: непроверяемые(unchecked) и проверяемые(checked). Проверяемые исключения — это исключения, которые проверяются на этапе компиляции, мы обязаны на них отреагировать.

Проверяемые исключения в Java используются тогда, когда разработчик никак не может предотвратить их возникновение. Причину возникновения java.lang.RuntimeException можно проверить и устранить заранее, например, проверить ссылку на null перед вызовом метода, на объекте по ссылке. А вот с причинами проверяемых исключений так сделать не получится, так как ошибка при чтении файла может возникнуть непосредственно в момент чтения, потому что другая программа его удалила. Соответственно, при чтении файла требуется обрабатывать java.io.IOException, который является потомком java.lang.Exception.

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

Обработка исключений происходит с помощью конструкции try/catch/finally. Один блок try может иметь несколько catch блоков. В таком случае будет выполняться первый подходящий блок.

Помните, что try блок не транзакционен, все ресурсы, занятые в try ДО исключения остаются в памяти. Их надо освобождать и очищать вручную.
Если вы используете Java версии 7 и выше, то отдавайте предпочтение конструкции try-with-resources.

Основное правило:

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

Определить когда надо реагировать, а когда делегировать проще простого. Задайте вопрос: «Знаю ли я как реагировать на это исключение?».
Если ответ «да, знаю», то реагируйте, пишите обработчик и код, отвечающий за эту реакцию, если не знаете что делать с исключением, то делегируйте вызывающему коду.

Помните, что перехват java.lang.Error стоит делать только если вы точно знаете, что делаете. Восстановление после таких ошибок не всегда возможно и почти всегда нетривиально.
Не забывайте, что большинство ошибок java.lang.RuntimeException и его потомков можно избежать.

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

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

(c) Евгений Матюшкин.

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

Постарайтесь не создавать ‘универсальных’ обработчиков, так как это чревато трудноуловимыми ошибками.

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

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

Помните:

In Java you can ignore exceptions, but you have to willfully do it. You can’t accidentally say, «I don’t care.» You have to explicitly say, «I don’t care.»

(c) James Gosling.

Для закрепления материала рекомендую ознакомиться с ссылками ниже и этим материалом.

Полезные ссылки

  1. Книга С. Стелтинг ‘Java без сбоев: обработка исключений, тестирование, отладка’
  2. Oracle Java Tutorials
  3. Лекция Технострим Исключения
  4. Лекция OTUS Исключения в Java
  5. Лекция Ивана Пономарёва по исключениям
  6. Заметка Евгения Матюшкина про Исключения
  7. Failure and Exceptions by James Gosling
  8. The Trouble with Checked Exceptions by Bill Venners with Bruce Eckel
  9. Никто не умеет обрабатывать ошибки
  10. Исключения и обобщенные типы в Java
  11. Вопросы для закрепления

Исключение в 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.

Исключения

Причиной появления исключений, на мой взгляд, стало очень простое предположение — во время исполнения программы вряд ли можно быть уверенным, что все функции будут делать свою работу без сбоев. Например, если ваш код должен прочитать файл с каким-то именем, а файла такого просто нет ? Или вы хотите по сети обратиться к другой программе, но добрая старушка-уборщица выдернула шнур из компьютера ? В конце концов ваш калькулятор при вычислении дроби получил в знаменателе ноль. Да много ли каких еще препятствий может встретиться на пути.
Конечно, не все методы/функции обязательно будут оканчиваться крахом, но это обычное дело. И что в этом случае необходимо предпринять ? В старое доброе время в языке Си было принято возвращать какое-то значение. Причем в разных случаях тот же ноль мог означать что все хорошо, но мог означать что все плохо. В общем стандарта как не было, так до сих пор и нет — что должна возвращать функция в случае возникновения ошибки. Определенно можно сказать только одно — функция должна возвращать:

а) признак того, что произошла ошибка
б) информацию о том, а что собственно плохого произошло

С пунктом б) как раз достаточно просто — у нас же ООП. Значит удобно всю необходимую информацию поместить в какой-либо объект какого-то класса и вернуть. В этом объекте мы можем развернуться по полной — и сообщение написать и код ошибки выдать и много чего еще.
Что касается пункта а), то несложно догадаться, что возвращать ошибку как результат выполнения функции — плохая идея. Если мы ожидаем целое число, а ошибка будет описываться пусть даже просто строкой, то вряд ли кто-то сможет предложить что-то более-менее приемлемое. Но идея возвращать объект-описание ошибки — красивая и благодарная идея.
Отсюда родилась следующее решение — метод может генерировать исключение (объект-ошибку) и завершаться не вызовом return в случае ошибки, а иначе — метод будет «бросать» исключение.
И тогда код, вызывающий такой метод может получить ответ двумя способами:

  1. Обычным возвратом значения из метода, если все хорошо
  2. Будет «ловить» исключение

Соответственно метод, который хочет возвращать ошибку, тоже должен уметь возвращать какое-то значение (или ничего не возвращать в случае void) когда все хорошо и создавать и «бросать» исключения, если что-то пошло не так. Я уже дважды использовал термин «бросать», причем не без умысла — в языке Java используется глагол throw (бросать, кидать), посему больше не буду брать его в кавычки.
Мы получаем три момента для рассмотрения:

  1. Описание объекта-исключения — его же надо уметь создавать
  2. Описание метода, который умеет бросать исключения
  3. Как правильно ловить исключение

Итак, еще раз — надо создать исключение, надо правильно бросить исключение, надо правильно поймать исключение.

Класс для исключения

Научимся создавать исключения. Для таких классов существует специальная иерархия классов, которая начинается с класса java.lang.Throwable. (Надеюсь вы помните, что java.lang означет пакет, в котором находится описание класса. По сути директория). Если перевести это название получится что-то вроде «готовый к бросанию» — с литературным переводом возможно у меня не очень красиво получилось, но идея именно такая: объекты этого класса (и всех потомков) могут быть брошены. Этот класс редко используется для прямого наследования, чаще используется его потомок, класс java.lang.Exception. А уж у этого класса «детишек» очень много. Как вы наверно уже догадались, количество готовых классов для Java огромно. Вряд ли конечно на каждого человека на земле приходится один класс, хотя возможно я не так уж и далек от истины. В общем, уже готовых классов-исключений среди других много. Так что будет что изучать. Но о них мы поговорим несколько позже, а пока все-таки создадим наш класс-исключение.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

package edu.javacourse.exception;

public class SimpleException extends Exception

{

    // Это наше поле для хранения информации, присущей данному

    // классу-исключению. Поле немножко надуманное, но здесь может быть

    // и достаточно важная информация

    private int errorCode;

    // переопределяем конструктор

    public SimpleException(String message)

    {

        this(0, message);

    }

    // Создаем свой конструктор

    public SimpleException(int errorCode, String message)

    {

        // Вызываем конструктор предка

        super(message);

        // Добавляем инициализацию своего поля

        this.errorCode = errorCode;

    }

    // Метод для получения кода ошибки

    public int getErrorCode()

    {

        return errorCode;

    }

}

Как вы можете видеть ничего особенного в описании нет — создали класс, унаследовали его от класса Exception, определили свой конструктор, переопределили уже существующий — в общем ничего неординарного и загадочного. Единственное, что хотелось бы отметить — это наличие у класса Throwable (предка Exception) нескольких достаточно востребованных методов.

getMessage() — получить сообщение об ошибке, которое обычно имеет смысл читать
printStackTrace() — распечатать полный стек вызовов. Стек вызовов — это полный список всех методов внутри которых случилась ошибка. Т.е. если вызывался method1, внутри него method2, потом method3 и в нем случилось исключение, то вы увидите все три метода. Чуть позже мы с вами посмотрим пример использования этого метода. Не пренебрегайте им — он очень удобный и информативный

Метод для генерации исключения

Генерировать исключение можно в методе и этот метод может объявить, что он кидает исключения определенного класса. Делается это достаточно просто — после списка аргументов (в скобках) пишется ключевое слово throws и через запятую перечисляются классы исключений, который может порождать данный метод. Потом открывается фигурная скобка и мы пишем тело метода. Давайте посмотрим пример такого описания — наш класс Generator включает метод helloMessage который принимает в качестве строки имя, чтобы отдать строку «Hello, <имя>«. Но если имя не указано (указатель на строку равен null), то метод не возвращает например пустую строку — он кидает исключение, которое можно использовать в дальнейшей логике.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

package edu.javacourse.exception;

public class Generator

{

    // Данный метод описан с указанием того, что он способен кинуть

    // исключение типа SimpleException

    public String helloMessage(String name) throws SimpleException

    {

        if (name == null) {

            // Мы должны сначала создать объект-исключение

            SimpleException se = new SimpleException(10, «Message is null»);

            // Теперь мы можем «кинуть» это исключение — это другой способ выйти

            // из метода — отличный от варианта с return

            throw se;

            // Можно совместить создание и кидание — можете закомментировать

            // предыдущие строки и использовать нижеприведенную

            // throw new SimpleException(10, «Message is null»);

        }

        return «Hello, « + name;

    }

}

Опять смотрим код и видим, что после проверки на null мы создаем объект-исключение (обратите внимание — мы просто создаем нужный нам объект, заполняем его нужными значениями (мы в нем описываем реальную проблему, которая произошла) и кидаем. Опять же обращаю ваше внимание на то, что мы не делаем вызов return — этот вызов делается в случае если все хорошо. Мы кидаем исключение — пишем специальную конструкцию throw (не перепутайте — в описании метода пишем глагол в третьем лице единственного числа по английской грамматике с окончанием s — throws — «бросает», а в коде используем повелительное наклонение — throw — «бросай»).
Несколько позже мы рассмотрим еще несколько моментов, которые касаются описания методов, которые бросают исключения. Пока же перейдем к третьему шагу — как поймать исключение.

Ловим исключение

Для того, чтобы поймать исключение используется конструкция try … catch. Перед блоком кода, который порождает исключение пишем слово try и открываем фигурные скобки. После окончания блока закрываем скобки, пишем слово catch, в скобках указываем переменную класса-исключения, открываем скобки и там пишем код, который будет вызываться ТОЛЬКО в случае если метод/методы внутри try породили исключение. Смотрим код:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

package edu.javacourse.exception;

public class Starter

{

    public static void main(String[] args)

    {

        // создаем наш класс для генерации исключений

        Generator generator = new Generator();

        // Данный блок будет обрабатывать исключение

        // и оно там действительно возникнет — мы же передали null

        try {

            String answer = generator.helloMessage(null);

            System.out.println(«Answer 1:» + answer);

        } catch (SimpleException ex) {

            // Здесь мы можем обработать объект-исключение,

            // получить некоторую информаицию

            System.out.println(«Error code:» + ex.getErrorCode());

            System.out.println(«Error message:» + ex.getMessage());

        }

        // Данный блок будет обрабатывать исключение

        // но его не будет — мы передали корректный параметр

        try {

            String answer = generator.helloMessage(«Yoda»);

            System.out.println(«Answer 2:» + answer);

        } catch (SimpleException ex) {

            // Здесь мы можем обработать объект-исключение,

            // получить некоторую информаицию

            System.out.println(«Error:» + ex.getMessage());

        }

    }

}

Полный текст проекта можно скачать тут — SimpleException. При запуске можно увидеть, что первый кусочек try … catch выкинет исключение и мы увидим текст об ошибках

Error code:10
Error message:Message is null

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

Answer 2:Hello, Yoda

Обратите внимание, что в первом блоке try … catch строка «Answer 1:» не выводится. Т.е. поведение при генерации исключения следующее: сразу после создания исключения все остальные строки внутри блока уже не выполняются и вы сразу перемещаетесь в блок catch. Думаю, что это достаточно очевидно.
Итак, мы рассмотрели простой вариант использования исключений, но это конечно же не все. Так что продолжим.

Блок finally

Как мы видели чуть выше, если во время исполнения внутри блока try … catch случается исключение, то все оставшиеся строки НЕ ВЫПОЛНЯЮТСЯ. Но это не всегда полностью соответствует нашим желаниям. Существует немалое количество ситуаций, когда, чтобы не случилось, какие-то строки кода должны выполняться вне зависимости от того, каков результат. Именно для этого существует секция finally.
Например, вы открыли файл на запись и долгое время что-то туда записывали. Но наступил момент, когда какие-то данные по определенным причинам туда не попали и запиcь надо прекращать. Но даже в этой ситуации файл нужно сохранить (закрыть). Или вы открыли сетевое соединение, которое в любом случае надо закрыть. Наконец вам просто надо всегда обнулить какой-то счетчик в конце. Давайте посмотрим структуру с finally

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

package edu.javacourse.exception;

public class Starter

{

    public static void main(String[] args)

    {

        // создаем наш класс для генерации исключений

        Generator generator = new Generator();

        // Данный блок будет обрабатывать исключение

        // и оно там действительно возникнет — мы же передали null

        try {

            String answer = generator.helloMessage(null);

            System.out.println(«Answer 1:» + answer);

        } catch (SimpleException ex) {

            // Здесь мы можем обработать объект-исключение,

            // получить некоторую информаицию

            System.out.println(«Error code:» + ex.getErrorCode());

            System.out.println(«Error message:» + ex.getMessage());

        } finally {

            // Этот блок будет вызываться всегда, независимо от результата

            System.out.println(«Этот блок вызываетя всегда»);

        }

        // Данный блок будет обрабатывать исключение

        // но его не будет — мы передали корректный параметр

        try {

            String answer = generator.helloMessage(«Yoda»);

            System.out.println(«Answer 2:» + answer);

        } catch (SimpleException ex) {

            // Здесь мы можем обработать объект-исключение,

            // получить некоторую информаицию

            System.out.println(«Error:» + ex.getMessage());

        } finally {

            // Этот блок будет вызываться всегда, независиом от результата

            System.out.println(«Этот блок вызываетя всегда»);

        }

    }

}

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

Множество исключений

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

try {

  ....

} catch(Exception1 ex) {

  // Обработка исключения класса Exception1

} catch(Exception2 ex) {

  // Обработка исключения класса Exception2

} catch(Exception3 ex) {

  // Обработка исключения класса Exception3

}

Рассмотрим несложный пример — класс Generator в методе helloMessage порождает два исключения, а класс Starter будет ловить эти два исключения.

package edu.javacourse.exception;

public class Generator

{

    public String helloMessage(String name) throws FirstException, SecondException

    {

        if («FIRST».equals(name)) {

            throw new FirstException(«FirstException occured»);

        }

        if(«SECOND».equals(name)) {

            throw new SecondException(«SecondException occured»);

        }

        return «Hello, « + name;

    }

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

package edu.javacourse.exception;

public class Starter

{

    public static void main(String[] args)

    {

        Generator generator = new Generator();

        try {

            String answer = generator.helloMessage(«FIRST»);

            //String answer = generator.helloMessage(«SECOND»);

            //String answer = generator.helloMessage(«OTHER»);

            System.out.println(«Answer 1:» + answer);

        } catch (FirstException ex) {

            System.out.println(«Error message:» + ex.getMessage());

        } catch (SecondException ex) {

            System.out.println(«Error message:» + ex.getMessage());

        }

    }

}

Ниже код для двух классов исключений.

package edu.javacourse.exception;

public class FirstException extends Exception

{

    public FirstException(String message)

    {

        super(message);

    }

}

package edu.javacourse.exception;

public class SecondException extends Exception

{

    public SecondException(String message) {

        super(message);

    }

}

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

catch (FirstException | SecondException ex) {
System.out.println(«Error message:» + ex.getMessage());
}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

package edu.javacourse.exception;

public class Starter

{

    public static void main(String[] args)

    {

        Generator generator = new Generator();

        try {

            String answer = generator.helloMessage(«FIRST»);

            //String answer = generator.helloMessage(«SECOND»);

            //String answer = generator.helloMessage(«OTHER»);

            System.out.println(«Answer 1:» + answer);

        } catch (FirstException | SecondException ex) {

            System.out.println(«Error message:» + ex.getMessage());

        }

    }

}

Но нередко все исключения можно обработать в одной ветке catch. В этом случае можно использовать полиморфизм и наследование исключений. Как и любой класс, исключение тоже наследуется. При построении секции catch это можно использовать. Принцип следующий — поиск подходящего исключения начинается с первого catch. Вы понимаете, что все наследники класса Exception подходят под этот класс. Значит если у нас самый первый catch будет ловить класс Exception, то туда будут попадать практически все исключения.
Давайте рассмотрим вот такой пример обработки:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

package edu.javacourse.exception;

public class Starter

{

    public static void main(String[] args)

    {

        Generator generator = new Generator();

        try {

            String answer = generator.helloMessage(«FIRST»);

            //String answer = generator.helloMessage(«SECOND»);

            //String answer = generator.helloMessage(«OTHER»);

            System.out.println(«Answer 1:» + answer);

        } catch (Exception ex) {

            System.out.println(«Error message:» + ex.getMessage());

        } catch (FirstException ex) {

            System.out.println(«Error message:» + ex.getMessage());

        } catch (SecondException ex) {

            System.out.println(«Error message:» + ex.getMessage());

        }

    }

}

Этот пример не будет компилироваться, т.к. второй и третий catch не будет достигнут никогда — мы все время попадаем в первую ветку catch. Но если мы переместим обработку Exception в конец, то этот код будет корректным, т.к. сначала мы проверяем более точные классы исключений. Код ниже на первый взгляд, не сильно отличается. но его можно скомпилировать.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

package edu.javacourse.exception;

public class Starter

{

    public static void main(String[] args)

    {

        Generator generator = new Generator();

        try {

            String answer = generator.helloMessage(«FIRST»);

            //String answer = generator.helloMessage(«SECOND»);

            //String answer = generator.helloMessage(«OTHER»);

            System.out.println(«Answer 1:» + answer);

        } catch (FirstException ex) {

            System.out.println(«Error message:» + ex.getMessage());

        } catch (SecondException ex) {

            System.out.println(«Error message:» + ex.getMessage());

        } catch (Exception ex) {

            System.out.println(«Error message:» + ex.getMessage());

        }

    }

}

Этим приемом нередко пользуется — учитесь строить, читать и распознавать такие конструкции.

Класс RuntimeException

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

Пожелания

Самое главное пожелание — не оставляйте блоки catch без информации. Очень неплохим подспорьем будет вызов метода printStackTrace. Очень важно понимать. что случилось, почему. И если блок catch не будет информативным, то выяснение причин будет крайне сложным занятием.
Ужасным кодом будет нечто вроде этого:

try {

  ...

} catch(Exception ex) {

  System.out.println(«Произошло исключение»);

}

Что здесь можно понять в случае возникновения исключения ? НИЧЕГО ? Уважайте свой труд — выводите подробную информацию.

Что дальше ?

Рассмотрев исключения, мы завершили знакомство с конструкциями языка Java. Я говорю именно о конструкциях — у нас еще впереди долгий путь изучения. Но если выражаться образно, то алфавит освоили. Мы еще будем встречать некоторые конструкции и слова, но подавляющая часть уже пройдена. Впереди нас ждет изучение уже готовых классов, которые предназначены для решения различных задач.
Все конструкции служат главному — позволить вам создавать описания классов, создавать объекты и управлять последовательностью вызовов для решения своей задачи. Можно по-разному оценивать удобство, но раз это так сделано разработчиками Java — надо уметь этим пользоваться.
И еще раз выскажу пожелание — учитесь читать код, учитесь понимать программу, просто просматривая что она делает, шаг за шагом.

И теперь нас ждет следующая статья: Решения на основе классов.

Классы Java для обработки исключительных ситуаций из пакета java.lang. Методы класса Throwable. Примеры


Содержание

  • 1. Типы исключений, которые поддерживаются системой обработки исключений Java
  • 2. Классификация исключений по признаку наличия в операторе throws. Непроверяемые исключения. Проверяемые исключения
  • 3. Перечень подклассов непроверяемых исключений из пакета java.lang
  • 4. Проверяемые исключения из пакета java.lang
  • 5. Какое назначение класса Throwable? Методы класса Throwable
  • 6. Пример использования некоторых методов класса Throwable. Разработка собственного класса исключения
  • Связанные темы

Поиск на других ресурсах:

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

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

Из класса Throwable унаследованы два основных класса:

  • Exception – предназначен для задания исключительных условий, которые перехватываются программой. Если нужно объявить собственный класс (тип) исключений, то этот класс может быть унаследован от класса Exception;
  • Error – класс, который предназначен для описания исключений (ошибок) возникающих в самой среде Java. Такие исключения не оговариваются во время нормального выполнения прикладной программы. Примеры системных ошибок: недостаточно памяти, переполнение стека.

Схема верхнего уровня иерархии классов Java приведена на рисунке

Java. Вершина иерархии классов исключений

Рисунок. Вершина иерархии классов исключений Java

 

2. Классификация исключений по признаку наличия в операторе throws. Непроверяемые исключения. Проверяемые исключения

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

Более подробно о работе оператора throws описывается в теме:

  • Операторы throw, throws. Примеры

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

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

 

3. Перечень подклассов непроверяемых исключений из пакета java.lang

Среди всего разнообразия классов и интерфейсов пакет java.lang содержит мощный арсенал классов для обработки исключений. Эти классы и интерфейсы составляют основу всех программ на Java. Пакет java.lang автоматически импортируется во все программы.

Ниже приведен перечень подклассов непроверяемых исключений производными от класса RuntimeException и которые определены в пакете java.lang:

  • ArithmeticException – арифметическая ошибка (например, деление на ноль);
  • ArrayIndexOutOfBoundsException – индекс за пределами массива;
  • ArrayStoreException – присваивание элементу массива объекта несовместимого типа;
  • ClassCastException – неправильное приведение типов;
  • EnumConstantNotPresent – попытка воспользоваться неопределенным значением перечисления;
  • IllegalArgumentException – недопустимый аргумент при вызове метода;
  • IllegalMonitorStateException – недопустимая контрольная операция;
  • IllegalStateException – неверное состояние среды или приложения;
  • IllegalThreadStateException – несовместимость запрашиваемой операции с текущим состоянием потока выполнения;
  • IndexOutOfBoundsException – выход индекса некоторого типа за допустимые границы;
  • NegativeArraySizeException – создание массива отрицательного размера;
  • NullPointerException – неправильное использование пустой ссылки;
  • NumberFormatException – неправильное преобразование символьной строки в числовой формат;
  • SecurityException – попытка нарушения безопасности;
  • StringIndexOutOfBounds – попытка доступа по индексу за пределами символьной строки;
  • TypeNotPresentException – тип не найден;
  • UnsupportedOperationException – найдена неподдерживаемая операция.

 

4. Проверяемые исключения из пакета java.lang

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

В языке Java в пакете java.lang реализован ряд проверяемых исключений. Ниже приведен их перечень:

  • ClassNotFoundException – класс не найден;
  • CloneNotSupportedException – попытка клонировать объект из класса, который не реализует интерфейс Cloneable;
  • IllegalAccessException – запрещен доступ к классу;
  • InstantiationException – попытка создать объект абстрактного класса или интерфейса;
  • InterruptedException – один поток выполнения прерван другим потоком;
  • NoSuchFieldException – запрашиваемое поле не существует;
  • NoSuchMethodException – запрашиваемый метод не существует;
  • ReflectiveOperationException – суперкласс исключений, связанных с рефлексией.

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



 

5. Какое назначение класса Throwable? Методы класса Throwable

Класс Throwable есть базовым для всех стандартных классов исключений Java. Этот класс предоставляет ряд методов, которые можно использовать или переопределять в собственных классах обработки исключений. Эти классы должны быть унаследованы от класса Exception, который унаследован от класса Throwable (см. рисунок). Класс Exception не содержит методов.
Ниже приведен перечень методов класса Throwable.

1. Метод

final void addSuppressed(Throwable исключение)

добавляет заданное исключение в список подавляемых исключений. Этот список связывается с вызывающим (данным) исключением. Метод используется для применения в операторе try с ресурсами.

2. Метод

Throwable fillInStackTrace()

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

3. Метод

Throwable getCause()

возвращает исключение, лежащее в основе текущего исключения. Метод возвращает null в случае, если такое исключение отсутствует. Этот метод используется при создании цепочек исключений – он вызывает исключение, вызывающее текущее исключение.

4. Метод

String getLocalizedMessage()

возвращает локализованное описание исключения.

5. Метод

String getMessage()

возвращает описание исключения.

6. Метод

StackTraceElement[] getStackTrace()

возвращает массив, содержащий поэлементную трассировку стека в виде объектов класса StackTraceElement.

7. Метод

final Throwable[] getSuppressed()

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

8. Метод

Throwable initCause(Throwable причина_исключения)

связывает входной параметр причина_исключения с вызывающим исключением, указывая его как причину этого вызывающего исключения. Возвращает ссылку на исключение. Метод используется при создании цепочек исключений.

9. Метод

printStackTrace();

выводит трассировку стека.

10. Метод printStackTrace() имеет еще две перегруженных реализации

void printStackTrace(PrintStream поток_вывода)
void printStackTrace(PrintWriter поток_вывода)

Метод направляет трассировку стека в заданный поток_вывода.

11. Метод

void setStackTrace(StackTraceElement элементы[])

устанавливает трассировку стека для заданных элементов.

12. Метод

String toString()

возвращает объект типа String содержащий описание исключения. Этот метод можно вызвать из метода println() при выводе объекта типа Throwable.

 



6. Пример использования некоторых методов класса Throwable. Разработка собственного класса исключения

В примере демонстрируется использование некоторых методов класса Throwable:

  • getLocalizedMessage();
  • getMessage();
  • toString();
  • getStackTrace();
  • fillInStackTrace().

Объявляется класс MyException, унаследованный от класса Exception. В иерархии классов исключений Java класс Exception унаследован от класса Throwable. Поэтому, класс MyException может использовать и переопределять методы класса Throwable.

Текст программы следующий:

import java.util.Scanner;

// собственный класс исключения, унаследован от Exception
class MyException extends Exception
{
  // переопределенная функция getLocalizedMessage()
  public String getLocalizedMessage()
  {
    return "MyException.getLocalizedMessage()";
  }
}

// класс, содержащий функцию main()
public class Train04 {
  // функция main() тестирует работу класса MyException
  public static void main(String[] args) {
    // Ввести число x. Если число за пределами [0..100],
    // то сгенерировать исключение MyException
    int x;
    Scanner sc = new Scanner(System.in);
    System.out.print("x = ");
    x = sc.nextInt(); // ввести x

    try {
      // сгенерировать исключение (создать объект типа MyException),
      // если x за пределами [0..100]
      if ((x<0)||(x>100))
        throw new MyException();
      System.out.println("OK!");
    }
    catch(MyException e)
    {
      // обработка исключения типа MyException,
      // демонстрация некоторых методов класса Throwable
      System.out.println("Return from getLocalizedMessage(): " + e.getLocalizedMessage());
      System.out.println("Return from getMessage(): " + e.getMessage());
      System.out.println("Method printStackTrace(): ");
      e.printStackTrace();
      System.out.println("Method toString(): " + e.toString());
      System.out.println("------------------------");
      System.out.println("Method getStackTrace(). Stack trace: ");
      StackTraceElement[] stE;

      stE = e.getStackTrace(); // метод getStackTrace()

      for (int i=0;i<stE.length;i++)
        System.out.println(stE[i].toString());

      System.out.println("-------------------------");
      System.out.println("Method fillStackTrace(). Stack trace: ");

      Throwable tA = e.fillInStackTrace();
      StackTraceElement[] stE2 = tA.getStackTrace();

      for (int i=0; i<stE2.length; i++)
        System.out.println(stE[i].toString());
      System.out.println("-------------------------");
    }
  }
}

Объясним некоторые фрагменты кода.

С целью демонстрации в классе MyException переопределяется метод getLocalizedMessage(). При вызове этого метода выводится сообщение из этого переопределенного метода. По данному примеру можно переопределять другие методы класса Throwable.

В функции main() продемонстрировано использование методов класса Throwable. Вводится переменная x, которая проверяется на допустимые значения в пределах то 0 до 100. Если значение x меньше 0 или больше 100, то генерируется исключение типа MyException.

Результат работы программы

x = 200
Return from getLocalizedMessage(): MyException.getLocalizedMessage()
Return from getMessage(): null
Method printStackTrace():
Method toString(): MyException: MyException.getLocalizedMessage()
------------------------
Method getStackTrace(). Stack trace:
Train04.main(Train04.java:36)
-------------------------
Method fillStackTrace(). Stack trace:
Train04.main(Train04.java:36)
-------------------------
MyException: MyException.getLocalizedMessage()

 


Связанные темы

  • Исключения. Исключительная ситуация. Ключевые слова trycatchfinallyПримеры
  • Операторы throw, throws. Примеры
  • Класс Exception. Создание собственных классов исключений. Примеры

 


Исключение в 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.

Иерархия исключений, исключения в Java, Java, RuntimeException Class

По ссылке представлен перечень наиболее распространенных контролируемых (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.

  • Описание серий сериала мистер ошибка
  • Описание кино роковая ошибка
  • Оплатив счет необходимо письменно сообщить издательству номер платежного поручения ошибка
  • Описание ошибки ошибка сохранения сообщения 0x80004005
  • Описание проблемы внутренняя ошибка сервера http код 500