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

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

В първата половина на този урок ще научите за основните езикови функции и типовете библиотеки, които съществуват от Java 1.0. През втората половина ще откриете разширени възможности, въведени в по-новите версии на Java.

Имайте предвид, че примерите за кодове в този урок са съвместими с JDK 12.

изтегляне Вземете кода Изтеглете изходния код, например приложения в този урок. Създадено от Jeff Friesen за 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изявления, за да тествате за неуспех. Вместо това обектите за изключение се хвърлят към манипулатор, който е отделен от програмния код. В резултат на това изходният код е по-лесен за четене и е по-малко вероятно да бъде бъги.

Хвърлимо и неговите подкласове

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 сигнализира, че е настъпила някаква грешка в I / O. Този тип се намира в 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.