Undantag i Java, del 1: Grunderna för undantagshantering

Java-undantag är bibliotektyper och språkfunktioner som används för att representera och hantera programfel. Om du har velat förstå hur fel representeras i källkoden har du kommit till rätt ställe. Förutom en översikt över Java-undantag kommer jag att komma igång med Javas språkfunktioner för att kasta objekt, prova kod som kan misslyckas, fånga kastade objekt och städa upp din Java-kod efter att ett undantag har kastats.

Under den första halvan av denna handledning lär du dig om grundläggande språkfunktioner och bibliotekstyper som har funnits sedan Java 1.0. Under andra halvåret kommer du att upptäcka avancerade funktioner som introducerats i nyare Java-versioner.

Observera att kodexempel i denna handledning är kompatibla med JDK 12.

ladda ner Skaffa koden Ladda ner källkoden till exempel applikationer i denna handledning. Skapad av Jeff Friesen för JavaWorld.

Vad är Java-undantag?

Fel uppstår när ett Java-programs normala beteende avbryts av oväntat beteende. Denna avvikelse är känd som ett undantag . Till exempel försöker ett program att öppna en fil för att läsa dess innehåll, men filen finns inte. Java klassificerar undantag i några typer, så låt oss överväga var och en.

Kontrollerade undantag

Java klassificerar undantag som härrör från externa faktorer (som en saknad fil) som markerade undantag . Java-kompilatorn kontrollerar att sådana undantag antingen hanteras (korrigeras) där de förekommer eller dokumenteras för att hanteras någon annanstans.

Undantagshanterare

En undantagshanterare är en sekvens av kod som hanterar ett undantag. Det förhör sammanhanget - vilket innebär att det läser värden som sparats från variabler som var inom räckvidden vid den tidpunkt då undantaget inträffade - använder sedan det som lär sig för att återställa Java-programmet till ett flöde av normalt beteende. En undantagshanterare kan till exempel läsa ett sparat filnamn och uppmana användaren att ersätta den saknade filen.

Runtime (unchecked) undantag

Antag att ett program försöker dela ett heltal med heltal 0. Denna omöjlighet illustrerar en annan typ av undantag, nämligen ett runtimeundantag . Till skillnad från markerade undantag uppstår undantag från runtime vanligtvis från dåligt skriven källkod och bör därför fixas av programmeraren. Eftersom kompilatorn inte kontrollerar att runtimeundantag hanteras eller dokumenteras för att hanteras någon annanstans kan du tänka på ett runtimeundantag som ett okontrollerat undantag .

Om undantag för runtime

Du kan ändra ett program för att hantera ett runtime-undantag, men det är bättre att fixa källkoden. Runtimeundantag uppstår ofta från att överföra ogiltiga argument till ett biblioteks metoder buggysamtalskoden bör fixas.

Fel

Vissa undantag är mycket allvarliga eftersom de äventyrar programmets förmåga att fortsätta körningen. Till exempel försöker ett program att tilldela minne från JVM men det finns inte tillräckligt med ledigt minne för att tillgodose begäran. En annan allvarlig situation uppstår när ett program försöker ladda en klassfil via ett metodanrop Class.forName(), men klassfilen är skadad. Denna typ av undantag kallas ett fel . Du bör aldrig försöka hantera fel själv eftersom JVM kanske inte kan återhämta sig från det.

Undantag i källkoden

Ett undantag kan representeras i källkoden som en felkod eller som ett objekt . Jag presenterar båda och visar varför föremål är överlägsna.

Felkoder kontra objekt

Programmeringsspråk som C använder heltalbaserade felkoder för att representera fel och orsaker till fel - dvs. undantag. Här är några exempel:

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

C: s chdir()(ändra katalog) -funktionen returnerar ett heltal: 0 vid framgång eller -1 vid misslyckande. På samma sätt fopen()returnerar C: s (filöppen) -funktion en icke- pekare ( heltaladress ) till en FILEstruktur efter framgång eller en null (0) -pekare (representerad av konstant NULL) vid misslyckande. I båda fallen måste du läsa den globala errnovariabelns heltalbaserade felkod för att identifiera undantaget som orsakade felet.

Felkoder ger vissa problem:

  • Heltals är meningslösa; de beskriver inte de undantag de representerar. Till exempel, vad betyder 6?
  • Att associera sammanhang med en felkod är besvärligt. Du kanske till exempel vill mata ut namnet på filen som inte kunde öppnas, men var ska du lagra filens namn?
  • Heltals är godtyckliga, vilket kan leda till förvirring när man läser källkod. Exempelvis är det tydligare att ange if (!chdir("C:\\temp"))( !betyder INTE) istället för if (chdir("C:\\temp"))att testa för fel. Emellertid valdes 0 för att indikera framgång och if (chdir("C:\\temp"))måste därför anges för att testa för misslyckande.
  • Felkoder är för lätta att ignorera, vilket kan leda till buggykod. Till exempel kan programmeraren specificera chdir("C:\\temp");och ignorera if (fp == NULL)kontrollen. Dessutom behöver programmeraren inte undersöka errno. Genom att inte testa för fel fungerar programmet oregelbundet när någon funktion returnerar en felindikator.

För att lösa dessa problem anammade Java ett nytt tillvägagångssätt för undantagshantering. I Java kombinerar vi objekt som beskriver undantag med en mekanism baserad på att kasta och fånga dessa objekt. Här är några fördelar med att använda objekt kontra felkod för att beteckna undantag:

  • Ett objekt kan skapas från en klass med ett meningsfullt namn. Till exempel FileNotFoundException(i java.iopaketet) är mer meningsfullt än 6.
  • Objekt kan lagra sammanhang i olika fält. Du kan till exempel lagra ett meddelande, namnet på filen som inte kunde öppnas, den senaste positionen där en analysoperation misslyckades och / eller andra objekt i ett objekts fält.
  • Du använder inte ifuttalanden för att testa för misslyckande. Istället kastas undantagsobjekt till en hanterare som är skild från programkoden. Som ett resultat är källkoden lättare att läsa och mindre sannolikt att vara buggy.

Kastbar och dess underklasser

Java ger en hierarki av klasser som representerar olika slags undantag. Dessa klasser är rotade i java.langpaketets Throwableklassen, tillsammans med dess Exception, RuntimeExceptionoch Errorunderklasser.

Throwableär den ultimata superklassen när det gäller undantag. Endast objekt som skapats från Throwableoch dess underklasser kan kastas (och fångas därefter). Sådana föremål är kända som kastbara .

Ett Throwableobjekt är associerat med ett detaljmeddelande som beskriver ett undantag. Flera konstruktörer, inklusive paret som beskrivs nedan, tillhandahålls för att skapa ett Throwableobjekt med eller utan ett detaljerat meddelande:

  • Throwable () skapar ett Throwablemeddelande utan detaljer. Denna konstruktör är lämplig för situationer där det inte finns något sammanhang. Till exempel vill du bara veta att en stack är tom eller full.
  • Throwable (strängmeddelande) skapar ett Throwablemed messagesom detaljmeddelande. Detta meddelande kan skickas till användaren och / eller loggas.

Throwabletillhandahåller String getMessage()metoden för att returnera detaljmeddelandet. Det ger också ytterligare användbara metoder som jag kommer att introducera senare.

Undantagsklassen

Throwable has two direct subclasses. One of these subclasses is Exception, which describes an exception arising from an external factor (such as attempting to read from a nonexistent file). Exception declares the same constructors (with identical parameter lists) as Throwable, and each constructor invokes its Throwable counterpart. Exception inherits Throwable's methods; it declares no new methods.

Java provides many exception classes that directly subclass Exception. Here are three examples:

  • CloneNotSupportedException signals an attempt to clone an object whose class doesn't implement the Cloneable interface. Both types are in the java.lang package.
  • IOException signals that some kind of I/O failure has occurred. This type is located in the java.io package.
  • ParseException signals that a failure has occurred while parsing text. This type can be found in the java.text package.

Notice that each Exception subclass name ends with the word Exception. This convention makes it easy to identify the class's purpose.

You'll typically subclass Exception (or one of its subclasses) with your own exception classes (whose names should end with Exception). Here are a couple of custom subclass examples:

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

The first example describes an exception class that doesn't require a detail message. It's default noargument constructor invokes Exception(), which invokes Throwable().

The second example describes an exception class whose constructor requires a detail message and the name of the empty directory. The constructor invokes Exception(String message), which invokes Throwable(String message).

Objects instantiated from Exception or one of its subclasses (except for RuntimeException or one of its subclasses) are checked exceptions.

The RuntimeException class

Exception is directly subclassed by RuntimeException, which describes an exception most likely arising from poorly written code. RuntimeException declares the same constructors (with identical parameter lists) as Exception, and each constructor invokes its Exception counterpart. RuntimeException inherits Throwable's methods. It declares no new methods.

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.