Java exception код ошибки

#База знаний

  • 24 фев 2021

  • 13

Разбираемся, что такое исключения, зачем они нужны и как с ними работать.

 vlada_maestro / shutterstock

Мария Помазкина

Хлебом не корми — дай кому-нибудь про Java рассказать.

Из этой статьи вы узнаете:

  • что такое исключения (Exceptions);
  • как они возникают и чем отличаются от ошибок (Errors);
  • зачем нужна конструкция try-catch;
  • как разобраться в полученном исключении
  • и как вызвать исключение самому.

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

За примером далеко ходить не надо: сделаем то, что нам запрещали ещё в школе, — поделим на ноль.

public static void main(String[] args) {
    hereWillBeTrouble(42, 0);
}

public static void hereWillBeTrouble(int a, int b) {
    int oops = a / b;
    System.out.println(oops);
}

А получим вот что:

Это и есть исключение.

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

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

У всех классов исключений есть общий класс-предок Throwable, от него наследуются классы Error и Exception, базовые для всех прочих.

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

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

Error is the superclass of all the exceptions from which ordinary programs are not ordinarily expected to recover.

Что в переводе означает: ошибки (Error) — это такие исключительные ситуации, в которых восстанавливать работу программы не предполагается.

То есть это проблемы, которые нельзя (недопустимо) исправлять на ходу. Всё, что нам остаётся, — извиниться перед пользователем и впредь писать программы, где возникнет меньше подобных ситуаций. Например, не допускать такой глубокой рекурсии, как в коде ниже:

static void notGood() {
    System.out.println("Только не снова!");
    notGood();
}

При работе этого метода у нас возникнет ошибка: Exception in thread «main» java.lang.StackOverflowError — стек вызовов переполнился, так как мы не указали условие выхода из рекурсии.

А теперь об Exception. Эти исключительные ситуации возникают, если разработчик допустил невыполнимую операцию, не предусмотрел особые случаи в бизнес-логике программы (или сообщает о них с помощью исключений).

1. Невыполнимая операция

Мир не рухнул, как в случае с Error, просто Java не знает, что делать дальше. Как раз из этого разряда деление на ноль в начале статьи: и правда, какое значение тогда присвоить переменной oops?

Убедитесь сами, что исключение класса ArithmeticException наследуется как раз от Exception.

Стоит запомнить. В IntelliJ IDEA, чтобы увидеть положение класса в иерархии, выберите его и нажмите Ctrl + H (или на пункт Type Hierarchy в меню Navigate).

Другая частая ситуация — обращение к несуществующему элементу массива. Например, у нас в нём десять элементов, а мы пытаемся обратиться к одиннадцатому.

2. Особый случай в бизнес-логике программы

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

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

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

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

Простейший вариант — ничего; возникает исключение — программа просто прекращает работать.

Чтобы убедиться в этом, выполним код:

public static void main(String[] args) {
    hereWillBeTrouble(42, 0);
}

public static void hereWillBeTrouble(int a, int b) {
    System.out.println("Всё, что было до...");
    int oops = a / b;
    System.out.println(oops);
    System.out.println("Всё, что будет после...");
}

Так и есть: до деления на ноль код выполнялся, а после — нет.

Это интересно: когда возникает исключение, программисты выдают что-то вроде «код [вы]бросил исключение» или «код кинул исключение». А глагол таков потому, что все исключения — наследники класса Throwable, что значит «бросаемый» / «который можно бросить».

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

Для этого нужно заключить кусок кода, который может вызвать исключение, в конструкцию try-catch.

Как это работает: если в блоке try возникает исключение, которое указано в блоке catch, то исполнение блока try прервётся и выполнится код из блока catch.

Например:

public static void main(String[] args) {
    hereWillBeTrouble();
}

private static void hereWillBeTrouble(int a, int b) {
    int oops;
    try {
        System.out.println("Всё, что было до...");
        oops = a / b;
        System.out.println(oops);
        System.out.println("Всё, что будет после...");
    } catch (ArithmeticException e) {
        System.out.println("Говорили же не делить на ноль!");
        oops = 0;
    }
    System.out.println("Метод отработал");
}

Разберём этот код.

Если блок try кинет исключение ArithmeticException, то управление перехватит блок catch, который выведет строку «Говорили же не делить на ноль!», а значение oops станет равным 0.

После этого программа продолжит работать как ни в чём не бывало: выполнится код после блока try-catch, который сообщит: «Метод отработал».

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

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

Вернёмся к первой картинке. Посмотрим, что нам сказала Java, когда произошло исключение:

Начинаем разбирать сверху вниз:

— это указание на поток, в котором произошло исключение. В нашей простой однопоточной программе это поток main.

— какое исключение брошено. У нас это ArithmeticException. А java.lang.ArithmeticException — полное название класса вместе с пакетом, в котором он размещается.

— весточка, которую принесло исключение. Дело в том, что одно и то же исключение нередко возникает по разным причинам. И тут мы видим стандартное пояснение «/ by zero» — из-за деления на ноль.

— это самое интересное: стектрейс.

Стектрейс (Stack trace) — это упорядоченный список методов, сквозь которые исключение пронырнуло.

У нас оно возникло в методе hereWillBeTrouble на 8-й строке в классе Main (номер строки и класс указаны в скобках синим). А этот метод, в свою очередь, вызван методом main на 3-й строке класса Main.

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

Советую закреплять теорию на практике. Поэтому вернитесь в блок про Error и вызовите метод notGood — увидите любопытный стектрейс.

Всё это время мы имели дело с исключением, которое бросает Java-машина — при делении на ноль. Но как вызвать исключение самим?

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

public static void main(String[] args) {
    hereWillBeTrouble(42, 0);
}

private static void hereWillBeTrouble(int a, int b) {
    if (b == 0) {
        throw new ArithmeticException("ты опять делишь на ноль?");
    }
    int oops = a / b;
    System.out.println(oops);
}

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

А получим мы то же самое, что и в самом первом примере, только вместо стандартной фразы «/by zero» теперь выдаётся наш вопрос-пояснение «ты опять делишь на ноль?»:

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

Научитесь: Профессия Java-разработчик PRO
Узнать больше

In real life there are two audiences:

  • the log handler with a configurable log level (ERROR) and logging (in English) with much info;
  • the end user with an localized translated message, also with parameters.

The first property is that you probably want a message as format String with Object... parameters. Probably should use MessageFormat.

Sensible would be to support typed parameters.

/** Type-checkable Message Definition. */
public record MessageDef(String format, Class<?>... parameterTypes) {
    public void checkFormat() {
        ... check actual parameters with parameterTypes.length
    }
    public void checkUsage(Object[] args) {
        ... check parameter types
    }
}

One could make an enum for the error categories. However enums are more suitable for closed domains with a fixed set of values. Extending values in future means that you have created a needless common bottleneck for source version control and so on.
An error is more like an open domain. However if you number them with error codes, an enum gives a nice overview.

The only advantage of error codes is the internationalisation. An Hungarian error message can be easily retrieved.

Then, if you rethrow check exceptions as RuntimeException, like IllegalArgumentException or you own custom ones, you might not want parallel classes: run-time exceptions and categories.

All-in-all I would advise an enum:

public enum MessageType {
    INVALID_TEMPLATE(400, Level.ERROR,
        new MessageDef("You have got the error in {0}.", String.class)),
    ...
    REQUEST_REJECTED(200, Level.INFO,
        new MessageDef("Done."));

    public final int code;
    public final Level level;
    public final MessageDef def;

    MessageType(int code, Level level, MessageDef def) {
        this.code = code;
        this.level = level;
        this.def = def;
    }
}

One small remark: such little discussion points in the beginning of a project sometimes might be better postponed to a fast refactoring after having written sufficient code. Here an enum might not fit, you might have much re-throwing of exceptions. A premature decision is not needed. And might hamper fast productivity.
Especially as you probably need not mark the code places, you most likely call the same show-error dialog.

% 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 пустым. Программа должна реагировать на исключения и должна информировать пользователя и разработчика о том, что что-то пошло не так. Безусловно, исключение это не повод тут же закрывать приложение, а попытаться повторить то действие, которое привело к исключению (например, повторно указать название файла, попытаться открыть базу данных через время и т.д.). В любом случае, когда приложение в ответ на ошибку никак не реагирует – не выдает сообщение, но и не делает того, чего от нее ожидали – это самый плохой вариант.

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

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

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

Что и как происходит, когда появляется ошибка

Когда в методе происходит исключение, то процесс создания объекта-исключения и передачи его в Runtime Environment называется «бросать исключение».

После создания исключения, Java Runtime Environment пытается найти обработчик исключения.

Обработчик исключения — блок кода, который может обрабатывать объект-исключение.

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

Пример

У нас есть 3 метода, каждый из которых вызывает друг-друга: А -> В -> С (А вызывает В, а В вызывает С). Если исключение появляется в методе C, то поиск соответствующего обработчика будет происходить в обратном порядке: С -> В -> А (сначала там, где было исключение — в С, если там нет обработчика, то идем в метод В — если тут тоже нет, то идем в А).

Если соответствующий обработчик исключений будет найден, то объект-исключение передаётся обработчику.

Обработать исключение — значит «поймать исключение».

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

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

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

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

  • Бросить исключение (throw) — ключевое слово, которое используется для того, чтобы бросить исключение во время выполнения. Мы знаем, что Java Runtime начинает поиск обработчика исключений как только оно будет брошено, но часто нам самим нужно генерировать исключение в нашем коде, например, в программе авторизации, если какое-то поле null. Именно для таких случаем и существует возможность бросить исключение.
  • throws — когда мы бросаем исключение в методе и не обрабатываем его, то мы должны использовать ключевое слово throws в сигнатуре метода для того, чтобы пробросить исключение для обработки в другом методе. Вызывающий метод может обработать это исключение или пробросить его еще дальше с помощью throws в сигнатуре метода. Следует отметить, что пробрасывать можно сразу несколько исключений.
  • Блок try-catch используется для обработки исключений в коде. Слово try — это начало блока обработки, catch — конец блока для обработки исключений. Мы можем использовать сразу несколько блоков catch при одном try. catch в качестве параметра принимает тип исключения для обработки.
  • finally — необязательная завершающая конструкция блока try-catch. Как только исключение остановило процесс исполнения программы, в finally мы можем безопасно освободить какие-то открытые ресурсы. Следует отметить, что finally блок выполняется всегда — не смотря на появление исключительной ситуации.

Давайте посмотрим простую программу обработки исключений в 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

31

32

33

34

package ua.com.prologistic;

import java.io.FileNotFoundException;

import java.io.IOException;

public class ExceptionHandling {

    // в методе main() пробрасывается сразу несколько исключений

    public static void main(String[] args) throws FileNotFoundException, IOException {

        // в блоке try-catch перехватываются сразу несколько исключений вызовом дополнительного catch(…)

        try{

            testException(5);

            testException(10);

        }catch(FileNotFoundException e){

            e.printStackTrace();

        }catch(IOException e){

            e.printStackTrace();

        }finally{

            System.out.println(«Необязательный блок, но раз уже написан, то выполнятся будет не зависимо от того было исключение или нет»);          

        }

        testException(15);

    }

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

    public static void testException(int i) throws FileNotFoundException, IOException{

        if(i < 0){

            FileNotFoundException myException = new FileNotFoundException(«число меньше 0: « + i);

            throw myException;

        }else if(i > 10){

            throw new IOException(«Число должно быть в пределах от 0 до 10»);

        }

    }

}

А в консоле эта программа напишет такое:

java.io.FileNotFoundException: число меньше 0: 5

    at ua.com.prologistic.ExceptionHandling.testException(ExceptionHandling.java:24)

    at ua.com.prologistic.ExceptionHandling.main(ExceptionHandling.java:10)

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

Exception in thread «main» java.io.IOException: Число должно быть в пределах от 0 до 10

    at ua.com.prologistic.ExceptionHandling.testException(ExceptionHandling.java:27)

    at ua.com.prologistic.ExceptionHandling.main(ExceptionHandling.java:19)

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

Важные моменты в обработке исключений:

  • Нельзя использовать блоки catch или finally без блока try.
  • Блок try также может быть использован только с catch блоком, или только с finally блоком, или с тем и другим блоком.
  • Мы можем использовать несколько блоков catch только с одним try.
  • try-catch блоки могут быть вложенными — этим они очень похожи на if-else конструкции.
  • Мы можем использовать только один, блок finally в одном try-catch.

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

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

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

На рисунке 1 представлена иерархия исключений в Java:

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

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

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

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

Полезные методы класса Throwable:

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

Автоматическое управление ресурсами и улучшения блока перехвата ошибок в Java 7

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

Это выглядит следующим образом:

catch(IOException | SQLException | Exception ex){

     //что-то сделать с перехваченной ошибкой…

}

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

В большинстве случаев мы используем блок finally для того, чтобы закрыть открытые потоки, подключения или освободить другие ресурсы. Очень часто мы забываем закрыть и получаем runtime исключения. Такие исключения трудно отлаживать. Поэтому в Java 7 был введен try с ресурсами, где мы можем открыть ресурс в самом try и использовать его внутри блока try-catch. Когда программа заканчивает выполнение блока try-catch, то среда выполнения автоматически закрывает эти ресурсы. Вот пример try-catch блока с ресурсами:

// try c ресурсами

try (MyResource mr = new MyResource()) {

            System.out.println(«Красивый и компактный код в try c ресурсами»);

        } catch (Exception e) {

            e.printStackTrace();

        }

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

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

Вот пример своего класса исключений и его использование:

package ua.com.prologistic;

// наследуемся от класс Exception

public class MyException extends Exception {

    private String errorCode = «Unknown_Exception»;

    public MyException(String message, String errorCode){

        super(message);

        this.errorCode = errorCode;

    }

    public String getErrorCode(){

        return this.errorCode;

    }

}

А теперь проверим в работе наш класс MyException:

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

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

package ua.com.prologistic;

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(«Неподходящий тип файла»);

            throw e;

        case «FILE_NOT_FOUND_EXCEPTION»:

            System.out.println(«Файл не найден»);

            throw e;

        case «FILE_CLOSE_EXCEPTION»:

            System.out.println(«Ошибка при закрытии файла»);

            break;

        default:

            System.out.println(«Произошла неизвестная ошибка « + 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»);

            }

        }

    }

}

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

  1. Не используйте для перехвата исключений класс Exception. В иерархии исключений есть множество классов на все случаи жизни вашей программы, которые не только эффективно обработают конкретную ошибку, но и предоставят полезную для пользователя и отладки информацию.
  2. Бросайте исключение как можно раньше. Это является хорошей практикой программирования на Java.
  3. Ловите исключения только тогда, когда сможете эффективно для пользователя и отладки их обработать.
  4. Освобождайте ресурсы. Перехватывая исключение всегда закрывайте открытые ресурсы. Еще проще и эффективнее это делать с Java 7. Используйте try с ресурсами для лаконичного и красивого кода.
  5. Логируйте исключения. Логируйте сообщения, которые предоставляет исключение. В большинстве случаев это даст вам четкое понимание причин и поможет в отладке. Не оставляйте пустым блок catch, иначе он будет просто поглощать исключение без каких-либо значимых деталей для отладки.
  6. Один catch для нескольких исключений. Используйте преимущества Java 7 для удобства и красоты вашего кода.
  7. Используйте свои исключения. Это позволит вам лучше чувствовать свою программу и эффективнее с ней работать.
  8. Соглашения об именовании. Когда вы создать свои классы исключений, следите за тем, что из самого названия класса будет ясно, что это исключение.
  9. Используйте исключения с умом. Бросить исключение — достаточно дорогостоящая в Java операция. Возможно, в некоторых случаях будем уместно не бросать исключений, а вернуть, например, логическую переменную, которая обозначала успешное или не успешное выполнение метода.
  10. Документируйте исключения. Желательно писать javadoc @throws для ваших исключений. Это будет особенно полезно в тех случаях, когда ваша программа предоставляет интерфейс для работы с другими приложениями.

Вот и все, что нужно знать об обработке исключений в Java.

Возможно, вам также будет интересно:

  • Jam 9000 kyocera 6525 ошибка
  • Jam 4211 ошибка kyocera
  • Jam 4209 ошибка kyocera 2035
  • Jam 4201 ошибка kyocera 6525
  • Jam 4201 ошибка kyocera 2035

  • Понравилась статья? Поделить с друзьями:
    0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии