Wyjątki w Javie, część 1: Podstawy obsługi wyjątków

Wyjątki Java to typy bibliotek i funkcje językowe używane do reprezentowania błędów programu i radzenia sobie z nimi. Jeśli chciałeś zrozumieć, w jaki sposób błąd jest reprezentowany w kodzie źródłowym, trafiłeś we właściwe miejsce. Oprócz przeglądu wyjątków Java, zacznę od funkcji języka Java do rzucania obiektów, próbowania kodu, który może się nie powieść, przechwytywania rzucanych obiektów i czyszczenia kodu Java po rzuceniu wyjątku.

W pierwszej połowie tego samouczka dowiesz się o podstawowych funkcjach językowych i typach bibliotek, które istniały od czasów Java 1.0. W drugiej połowie poznasz zaawansowane możliwości wprowadzone w nowszych wersjach Java.

Zwróć uwagę, że przykłady kodu w tym samouczku są zgodne z JDK 12.

pobierz Pobierz kod Pobierz kod źródłowy na przykład aplikacje w tym samouczku. Stworzone przez Jeffa Friesena dla JavaWorld.

Co to są wyjątki Java?

Niepowodzenie występuje, gdy normalne zachowanie programu Java zostaje przerwane przez nieoczekiwane zachowanie. Ta rozbieżność jest znana jako wyjątek . Na przykład program próbuje otworzyć plik w celu odczytania jego zawartości, ale plik nie istnieje. Java klasyfikuje wyjątki na kilka typów, więc rozważmy każdy z nich.

Sprawdzone wyjątki

Java klasyfikuje wyjątki wynikające z czynników zewnętrznych (takich jak brakujący plik) jako sprawdzone wyjątki . Kompilator Java sprawdza, czy takie wyjątki są obsługiwane (korygowane) w miejscu ich wystąpienia lub udokumentowane w celu obsługi w innym miejscu.

Programy obsługi wyjątków

Obsługi wyjątku jest sekwencją kodu, który obsługuje wyjątek. Bada kontekst - co oznacza, że ​​odczytuje wartości zapisane ze zmiennych, które znajdowały się w zakresie w momencie wystąpienia wyjątku - a następnie wykorzystuje to, czego się nauczy, do przywrócenia programu Java do przepływu normalnego zachowania. Na przykład program obsługi wyjątków może odczytać zapisaną nazwę pliku i poprosić użytkownika o zastąpienie brakującego pliku.

Wyjątki w czasie wykonywania (niezaznaczone)

Załóżmy, że program próbuje podzielić liczbę całkowitą przez liczbę całkowitą 0. Ta niemożność ilustruje inny rodzaj wyjątku, a mianowicie wyjątek czasu wykonywania . W przeciwieństwie do sprawdzonych wyjątków, wyjątki środowiska uruchomieniowego zwykle wynikają ze źle napisanego kodu źródłowego i dlatego powinny zostać naprawione przez programistę. Ponieważ kompilator nie sprawdza, czy wyjątki środowiska uruchomieniowego są obsługiwane lub dokumentowane w celu obsługi w innym miejscu, można traktować wyjątek czasu wykonywania jako niezaznaczony wyjątek .

Informacje o wyjątkach czasu wykonywania

Możesz zmodyfikować program, aby obsługiwał wyjątek czasu wykonywania, ale lepiej jest naprawić kod źródłowy. Wyjątki w czasie wykonywania często wynikają z przekazywania nieprawidłowych argumentów do metod biblioteki; błędny kod wywoławczy powinien zostać naprawiony.

Błędy

Niektóre wyjątki są bardzo poważne, ponieważ zagrażają zdolności programu do dalszego wykonywania. Na przykład program próbuje przydzielić pamięć z JVM, ale nie ma wystarczającej ilości wolnej pamięci, aby spełnić żądanie. Inna poważna sytuacja występuje, gdy program próbuje załadować plik klasy za pomocą Class.forName()wywołania metody, ale plik klasy jest uszkodzony. Ten rodzaj wyjątku jest nazywany błędem . Nigdy nie należy próbować samodzielnie obsługiwać błędów, ponieważ maszyna JVM może nie być w stanie ich naprawić.

Wyjątki w kodzie źródłowym

Wyjątek może być przedstawiony w kodzie źródłowym jako kod błędu lub jako obiekt . Przedstawię oba i pokażę, dlaczego przedmioty są lepsze.

Kody błędów a obiekty

Języki programowania, takie jak C, używają kodów błędów opartych na liczbach całkowitych , aby przedstawić awarię i przyczyny niepowodzenia - tj. Wyjątki. Oto kilka przykładów:

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()Funkcja C (zmiana katalogu) zwraca liczbę całkowitą: 0 w przypadku powodzenia lub -1 w przypadku niepowodzenia. Podobnie fopen()funkcja języka C (otwieranie pliku) zwraca wskaźnik inny niż pusty (adres całkowity) do FILEstruktury w przypadku sukcesu lub wskaźnik zerowy (0) (reprezentowany przez stałą NULL) w przypadku błędu. W obu przypadkach, aby zidentyfikować wyjątek, który spowodował błąd, należy odczytać errnokod błędu zmiennej globalnej oparty na liczbach całkowitych.

Kody błędów powodują pewne problemy:

  • Liczby całkowite są bez znaczenia; nie opisują wyjątków, które reprezentują. Na przykład, co oznacza 6?
  • Powiązanie kontekstu z kodem błędu jest niewygodne. Na przykład możesz chcieć wyświetlić nazwę pliku, którego nie można otworzyć, ale gdzie zamierzasz zapisać nazwę pliku?
  • Liczby całkowite są dowolne, co może prowadzić do nieporozumień podczas czytania kodu źródłowego. Na przykład określenie if (!chdir("C:\\temp"))( !oznacza NIE) zamiast if (chdir("C:\\temp"))testowania pod kątem niepowodzenia jest bardziej przejrzyste. Jednak wybrano 0, aby wskazać sukces, dlatego if (chdir("C:\\temp"))należy określić, aby testować pod kątem niepowodzenia.
  • Kody błędów są zbyt łatwe do zignorowania, co może prowadzić do błędnego kodu. Na przykład programista może określić chdir("C:\\temp");i zignorować if (fp == NULL)sprawdzenie. Ponadto programista nie musi badać errno. Nie testując awarii, program zachowuje się nieprawidłowo, gdy jedna z funkcji zwraca wskaźnik awarii.

Aby rozwiązać te problemy, Java przyjęła nowe podejście do obsługi wyjątków. W Javie łączymy obiekty opisujące wyjątki z mechanizmem polegającym na rzucaniu i łapaniu tych obiektów. Oto kilka zalet używania obiektów w porównaniu z kodem błędu do oznaczania wyjątków:

  • Obiekt można utworzyć z klasy o znaczącej nazwie. Na przykład FileNotFoundException(w java.iopakiecie) jest bardziej znaczące niż 6.
  • Obiekty mogą przechowywać kontekst w różnych polach. Na przykład, możesz zapisać wiadomość, nazwę pliku, którego nie można otworzyć, ostatnią pozycję, w której operacja analizy nie powiodła się i / lub inne elementy w polach obiektu.
  • Nie używasz ifinstrukcji do testowania błędów. Zamiast tego obiekty wyjątków są zgłaszane do programu obsługi, który jest oddzielny od kodu programu. W rezultacie kod źródłowy jest łatwiejszy do odczytania i rzadziej zawiera błędy.

Rzut i jego podklasy

Java udostępnia hierarchię klas, które reprezentują różne rodzaje wyjątków. Klasy te są zakorzenione w java.langpaczki Throwableklasy, wraz z jego Exception, RuntimeExceptioni Errorpodklasy.

Throwablejest ostateczną superklasą, jeśli chodzi o wyjątki. Tylko obiekty utworzone na podstawie Throwablei ich podklasy mogą być wyrzucane (a następnie przechwytywane). Takie przedmioty są znane jako przedmioty do rzucania .

ThrowableObiekt jest związany z komunikatem szczegółowym opisującej wyjątek. Dostępnych jest kilka konstruktorów, w tym para opisana poniżej, w celu utworzenia Throwableobiektu z komunikatem szczegółowym lub bez niego:

  • Throwable () tworzy komunikat Throwablebez szczegółów. Ten konstruktor jest odpowiedni w sytuacjach, w których nie ma kontekstu. Na przykład chcesz tylko wiedzieć, że stos jest pusty lub pełny.
  • Throwable (komunikat String) tworzy Throwablez messagejako wiadomość szczegółową. Ta wiadomość może zostać wysłana do użytkownika i / lub zarejestrowana.

Throwablezapewnia String getMessage()metodę zwracania szczegółowego komunikatu. Zawiera również dodatkowe przydatne metody, które przedstawię później.

Klasa Exception

Throwablema dwie bezpośrednie podklasy. Jedna z tych podklas to Exception, która opisuje wyjątek wynikający z czynnika zewnętrznego (takiego jak próba odczytu z nieistniejącego pliku). Exceptiondeklaruje te same konstruktory (z identycznymi listami parametrów) co Throwable, a każdy konstruktor wywołuje swój Throwableodpowiednik. Exceptiondziedziczy Throwablemetody; deklaruje żadnych nowych metod.

Java udostępnia wiele klas wyjątków, które są bezpośrednio podklasy Exception. Oto trzy przykłady:

  • CloneNotSupportedException sygnalizuje próbę sklonowania obiektu, którego klasa nie implementuje Cloneableinterfejsu. W java.langopakowaniu znajdują się oba typy .
  • IOException sygnalizuje, że wystąpił jakiś błąd we / wy. Ten typ znajduje się w java.ioopakowaniu.
  • ParseException sygnalizuje, że wystąpił błąd podczas analizowania tekstu. Ten typ można znaleźć w java.textpakiecie.

Zauważ, że każda Exceptionnazwa podklasy kończy się słowem Exception. Ta konwencja ułatwia identyfikację celu klasy.

Zwykle będziesz podklasę Exception(lub jedną z jej podklas) z własnymi klasami wyjątków (których nazwy powinny kończyć się na Exception). Oto kilka przykładów podklas niestandardowych:

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; } }

Pierwszy przykład opisuje klasę wyjątku, która nie wymaga szczegółowego komunikatu. Jest to domyślne wywołanie konstruktora noargument Exception(), które wywołuje Throwable().

Drugi przykład opisuje klasę wyjątków, której konstruktor wymaga szczegółowego komunikatu i nazwy pustego katalogu. Konstruktor wywołuje Exception(String message), który wywołuje Throwable(String message).

Obiekty, których wystąpienie pochodzi z Exceptionlub jednej z jej podklas (z wyjątkiem RuntimeExceptionlub jednej z jej podklas), są sprawdzane jako wyjątki.

Klasa RuntimeException

Exceptionjest bezpośrednio podklasą przez RuntimeException, co opisuje wyjątek najprawdopodobniej wynikający ze źle napisanego kodu. RuntimeExceptiondeklaruje te same konstruktory (z identycznymi listami parametrów) co Exception, a każdy konstruktor wywołuje swój Exceptionodpowiednik. RuntimeExceptiondziedziczy Throwablemetody. Nie deklaruje żadnych nowych metod.

Java udostępnia wiele klas wyjątków, które są bezpośrednio podklasy RuntimeException. Poniższe przykłady są członkami java.langpakietu:

  • ArithmeticException sygnalizuje niedozwoloną operację arytmetyczną, taką jak próba podzielenia liczby całkowitej przez 0.
  • IllegalArgumentException sygnalizuje, że do metody został przekazany niedozwolony lub niewłaściwy argument.
  • NullPointerException sygnalizuje próbę wywołania metody lub uzyskania dostępu do pola wystąpienia za pośrednictwem odwołania o wartości null.

Obiekty, dla których utworzono wystąpienie z RuntimeExceptionlub jednej z jej podklas, są niezaznaczonymi wyjątkami .

Klasa Error

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.