Undantagshantering i Java

1. Översikt

I den här handledningen går vi igenom grunderna för undantagshantering i Java liksom några av dess gotchas.

2. Första principerna

2.1. Vad är det?

För att bättre förstå undantag och hantering av undantag, låt oss göra en verklig jämförelse.

Tänk dig att vi beställer en produkt online, men när vi är på väg är det fel i leveransen. Ett bra företag kan hantera detta problem och omdirigera vårt paket så att det fortfarande kommer i tid.

På samma sätt i Java kan koden uppstå fel när våra instruktioner utförs. Bra undantagshantering kan hantera fel och graciöst omdirigera programmet för att ge användaren fortfarande en positiv upplevelse .

2.2. Varför använda det?

Vi skriver vanligtvis kod i en idealiserad miljö: filsystemet innehåller alltid våra filer, nätverket är friskt och JVM har alltid tillräckligt med minne. Ibland kallar vi detta för "lycklig väg".

I produktionen kan dock filsystem skadas, nätverk går sönder och JVM-enheter har slut på minne. Vår kods välbefinnande beror på hur den hanterar "olyckliga vägar".

Vi måste hantera dessa villkor eftersom de påverkar applikationsflödet negativt och bildar undantag :

public static List getPlayers() throws IOException { Path path = Paths.get("players.dat"); List players = Files.readAllLines(path); return players.stream() .map(Player::new) .collect(Collectors.toList()); }

Den här koden väljer att inte hantera IOException , utan skickar den upp i samtalsstacken istället. I en idealiserad miljö fungerar koden bra.

Men vad kan hända i produktionen om spelare.dat saknas?

Exception in thread "main" java.nio.file.NoSuchFileException: players.dat <-- players.dat file doesn't exist at sun.nio.fs.WindowsException.translateToIOException(Unknown Source) at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source) // ... more stack trace at java.nio.file.Files.readAllLines(Unknown Source) at java.nio.file.Files.readAllLines(Unknown Source) at Exceptions.getPlayers(Exceptions.java:12) <-- Exception arises in getPlayers() method, on line 12 at Exceptions.main(Exceptions.java:19) <-- getPlayers() is called by main(), on line 19

Utan att hantera detta undantag kan ett annars hälsosamt program sluta köras helt! Vi måste se till att vår kod har en plan för när saker går fel.

Observera också en fördel här till undantag, och det är själva stackspåret. På grund av detta stackspår kan vi ofta hitta felande kod utan att behöva bifoga en felsökare.

3. Undantagshierarki

I slutändan är undantag bara Java-objekt med alla som sträcker sig från Throwable :

 ---> Throwable  Exception Error | (checked) (unchecked) | RuntimeException (unchecked)

Det finns tre huvudkategorier av exceptionella förhållanden:

  • Kontrollerade undantag
  • Okontrollerade undantag / Runtime-undantag
  • Fel

Runtime och okontrollerade undantag hänvisar till samma sak. Vi kan ofta använda dem omväxlande.

3.1. Kontrollerade undantag

Kontrollerade undantag är undantag som Java-kompilatorn kräver att vi hanterar. Vi måste antingen deklarativt kasta undantaget i samtalsstacken eller så måste vi hantera det själva. Mer om båda dessa på ett ögonblick.

Oracles dokumentation säger att vi ska använda kontrollerade undantag när vi rimligen kan förvänta oss att den som ringer till vår metod ska kunna återhämta sig.

Ett par exempel på markerade undantag är IOException och ServletException.

3.2. Okontrollerade undantag

Okontrollerade undantag är undantag som Java-kompilatorn inte kräver att vi hanterar.

Enkelt uttryckt, om vi skapar ett undantag som förlänger RuntimeException kommer det att vara avmarkerat; annars kommer det att kontrolleras.

Och även om detta låter bekvämt, säger Oracles dokumentation att det finns goda skäl för båda begreppen, som att skilja mellan ett lägesfel (markerat) och ett användningsfel (okontrollerat).

Några exempel på okontrollerade undantag är NullPointerException, IllegalArgumentException och SecurityException .

3.3. Fel

Fel representerar allvarliga och vanligtvis oåterkalleliga förhållanden som bibliotekskompatibilitet, oändlig rekursion eller minnesläckage.

Och även om de inte förlänger RuntimeException , är de också avmarkerade.

I de flesta fall skulle det vara konstigt för oss att hantera, initiera eller utöka fel . Vanligtvis vill vi att dessa ska spridas hela vägen upp.

Ett par exempel på fel är en StackOverflowError och OutOfMemoryError .

4. Hantering av undantag

I Java API finns det gott om platser där saker kan gå fel, och vissa av dessa platser är markerade med undantag, antingen i signaturen eller i Javadoc:

/** * @exception FileNotFoundException ... */ public Scanner(String fileName) throws FileNotFoundException { // ... }

Som nämnts lite tidigare, när vi kallar dessa "riskabla" metoder, måste vi hantera de markerade undantagen, och vi kan hantera de okontrollerade. Java ger oss flera sätt att göra detta:

4.1. kastar

Det enklaste sättet att "hantera" ett undantag är att kasta om det:

public int getPlayerScore(String playerFile) throws FileNotFoundException { Scanner contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); }

Eftersom FileNotFoundException är ett kontrollerat undantag är detta det enklaste sättet att tillfredsställa kompilatorn, men det betyder att alla som ringer till vår metod nu måste hantera det också!

parseInt kan kasta ett NumberFormatException , men eftersom det inte är markerat behöver vi inte hantera det.

4.2. försök fånga

Om vi ​​själva vill försöka hantera undantaget kan vi använda ett försök-fångstblock . Vi kan hantera det genom att lägga om vårt undantag:

public int getPlayerScore(String playerFile) { try { Scanner contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException noFile) { throw new IllegalArgumentException("File not found"); } }

Eller genom att utföra återställningssteg:

public int getPlayerScore(String playerFile) { try { Scanner contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } catch ( FileNotFoundException noFile ) { logger.warn("File not found, resetting score."); return 0; } }

4.3. till sist

Nu finns det tillfällen då vi har kod som måste köras oavsett om ett undantag inträffar, och det är här det slutliga nyckelordet kommer in.

I våra exempel hittills har det funnits en otäck bugg i skuggorna, vilket är att Java som standard inte returnerar filhandtag till operativsystemet.

Oavsett om vi kan läsa filen eller inte vill vi se till att vi gör rätt rengöring!

Låt oss försöka detta på "lat" sätt först:

public int getPlayerScore(String playerFile) throws FileNotFoundException { Scanner contents = null; try { contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } finally { if (contents != null) { contents.close(); } } } 

Här slutligen indikerar blocket vilken kod vi vill Java för att köra oavsett vad som händer med att försöka läsa filen.

Även om en FileNotFoundException kastas upp i samtalsstacken kommer Java äntligen att ringa upp innehållet innan det görs.

Vi kan också både hantera undantaget och se till att våra resurser stängs:

public int getPlayerScore(String playerFile) { Scanner contents; try { contents = new Scanner(new File(playerFile)); return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException noFile ) { logger.warn("File not found, resetting score."); return 0; } finally { try { if (contents != null) { contents.close(); } } catch (IOException io) { logger.error("Couldn't close the reader!", io); } } }

Eftersom nära också är en "riskabel" metod, måste vi också fånga dess undantag!

This may look pretty complicated, but we need each piece to handle each potential problem that can arise correctly.

4.4. try-with-resources

Fortunately, as of Java 7, we can simplify the above syntax when working with things that extend AutoCloseable:

public int getPlayerScore(String playerFile) { try (Scanner contents = new Scanner(new File(playerFile))) { return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException e ) { logger.warn("File not found, resetting score."); return 0; } }

When we place references that are AutoClosable in the try declaration, then we don't need to close the resource ourselves.

We can still use a finally block, though, to do any other kind of cleanup we want.

Check out our article dedicated to try-with-resources to learn more.

4.5. Multiple catch Blocks

Sometimes, the code can throw more than one exception, and we can have more than one catch block handle each individually:

public int getPlayerScore(String playerFile) { try (Scanner contents = new Scanner(new File(playerFile))) { return Integer.parseInt(contents.nextLine()); } catch (IOException e) { logger.warn("Player file wouldn't load!", e); return 0; } catch (NumberFormatException e) { logger.warn("Player file was corrupted!", e); return 0; } }

Multiple catches give us the chance to handle each exception differently, should the need arise.

Also note here that we didn't catch FileNotFoundException, and that is because it extends IOException. Because we're catching IOException, Java will consider any of its subclasses also handled.

Let's say, though, that we need to treat FileNotFoundException differently from the more general IOException:

public int getPlayerScore(String playerFile) { try (Scanner contents = new Scanner(new File(playerFile)) ) { return Integer.parseInt(contents.nextLine()); } catch (FileNotFoundException e) { logger.warn("Player file not found!", e); return 0; } catch (IOException e) { logger.warn("Player file wouldn't load!", e); return 0; } catch (NumberFormatException e) { logger.warn("Player file was corrupted!", e); return 0; } }

Java lets us handle subclass exceptions separately, remember to place them higher in the list of catches.

4.6. Union catch Blocks

When we know that the way we handle errors is going to be the same, though, Java 7 introduced the ability to catch multiple exceptions in the same block:

public int getPlayerScore(String playerFile) { try (Scanner contents = new Scanner(new File(playerFile))) { return Integer.parseInt(contents.nextLine()); } catch (IOException | NumberFormatException e) { logger.warn("Failed to load score!", e); return 0; } }

5. Throwing Exceptions

If we don't want to handle the exception ourselves or we want to generate our exceptions for others to handle, then we need to get familiar with the throw keyword.

Let's say that we have the following checked exception we've created ourselves:

public class TimeoutException extends Exception { public TimeoutException(String message) { super(message); } }

and we have a method that could potentially take a long time to complete:

public List loadAllPlayers(String playersFile) { // ... potentially long operation }

5.1. Throwing a Checked Exception

Like returning from a method, we can throw at any point.

Of course, we should throw when we are trying to indicate that something has gone wrong:

public List loadAllPlayers(String playersFile) throws TimeoutException { while ( !tooLong ) { // ... potentially long operation } throw new TimeoutException("This operation took too long"); }

Because TimeoutException is checked, we also must use the throws keyword in the signature so that callers of our method will know to handle it.

5.2. Throwing an Unchecked Exception

If we want to do something like, say, validate input, we can use an unchecked exception instead:

public List loadAllPlayers(String playersFile) throws TimeoutException { if(!isFilenameValid(playersFile)) { throw new IllegalArgumentException("Filename isn't valid!"); } // ... } 

Because IllegalArgumentException is unchecked, we don't have to mark the method, though we are welcome to.

Some mark the method anyway as a form of documentation.

5.3. Wrapping and Rethrowing

We can also choose to rethrow an exception we've caught:

public List loadAllPlayers(String playersFile) throws IOException { try { // ... } catch (IOException io) { throw io; } }

Or do a wrap and rethrow:

public List loadAllPlayers(String playersFile) throws PlayerLoadException { try { // ... } catch (IOException io) { throw new PlayerLoadException(io); } }

This can be nice for consolidating many different exceptions into one.

5.4. Rethrowing Throwable or Exception

Now for a special case.

If the only possible exceptions that a given block of code could raise are unchecked exceptions, then we can catch and rethrow Throwable or Exception without adding them to our method signature:

public List loadAllPlayers(String playersFile) { try { throw new NullPointerException(); } catch (Throwable t) { throw t; } }

While simple, the above code can't throw a checked exception and because of that, even though we are rethrowing a checked exception, we don't have to mark the signature with a throws clause.

This is handy with proxy classes and methods. More about this can be found here.

5.5. Inheritance

When we mark methods with a throws keyword, it impacts how subclasses can override our method.

In the circumstance where our method throws a checked exception:

public class Exceptions { public List loadAllPlayers(String playersFile) throws TimeoutException { // ... } }

A subclass can have a “less risky” signature:

public class FewerExceptions extends Exceptions { @Override public List loadAllPlayers(String playersFile) { // overridden } }

But not a “more riskier” signature:

public class MoreExceptions extends Exceptions { @Override public List loadAllPlayers(String playersFile) throws MyCheckedException { // overridden } }

This is because contracts are determined at compile time by the reference type. If I create an instance of MoreExceptions and save it to Exceptions:

Exceptions exceptions = new MoreExceptions(); exceptions.loadAllPlayers("file");

Then the JVM will only tell me to catch the TimeoutException, which is wrong since I've said that MoreExceptions#loadAllPlayers throws a different exception.

Simply put, subclasses can throw fewer checked exceptions than their superclass, but not more.

6. Anti-Patterns

6.1. Swallowing Exceptions

Now, there’s one other way that we could have satisfied the compiler:

public int getPlayerScore(String playerFile) { try { // ... } catch (Exception e) {} // <== catch and swallow return 0; }

The above is calledswallowing an exception. Most of the time, it would be a little mean for us to do this because it doesn't address the issue and it keeps other code from being able to address the issue, too.

There are times when there's a checked exception that we are confident will just never happen. In those cases, we should still at least add a comment stating that we intentionally ate the exception:

public int getPlayerScore(String playerFile) { try { // ... } catch (IOException e) { // this will never happen } }

Another way we can “swallow” an exception is to print out the exception to the error stream simply:

public int getPlayerScore(String playerFile) { try { // ... } catch (Exception e) { e.printStackTrace(); } return 0; }

We've improved our situation a bit by a least writing the error out somewhere for later diagnosis.

It'd be better, though, for us to use a logger:

public int getPlayerScore(String playerFile) { try { // ... } catch (IOException e) { logger.error("Couldn't load the score", e); return 0; } }

While it's very convenient for us to handle exceptions in this way, we need to make sure that we aren't swallowing important information that callers of our code could use to remedy the problem.

Finally, we can inadvertently swallow an exception by not including it as a cause when we are throwing a new exception:

public int getPlayerScore(String playerFile) { try { // ... } catch (IOException e) { throw new PlayerScoreException(); } } 

Here, we pat ourselves on the back for alerting our caller to an error, but we fail to include the IOException as the cause. Because of this, we've lost important information that callers or operators could use to diagnose the problem.

We'd be better off doing:

public int getPlayerScore(String playerFile) { try { // ... } catch (IOException e) { throw new PlayerScoreException(e); } }

Notice the subtle difference of including IOException as the cause of PlayerScoreException.

6.2. Using return in a finally Block

Another way to swallow exceptions is to return from the finally block. This is bad because, by returning abruptly, the JVM will drop the exception, even if it was thrown from by our code:

public int getPlayerScore(String playerFile) { int score = 0; try { throw new IOException(); } finally { return score; // <== the IOException is dropped } }

According to the Java Language Specification:

If execution of the try block completes abruptly for any other reason R, then the finally block is executed, and then there is a choice.

If the finally block completes normally, then the try statement completes abruptly for reason R.

If the finally block completes abruptly for reason S, then the try statement completes abruptly for reason S (and reason R is discarded).

6.3. Using throw in a finally Block

Similar to using return in a finally block, the exception thrown in a finally block will take precedence over the exception that arises in the catch block.

This will “erase” the original exception from the try block, and we lose all of that valuable information:

public int getPlayerScore(String playerFile) { try { // ... } catch ( IOException io ) { throw new IllegalStateException(io); // <== eaten by the finally } finally { throw new OtherException(); } }

6.4. Using throw as a goto

Some people also gave into the temptation of using throw as a goto statement:

public void doSomething() { try { // bunch of code throw new MyException(); // second bunch of code } catch (MyException e) { // third bunch of code } }

This is odd because the code is attempting to use exceptions for flow control as opposed to error handling.

7. Common Exceptions and Errors

Here are some common exceptions and errors that we all run into from time to time:

7.1. Checked Exceptions

  • IOException – This exception is typically a way to say that something on the network, filesystem, or database failed.

7.2. RuntimeExceptions

  • ArrayIndexOutOfBoundsException – this exception means that we tried to access a non-existent array index, like when trying to get index 5 from an array of length 3.
  • ClassCastException – this exception means that we tried to perform an illegal cast, like trying to convert a String into a List. We can usually avoid it by performing defensive instanceof checks before casting.
  • IllegalArgumentException – this exception is a generic way for us to say that one of the provided method or constructor parameters is invalid.
  • IllegalStateException – This exception is a generic way for us to say that our internal state, like the state of our object, is invalid.
  • NullPointerException – This exception means we tried to reference a null object. We can usually avoid it by either performing defensive null checks or by using Optional.
  • NumberFormatException – This exception means that we tried to convert a String into a number, but the string contained illegal characters, like trying to convert “5f3” into a number.

7.3. Errors

  • StackOverflowError - detta undantag betyder att stackspårningen är för stor. Detta kan ibland hända i massiva applikationer; dock betyder det vanligtvis att vi har någon oändlig rekursion som händer i vår kod.
  • NoClassDefFoundError - detta undantag betyder att en klass inte kunde laddas antingen på grund av att den inte befann sig på klassvägen eller på grund av fel i statisk initialisering.
  • OutOfMemoryError - detta undantag innebär att JVM inte har mer minne tillgängligt att tilldela för fler objekt. Ibland beror detta på minnesläckage.

8. Slutsats

I den här artikeln har vi gått igenom grunderna för undantagshantering samt några bra och dåliga exempel.

Som alltid finns all kod i den här artikeln på GitHub!