Исключения в Java, часть 1: основы обработки исключений

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

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

Обратите внимание, что примеры кода в этом руководстве совместимы с JDK 12.

загрузить Получить код Загрузите исходный код для примеров приложений из этого руководства. Создано Джеффом Фризеном для JavaWorld.

Что такое исключения Java?

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

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

Java классифицирует исключения, возникающие из-за внешних факторов (таких как отсутствующий файл), как проверенные исключения . Компилятор Java проверяет, что такие исключения либо обрабатываются (исправляются) там, где они возникают, либо задокументированы для обработки в другом месте.

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

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

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

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

Об исключениях времени выполнения

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

Ошибки

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

Исключения в исходном коде

Исключение может быть представлено в исходном коде как код ошибки или как объект . Я представлю оба и покажу вам, почему объекты лучше.

Коды ошибок по сравнению с объектами

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

if (chdir("C:\\temp")) printf("Unable to change to temp directory: %d\n", errno); FILE *fp = fopen("C:\\temp\\foo"); if (fp == NULL) printf("Unable to open foo: %d\n", errno);

chdir()Функция C (изменить каталог) возвращает целое число: 0 в случае успеха или -1 в случае неудачи. Точно так же fopen()функция C (открытие файла) возвращает ненулевой указатель (целочисленный адрес) на FILEструктуру в случае успеха или нулевой (0) указатель (представленный константой NULL) в случае ошибки. В любом случае, чтобы определить исключение, вызвавшее сбой, необходимо прочитать errnoкод ошибки глобальной переменной, основанный на целых числах.

Коды ошибок представляют некоторые проблемы:

  • Целые числа бессмысленны; они не описывают исключения, которые они представляют. Например, что означает 6?
  • Связать контекст с кодом ошибки неудобно. Например, вы можете захотеть вывести имя файла, который не удалось открыть, но где вы собираетесь сохранить имя файла?
  • Целые числа являются произвольными, что может привести к путанице при чтении исходного кода. Например, указание if (!chdir("C:\\temp"))( !означает НЕ) вместо if (chdir("C:\\temp"))проверки на сбой более понятно. Тем не менее, 0 был выбран для обозначения успеха, и поэтому if (chdir("C:\\temp"))должен быть указан для проверки на отказ.
  • Коды ошибок слишком легко игнорировать, что может привести к ошибкам в коде. Например, программист может указать chdir("C:\\temp");и проигнорировать if (fp == NULL)проверку. Более того, программисту не нужно разбираться errno. Не выполняя проверку на сбой, программа ведет себя хаотично, когда одна из функций возвращает индикатор сбоя.

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

  • Объект может быть создан из класса с осмысленным именем. Например, FileNotFoundExceptionjava.ioпакете) более значимо, чем 6.
  • Объекты могут хранить контекст в различных полях. Например, вы можете сохранить сообщение, имя файла, который не удалось открыть, самую последнюю позицию, в которой операция синтаксического анализа не удалась, и / или другие элементы в полях объекта.
  • Вы не используете ifинструкции для проверки на неудачу. Вместо этого объекты исключения передаются обработчику, отдельному от программного кода. В результате исходный код легче читать и с меньшей вероятностью будет содержать ошибки.

Throwable и его подклассы

Java предоставляет иерархию классов, которые представляют различные виды исключений. Эти классы коренятся в java.langпакете в Throwableклассе, наряду с его Exception, RuntimeExceptionи Errorподклассами.

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

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

  • Throwable () создает Throwableсообщение без подробностей. Этот конструктор подходит для ситуаций, когда нет контекста. Например, вам нужно знать только, что стек пуст или полон.
  • Throwable (String message) создает сообщение Throwableс messageподробным описанием. Это сообщение может быть выведено пользователю и / или зарегистрировано.

Throwableпредоставляет String getMessage()метод для возврата подробного сообщения. Он также предоставляет дополнительные полезные методы, о которых я расскажу позже.

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

Throwableимеет два прямых подкласса. Один из этих подклассов Exception, который описывает исключение, возникающее из-за внешнего фактора (например, попытки чтения из несуществующего файла). Exceptionобъявляет те же конструкторы (с идентичными списками параметров) Throwable, что и, и каждый конструктор вызывает свой Throwableаналог. Exceptionнаследует Throwableметоды; он не объявляет никаких новых методов.

Java предоставляет множество классов исключений, которые напрямую являются подклассами Exception. Вот три примера:

  • CloneNotSupportedException сигнализирует о попытке клонирования объекта, класс которого не реализует Cloneableинтерфейс. Оба типа есть в java.langупаковке.
  • IOException сигнализирует о том, что произошел какой-то сбой ввода-вывода. Этот тип находится в java.ioупаковке.
  • ParseException сигнализирует о сбое при синтаксическом анализе текста. Этот тип можно найти в java.textупаковке.

Обратите внимание, что Exceptionимя каждого подкласса заканчивается словом Exception. Это соглашение позволяет легко определить цель класса.

Обычно вы создаете подкласс Exception(или один из его подклассов) со своими собственными классами исключений (имена которых должны заканчиваться на Exception). Вот несколько примеров пользовательских подклассов:

public class StackFullException extends Exception { } public class EmptyDirectoryException extends Exception { private String directoryName; public EmptyDirectoryException(String message, String directoryName) { super(message); this.directoryName = directoryName; } public String getDirectoryName() { return directoryName; } }

В первом примере описывается класс исключения, для которого не требуется подробное сообщение. По умолчанию вызывается конструктор noargument Exception(), который вызывает Throwable().

Во втором примере описывается класс исключения, конструктору которого требуется подробное сообщение и имя пустого каталога. Конструктор вызывает Exception(String message), который вызывает Throwable(String message).

Объекты, созданные из Exceptionодного или одного из его подклассов (за исключением RuntimeExceptionодного из его подклассов), являются отмеченными исключениями.

Класс RuntimeException

Exceptionнапрямую подклассифицируется по RuntimeException, что описывает исключение, которое, скорее всего, возникает из-за плохо написанного кода. RuntimeExceptionобъявляет те же конструкторы (с идентичными списками параметров) Exception, что и, и каждый конструктор вызывает свой Exceptionаналог. RuntimeExceptionнаследует Throwableметоды. Он не объявляет никаких новых методов.

Java provides many exception classes that directly subclass RuntimeException. The following examples are all members of the java.lang package:

  • ArithmeticException signals an illegal arithmetic operation, such as attempting to divide an integer by 0.
  • IllegalArgumentException signals that an illegal or inappropriate argument has been passed to a method.
  • NullPointerException signals an attempt to invoke a method or access an instance field via the null reference.

Objects instantiated from RuntimeException or one of its subclasses are unchecked exceptions.

The Error class

Throwable's other direct subclass is Error, which describes a serious (even abnormal) problem that a reasonable application should not try to handle--such as running out of memory, overflowing the JVM's stack, or attempting to load a class that cannot be found. Like Exception, Error declares identical constructors to Throwable, inherits Throwable's methods, and doesn't declare any of its own methods.

You can identify Error subclasses from the convention that their class names end with Error. Examples include OutOfMemoryError, LinkageError, and StackOverflowError. All three types belong to the java.lang package.

Throwing exceptions

A C library function notifies calling code of an exception by setting the global errno variable to an error code and returning a failure code. In contrast, a Java method throws an object. Knowing how and when to throw exceptions is an essential aspect of effective Java programming. Throwing an exception involves two basic steps:

  1. Use the throw statement to throw an exception object.
  2. Use the throws clause to inform the compiler.

Later sections will focus on catching exceptions and cleaning up after them, but first let's learn more about throwables.

The throw statement

Java provides the throw statement to throw an object that describes an exception. Here's the syntax of the throw statement :

throw throwable;

The object identified by throwable is an instance of Throwable or any of its subclasses. However, you usually only throw objects instantiated from subclasses of Exception or RuntimeException. Here are a couple of examples:

throw new FileNotFoundException("unable to find file " + filename); throw new IllegalArgumentException("argument passed to count is less than zero");

The throwable is thrown from the current method to the JVM, which checks this method for a suitable handler. If not found, the JVM unwinds the method-call stack, looking for the closest calling method that can handle the exception described by the throwable. If it finds this method, it passes the throwable to the method's handler, whose code is executed to handle the exception. If no method is found to handle the exception, the JVM terminates with a suitable message.

The throws clause

You need to inform the compiler when you throw a checked exception out of a method. Do this by appending a throws clause to the method's header. This clause has the following syntax:

throws checkedExceptionClassName (, checkedExceptionClassName)*

A throws clause consists of keyword throws followed by a comma-separated list of the class names of checked exceptions thrown out of the method. Here is an example:

public static void main(String[] args) throws ClassNotFoundException { if (args.length != 1) { System.err.println("usage: java ... classfile"); return; } Class.forName(args[0]); }

This example attempts to load a classfile identified by a command-line argument. If Class.forName() cannot find the classfile, it throws a java.lang.ClassNotFoundException object, which is a checked exception.

Checked exception controversy

The throws clause and checked exceptions are controversial. Many developers hate being forced to specify throws or handle the checked exception(s). Learn more about this from my Are checked exceptions good or bad? blog post.