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

Введение

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

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

Генерация и Перехват Исключений

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

Если в методе происходит исключение, процесс создания объекта и передачи его среде выполнения называется «генерацией исключения». Обычный ход программы прерывается, и среда выполнения Java (JRE) пытается найти обработчик исключения. Обработчик исключения – это блок кода, который может обработать объект исключения.

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

Таким образом, если стек вызовов метода выглядит как A->B->C, и исключение возникает в методе C, то поиск подходящего обработчика будет двигаться от C->B->A.

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

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

Ключевые слова обработки исключений Java

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

  1. throw – Мы знаем, что при возникновении ошибки создается объект исключения, а затем Java runtime начинает его обработку. Иногда мы можем явным образом генерировать исключения в нашем коде. Например, в программе аутентификации пользователя мы должны генерировать исключения для клиентов, если пароль равен null. Ключевое слово throw используется для генерации исключений, которые будут обработаны runtime.
  2. throws – Когда мы генерируем исключение в методе и не обрабатываем его, мы должны использовать ключевое слово throws в сигнатуре метода, чтобы предупредить программу вызывающего о возможных исключениях, которые могут быть сгенерированы методом. Метод вызывающего может обработать эти исключения или передать их своему вызывающему методу, используя ключевое слово throws. Мы можем указать несколько исключений в блоке throws, и это также может использоваться с методом main().
  3. try-catch – Мы используем блок try-catch для обработки исключений в нашем коде. try – начало блока, а catch – в конце блока try для обработки исключений. Мы можем иметь несколько блоков catch с блоком try. Блок try-catch может быть также вложенным. Блок catch требует параметра, который должен быть типа Exception.
  4. наконец – блок finally является необязательным и может использоваться только с блоком try-catch. Поскольку исключение прерывает процесс выполнения, у нас может быть открыто несколько ресурсов, которые не будут закрыты, поэтому мы можем использовать блок finally. Блок finally всегда выполняется, независимо от того, возникло исключение или нет.

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

ExceptionHandling.java
package com.journaldev.exceptions;

import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandling {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try {
			testException(-5);
			testException(-10);
		} catch(FileNotFoundException e) {
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("Releasing resources");
		}
		testException(15);
	}

	public static void testException(int i) throws FileNotFoundException, IOException {
		if (i < 0) {
			FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
			throw myException;
		} else if (i > 10) {
			throw new IOException("Only supported for index 0 to 10");
		}
	}
}
  • Метод testException() генерирует исключения с использованием ключевого слова throw. Сигнатура метода использует ключевое слово throws, чтобы уведомить вызывающего о типах исключений, которые он может сгенерировать.
  • В методе main() я обрабатываю исключения с использованием блока try-catch в методе main(). Когда я не обрабатываю его, я передаю его на выполнение с помощью клавиши throws в методе main().
  • Метод testException(-10) никогда не выполняется из-за исключения, а затем выполняется блок finally.

printStackTrace() – один из полезных методов класса Exception в целях отладки.

Этот код выведет следующее:

Output
java.io.FileNotFoundException: Negative Integer -5 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10) Releasing resources Exception in thread "main" java.io.IOException: Only supported for index 0 to 10 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)

Несколько важных моментов:

  • Нельзя иметь блок catch или finally без оператора try.
  • A try statement should have either catch block or finally block, it can have both blocks.
  • Мы не можем писать код между блоками try-catch-finally.
  • Мы можем иметь несколько блоков catch с одним оператором try.
  • Блоки try-catch могут быть вложены, аналогично операторам if-else.
  • Мы можем иметь только один блок finally с оператором try-catch.

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

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

  1. Ошибки: Error – исключительные ситуации, выходящие за рамки приложения, и их невозможно предвидеть и восстановить. Например, сбой оборудования, сбой виртуальной машины Java (JVM) или ошибка “недостаток памяти”. Поэтому у нас есть отдельная иерархия Error, и мы не должны пытаться обрабатывать эти ситуации. Некоторые из распространенных Error – это OutOfMemoryError и StackOverflowError.
  2. Проверяемые исключения: Проверяемые исключения – это исключительные ситуации, которые мы можем предвидеть в программе и попытаться восстановиться от них. Например, FileNotFoundException. Мы должны перехватывать это исключение и предоставлять полезное сообщение пользователю, а также правильно регистрировать его для целей отладки. Exception является родительским классом всех проверяемых исключений. Если мы бросаем проверяемое исключение, мы должны его перехватить в том же методе или передать вызывающему с использованием ключевого слова throws.
  3. Исключение времени выполнения: Исключения времени выполнения вызваны плохим программированием. Например, попытка извлечения элемента из массива. Мы должны сначала проверить длину массива, прежде чем пытаться извлечь элемент, в противном случае это может вызвать ArrayIndexOutOfBoundException во время выполнения. RuntimeException является родительским классом всех исключений времени выполнения. Если мы выбрасываем исключение времени выполнения в методе, не обязательно указывать их в сигнатуре метода с помощью ключевого слова throws. Исключения времени выполнения могут быть предотвращены с помощью лучшего программирования.

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

Java Exception и все его подклассы не предоставляют конкретных методов, и все методы определены в базовом классе – Throwable. Классы Exception созданы для указания различных видов сценариев Exception, чтобы мы могли легко идентифицировать корневую причину и обрабатывать Exception в соответствии с его типом. Класс Throwable реализует интерфейс Serializable для взаимодействия.

Некоторые полезные методы класса Throwable включают:

  1. public String getMessage() – Этот метод возвращает строку сообщения Throwable, и сообщение можно указать при создании исключения через его конструктор.
  2. public String getLocalizedMessage() – Этот метод предоставляется для того, чтобы подклассы могли переопределить его и предоставить локализованное сообщение вызывающей программе. Реализация этого метода в классе Throwable использует метод getMessage() для возврата сообщения об исключении.
  3. public synchronized Throwable getCause() – Этот метод возвращает причину исключения или null, если причина неизвестна.
  4. public String toString() – Этот метод возвращает информацию об Throwable в формате String, возвращаемая строка содержит имя класса Throwable и локализованное сообщение.
  5. public void printStackTrace() – Этот метод выводит информацию о стеке вызовов в стандартный поток ошибок, этот метод перегружен, и мы можем передать PrintStream или PrintWriter в качестве аргумента для записи информации о стеке вызовов в файл или поток.

Управление ресурсами и улучшения в блоке catch в Java 7

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

catch (IOException | SQLException ex) {
    logger.error(ex);
    throw new MyException(ex.getMessage());
}

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

Большую часть времени мы используем блок finally только для закрытия ресурсов. Иногда мы забываем их закрыть и получаем исключения времени выполнения, когда ресурсы исчерпываются. Эти исключения трудно отлаживать, и нам может потребоваться просмотреть каждое место, где мы используем этот ресурс, чтобы убедиться, что мы его закрываем. В Java 7 одним из улучшений было использование try-with-resources, где мы можем создать ресурс в самом операторе try и использовать его внутри блока try-catch. Когда выполнение выходит из блока try-catch, среда выполнения автоматически закрывает эти ресурсы. Вот пример блока try-catch с этим улучшением:

try (MyResource mr = new MyResource()) {
	System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
	e.printStackTrace();
}

A Custom Exception Class Example

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

Сначала создайте MyException:

MyException.java
package com.journaldev.exceptions;

public class MyException extends Exception {

	private static final long serialVersionUID = 4664456874499611218L;

	private String errorCode = "Unknown_Exception";

	public MyException(String message, String errorCode) {
		super(message);
		this.errorCode=errorCode;
	}

	public String getErrorCode() {
		return this.errorCode;
	}
}

Затем создайте CustomExceptionExample:

CustomExceptionExample.java
package com.journaldev.exceptions;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class CustomExceptionExample {

	public static void main(String[] args) throws MyException {
		try {
			processFile("file.txt");
		} catch (MyException e) {
			processErrorCodes(e);
		}
	}

	private static void processErrorCodes(MyException e) throws MyException {
		switch (e.getErrorCode()) {
			case "BAD_FILE_TYPE":
				System.out.println("Bad File Type, notify user");
				throw e;
			case "FILE_NOT_FOUND_EXCEPTION":
				System.out.println("File Not Found, notify user");
				throw e;
			case "FILE_CLOSE_EXCEPTION":
				System.out.println("File Close failed, just log it.");
				break;
			default:
				System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
				e.printStackTrace();
		}
	}

	private static void processFile(String file) throws MyException {
		InputStream fis = null;

		try {
			fis = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
		} finally {
			try {
				if (fis != null) fis.close();
			} catch (IOException e) {
				throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
			}
		}
	}
}

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

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

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

Лучшие практики обработки исключений в Java

  • Используйте конкретные исключения – Базовые классы иерархии Exception не предоставляют полезной информации, поэтому в Java существует много классов исключений, таких как IOException с дополнительными подклассами, такими как FileNotFoundException, EOFException и т. д. Мы всегда должны throw и catch конкретные классы исключений, чтобы вызывающий знал причину исключения легко и обрабатывал их. Это облегчает отладку и помогает клиентским приложениям обрабатывать исключения должным образом.
  • Бросайте рано или завершайте быстро – Мы должны стараться бросать исключения как можно раньше. Рассмотрим вышеуказанный метод processFile(): если мы передадим в этот метод аргумент null, мы получим следующее исключение:
Output
Exception in thread "main" java.lang.NullPointerException at java.io.FileInputStream.<init>(FileInputStream.java:134) at java.io.FileInputStream.<init>(FileInputStream.java:97) at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)

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

private static void processFile(String file) throws MyException {
	if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");

	// ... дальнейшая обработка
}

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

Output
com.journaldev.exceptions.MyException: File name can't be null at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
  • Catch Late – Поскольку Java требует либо обработать проверяемое исключение, либо объявить его в сигнатуре метода, иногда разработчики склонны перехватывать исключение и записывать ошибку в лог. Но эта практика вредна, потому что вызывающая программа не получает уведомления об исключении. Мы должны перехватывать исключения только тогда, когда можем обработать их должным образом. Например, в указанном выше методе я использую оператор throw, чтобы вернуть исключения вызывающему методу для их обработки. Тот же самый метод может использоваться другими приложениями, которые могут захотеть обработать исключение по-другому. При реализации любой функции мы всегда должны использовать оператор throw, чтобы вернуть исключения вызывающему и позволить им решить, как с ними обращаться.
  • Закрытие ресурсов – Поскольку исключения приостанавливают выполнение программы, мы должны закрывать все ресурсы в блоке finally или использовать улучшение Java 7 try-with-resources, чтобы позволить Java автоматически закрыть их.
  • Регистрация исключений – Мы всегда должны регистрировать сообщения об исключениях и при выбрасывании исключений предоставлять четкое сообщение, чтобы вызывающая сторона легко понимала, почему произошло исключение. Мы всегда должны избегать пустого блока catch, который просто обрабатывает исключение, не предоставляя каких-либо значимых подробностей об исключении для отладки.
  • Одиночный блок catch для нескольких исключений – В большинстве случаев мы регистрируем подробности об исключениях и предоставляем сообщение пользователю, в этом случае мы должны использовать функцию Java 7 для обработки нескольких исключений в одном блоке catch. Этот подход уменьшит размер нашего кода и будет выглядеть более чисто.
  • Использование пользовательских исключений – Всегда лучше определить стратегию обработки исключений на этапе проектирования, а вместо выбрасывания и обработки нескольких исключений мы можем создать пользовательское исключение с кодом ошибки, и вызывающая программа может обрабатывать эти коды ошибок. Также хорошей идеей является создание утилитарного метода для обработки различных кодов ошибок и их использования.
  • Соглашения об именах и упаковка – При создании пользовательского исключения убедитесь, что оно заканчивается на Exception, чтобы уже из самого названия было понятно, что это класс исключения. Также убедитесь, что они упакованы, как это делается в Java Development Kit (JDK). Например, IOException является базовым исключением для всех операций ввода-вывода.
  • Используйте Исключения Осмотрительно – Исключения затратны, и иногда их вовсе не нужно выбрасывать, и мы можем вернуть логическую переменную в вызывающую программу, чтобы указать, успешно ли выполнена операция или нет. Это полезно там, где операция необязательна, и вы не хотите, чтобы ваша программа застревала из-за ошибки. Например, при обновлении котировок на бирже в базе данных из веб-сервиса третьей стороны мы можем избежать выбрасывания исключений, если соединение не установлено.
  • Документируйте Брошенные Исключения – Используйте Javadoc @throws, чтобы четко указать исключения, выбрасываемые методом. Это очень полезно, когда вы предоставляете интерфейс для использования другими приложениями.

Вывод

В этой статье вы узнали о обработке исключений в Java. Вы узнали о throw и throws. Вы также узнали о блоках trytry-with-resources), catch и finally.

Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java