Exception handling

"It is common sense to take a method and try it. If it fails, admit it frankly and try another. But above all, try something"

Franklin D. Roosevelt

Fejl-situationer Under udførelsen af et program, kan der opstå undtagelser (eng. exceptions), fejl og lignende, der skal håndteres på en fornuftig måde for at programmet kan være robust (at det kan klare enhver situation der måtte opstå). Det der kendetegner disse situationer er, at de forekommer med relativ ringe hyppighed, og derfor ikke rent algoritmisk spiller nogen central rolle i forståelsen af hvad programmet gør.
Forskellige fejlkilder

De problemer, der kan opstå under et programs udførelse kan f.eks. hidrøre fra mistede netværksforbindelser, fejl på harddiske, generelle kommunikationsfejl osv. Hvis man f.eks. skal læse data fra harddisken, kan det være, at de mod forventning ikke findes. Hvis de er der, kan der senere under indlæsningen opstår fejl, fordi harddisken er defekt.

Relativ sjældne Der er mange sådanne problemer som man nødvendigvis må være forberedt på at tage sig af, men som er relativ sjældne.
Vi kan illusterere sammenhængen mellem den "almindelige" kode og fejlhåndtering i en kildetekst med følgende figur.
Figur 1:
Almindelig kode og fejlhåndtering og
Kontrollere det der kan gå galt Til venstre har vi flettet fejlhåndteringen (det røde) ind i den almindelig kode (det blå). Når vi udfører en handling, kontrollerer vi efterfølgende, at der ikke opstod en fejl. Det gør vi for hver af de operationer som kan gå galt. Hvis man ønsker at læse koden med henblik på at forstå hvad der sker, vil man hele tiden blive forstyret af kode til fejlhåndtering. Vi ønsker i virkeligheden kun at se det blå, da det røde ikke bidrager til vores algoritmiske forståelse af programmet.
Samle fejlhåndtering Til højre har vi flyttet fejlhåndteringen ned i bunden, for sig selv. Øverst har vi samlet den almindelig kode, som nu er mere sammenhængende og lettere at læse. Alt i alt en bedre strukturering af koden, men hvordan kan det lade sig gøre. Umiddelbart er det umuligt. Til venstre har vi netop flettet fejlhåndteringen ind i den almindelige kode, fordi vi ikke kan gå videre til det efterfølgende, uden at håndtere fejlen først.
At lave konstruktionen til højre kræver nye sproglige hjælpemidler, og det er dem vi skal studere i dette kapitel - vi skal bruge exceptions!
1. Eksempel: Division med nul
Simpel men atypisk exception Vi vil starte med et simpelt eksempel på en exception. Det er den der opstår når man dividerer med nul - en meget simpel fejl. At fejlen er simpel gør den til et godt eksempel at starte med, men den er til gengæld atypisk. De fleste exceptions opstår nemlig i forbindelse med resourcer som programmet bruger, f.eks. netværk, harddiske osv. Division ned nul har ikke noget med en resource at gøre.
Betragt følgende program:
Source 1:
Division med nul
Main.java
public class Main {

  public static void main( String[] args ) {
    int x = 5 / 0;
  }
}
Exception in thread "main" java.lang.ArithmeticException: / by zero
  at Main.main(Main.java:4)
Vi prøver at dividere 5 med 0, og det går naturligvis galt.
Man ser på runtime fejlmeddelelsen, at der er tale om en exception - en ArithmeticException mere præcist.
Gribe fejl Her har vi har ikke nogen fejlhåndtering, som kan tage sig af problemet. I vores simple eksempel kan det umiddelbart være vanskeligt at se, hvad det i det hele taget vil sige: "at håndtere situationen". Hvis vi forsøger at dividere med nul, er der ikke meget andet at gøre, end at konstatere, at fejlen opstod. For eksemplets skyld vil vi dog "gribe" denne fejl og håndtere den, ved at udskrive en fejlmeddelelse på dansk, i stedet for den systemet selv laver:
Source 2:
try-/catch-blokke
Main.java
public class Main {

  public static void main( String[] args ) {
    try {
      int x = 5 / 0;
    }
    catch ( ArithmeticException e ) {
      System.out.println( "Man kan ikke dividere med nul!" );
    }
  }
}
Man kan ikke dividere med nul!
Her har vi anvendt to sproglige strukturer i Java.
try-blok Først er der en try-blok. En try-blok indeholder kode der kan "gå galt" - som kan kaste en exception. I vores tilfælde kastes der måske en ArithmeticException (i vores simple eksempel er det endog helt sikkert, at det vil ske).
catch-blok Dernæst er der en catch-blok. Der skal altid følge en catch-blok efter en try-blok, og en catch-blok kan ikke stå alene uden en try-blok. Catch-blokken griber en exception; hvis den kastes i try-blokken. Catch-blokken tager én parameter, der angiver hvilken exception catch-blokken skal kunne gribe. Såfremt der kastes en anden slags exception i try-blokken, end den der er nævnt i catch-blokken, vil den ikke gribe den.
I vores catch-blok bruger vi ikke parameteren e til noget, vi noterer os blot, at der er tale om en ArithmeticException.
getMessage() At vi modtager en exception som en parameter til catch-blokken giver os mulighed for at udlede en række ting vedrørende fejlen. Først og fremmest kan man få en tekstuel beskrivelse af exception ved at bruge metoden getMessage() på objektet:
Source 3:
Anvendelse af getMessage()
Main.java
public class Main {

  public static void main( String[] args ) {
    try {
      int x = 5 / 0;
    }
    catch ( ArithmeticException e ) {
      System.out.println( "Man kan ikke dividere med nul!" );
      System.out.println( e.getMessage() );
    }
  }
}
Man kan ikke dividere med nul!
/ by zero
printStackTrace() I forbindelse med debugging kan det være lidt uklart hvor fejlen opstod. Man kan i stedet bruge en anden metode: printStackTrace():
Source 4:
Anvendelse af printStack­Trace()
Main.java
public class Main {

  public static void main( String[] args ) {
    try {
      int x = 5 / 0;
    }
    catch ( ArithmeticException e ) {
      System.out.println( "Man kan ikke dividere med nul!" );
      e.printStackTrace();
    }
  }
}
Man kan ikke dividere med nul!
java.lang.ArithmeticException: / by zero
  at Main.main(Main.java:5)
Her får man filens navn (Main.java), metodens navn (main) og linienummeret (5).
Ikke altid et linienummer Man kan ikke være sikker på at få den sidste af disse tre oplysninger. JVM vil compilere (Med den såkaldte JIT compiler) dele af programmet mens det udføres, og man kan derfor få den lidet informative besked: "Compiled Code" i stedet for linienummeret.
2. Metoder og exceptions
Hvis en exception opstår dybere inde i en række metodekald, giver printStackTrace() en beskrivelse af den kaldesekvens der ledte ind til fejlen:
Source 5:
Exception i kalde-sekvens
Main.java
public class Main {

  public static void b() {
    try {
      int x = 5 / 0;
    }
    catch ( ArithmeticException e ) {
      System.out.println( "Man kan ikke dividere med nul!" );
      e.printStackTrace();
    }
  }
  
  public static void a() {
    b();
  }
  
  public static void main( String[] args ) {
    a();
  }
}
Man kan ikke dividere med nul!
java.lang.ArithmeticException: / by zero
  at Main.b(Main.java:5)
  at Main.a(Main.java:14)
  at Main.main(Main.java:18)
Her kan vi se at fejlen opstår i b() i linie 5, som er blevet kaldt fra a() i linie 14, som er blevet kaldt af main() i linie 18.
Exception fra metode I eksemplet ovenfor, tager vi os af exception i den metode hvor fejlen opstår, men det behøver ikke være sådan. Vi kunne lade det være f.eks. a()'s problem at tage sig af den exception der kastes i b(). Hvis det i stedet er a(), der tager sig af b()'s exception skal koden have følgende udseende:
Source 6:
Metode-kald der kaster exception
Main.java
public class Main {

  public static void b() {
    int x = 5 / 0;
  }
  
  public static void a() {
    try {
      b();
    }
    catch ( ArithmeticException e ) {
      System.out.println( "Man kan ikke dividere med nul!" );
      e.printStackTrace();
    }
  }
  
  public static void main( String[] args ) {
    a();
  }
}
Man kan ikke dividere med nul!
java.lang.ArithmeticException: / by zero
  at Main.b(Main.java:4)
  at Main.a(Main.java:9)
  at Main.main(Main.java:18)
Udskriften fra printStackTrace() er den samme, på nær at linienumrene har ændret sig lidt pga. flytningen af try-/catch-blokken.
Exception opstår i b(), men der er ikke nogen try/catch der håndterer den. Udførelsen af metoden stopper idet exception kastes, og eftersom den ikke håndteres i metoden, kastes den tilbage til det sted metoden blev kaldt. I a(), hvor b() bliver kaldt, er der en try/catch der griber exception, idet den kommer tilbage fra b(). Fordi a() håndterer exception hører main() aldrig om den.
I forbindelse med printStackTrace(), observerer man, at exception husker hvorfra den blev kastet. Informationerne i den liste, som udskrives, starter det sted exception bliver kastet, og ikke først fra det sted den håndteres.
Hvordan bliver en exception kastet? - hvordan sker det? - hvor sker det?
Kastes af JVM Den ArithmeticException der kastes i vores eksempel, bliver kastet af JVM. Det skyldes at det er en sprogbetinget exception - det er en grundlæggende del af sproget, at man ikke kan dividere med nul.
I de fleste tilfælde vil exceptions dog blive kastet helt bevidst af Java-kode - af selve programmet.
Vi kan selv kaste en ArithmeticException, hvis vi skulle ønske det:
Source 7:
Selv kaste en exception
Main.java
public class Main {

  public static void b() {
    throw new ArithmeticException();
  }
  
  public static void a() {
    try {
      b();
    }
    catch ( ArithmeticException e ) {
      e.printStackTrace();
    }
  }
  
  public static void main( String[] args ) {
    a();
  }
}
java.lang.ArithmeticException
  at Main.b(Main.java:4)
  at Main.a(Main.java:9)
  at Main.main(Main.java:17)
Det er naturligvis lidt kunstigt at kaste en ArithmeticException ganske umotiveret, men det illustrerer teknikken. Vi har samtidig fjernet vores egen linie, der fortalte om en division med nul, da det ikke længere er tilfældet.
I Java er en exception et objekt I b() kastes (eng. throw) der en AritmeticException, og ved at læse linien bliver det tydeligt, at en exception er et objekt. Der bliver lavet en instans af ArithmeticException og man sætter det reserverede ord throw foran. Derved kastes den exception man lige har instantieret.
Tekstuel beskrivelse af exception Hvis man ser efter, i udskriften, vil man bemærke at teksten "/ by zero" mangler. Det skyldes at en ArithmeticException kan kastes i flere forskellige situationer, bla. når der divideres med nul. Derfor tager en af konstruktorerne i ArithmeticException en tekststreng som parameter; hvilket giver mulighed for at knytte en tekstuel beskrivelse til exception. Det er den, der returneres af getMessage(), og det er den der bla. udskrives af printStackTrace().
Lad os prøve at angive en tekst der beskriver vores exception:
Source 8:
Tekstuel beskrivelse af exception
Main.java
public class Main {

  public static void b() {
    throw new ArithmeticException( "Umotiveret exception" );
  }
  
  public static void a() {
    try {
      b();
    }
    catch ( ArithmeticException e ) {
      e.printStackTrace();
    }
  }
  
  public static void main( String[] args ) {
    a();
  }
}
java.lang.ArithmeticException: Umotiveret exception
  at Main.b(Main.java:4)
  at Main.a(Main.java:9)
  at Main.main(Main.java:17)
Man ser her at vores beskrivende tekst, i forbindelse med vores ArithmeticException, optræder i udskriften fra printStackTrace().
3. Exception klasser
Vi har brugt den simple ArithmeticException som introducerende eksempel, men der findes mange andre.
Mange exceptions Alle exceptions er organiseret i et klasse-hierarki, med utallige exceptions (der er alene 127 filer i Java's standard packages der opfylder *Exception.java). Følgende er et lille udvalg af disse klasser.
Figur 2:
Lille del af klasse-hierarkiet for exceptions
Alle exceptions er subklasser til den fælles superklasse Exception.
Gamle kendinge Vi har her udvalgt nogle gamle kendinge; hvis man da ellers er stødt på exceptions i forbindelse med de ting man tidligere har lavet.
ArithmeticException har vi lige set i forbindelse med division med nul.
NulPointerException optråder når man forsøger at sende en request vi en reference som er null, dvs. ikke refererer til noget objekt. ArrayIndexOutOfBoundsException får man hvis et index i et array ligger unden for arrayets index-interval.
IOException har vi taget med fordi den optræder i forbindelse med streams og filer. To emner der er naturlige at beskæftige sig med i forlængelse af dette kapitel om exceptions.
Nu da vi ved, at der findes forskellige exceptions, kan vi se på muligheden for at have flere catch-blokke i forbindelse med én try-blok. Betragt følgende eksempel:
Source 9:
Flere catch-blokke til samme try-blok
Main.java
public class Main {

  public static void main( String[] args ) {
    try {
      int[] t = new int[ 5 ];
      
      t[ 20 ] = 3;
      
      int x = 5 / 0;
    }
    catch ( IndexOutOfBoundsException e ) {
      System.out.println( "Der er problemer med et index!" );
      e.printStackTrace();
    }
    catch ( ArithmeticException e ) {
      System.out.println( "Der er et matematisk problem!" );
      e.printStackTrace();
    }
  }
}
Der er problemer med et index!
java.lang.ArrayIndexOutOfBoundsException: 20
  at Main.main(Main.java:7)
Der opstår her en exception, idet 20 er for stort et index. Denne exception gribes af den første catch-blok som udskriver en fejlmeddelelse.
Der er en række ting man skal bemærke her.
Stopper udførelse Det første er at udførelsen af try-blokken stopper i samme øjeblik der kastes en exception. Vi kommer aldrig til at foretage divisionen med nul, da der kastes en exception i linien før.
Griber også subklasser Den anden ting er, at parametertypen til en catch-blok ikke alene fanger exceptions af den pågældende type, men også exceptions af alle subklasser til parametertypen. I vores eksempel har vi anvendet IndexOutOfBoundsException. Selv om det er en ArrayIndexOutOfBoundsException der kastes, bliver den alligevel grebet af IndexOutOfBoundsException, da før nævnte er en subklasse af sidst nævnte (se evt. figur 2).
Først til mølle Den tredie og sidste ting er, at den første catch der griber exception er den eneste der får den. Selv om efterfølgende catch-blokke ville passe til den pågældende exception er det "først til mølle"-princippet der gælder. Catch-blokkene kommer til i den rækkefølge de står, og den første der kan griber den, får den. Det skal dog bemærkes at det er sjældent denne tredie egenskab ved catch-blokke anvendes.
4. Kaste videre
En catch-blok kan vælge at kaste en exception videre. F.eks.:
Source 10:
Kaste en excep­tion videre
try {
  ...
}
catch ( ... e ) {
  ...
  throw e;
}
Da vi i catch-blokken befinder os uden for try-blokken, vil denne exception ikke blive grebet af nogen af de catch-blokke der er tilknyttet try-blokken. Dermed vil exceptionen blive kastet videre, tilbage til det sted metoden blev kaldt (dog ikke hvis det er en nestet try/catch - se senere).
5. throws clause
Betragt følgende eksempel:
Source 11:
Throws clause
Main.java
import java.io.IOException;

public class Main {

  public static void lavNogetIO() throws IOException {
    throw new IOException( "Input/Output fejl" );
  }
  
  public static void main( String[] argv ) {
    try {
      lavNogetIO();
    }
    catch ( IOException e ) {
      e.printStackTrace();
    }
  }
}
java.io.IOException: Input/Output fejl
  at Main.lavNogetIO(Main.java:6)
  at Main.main(Main.java:11)
Runtime­Exceptions behøver ikke Med rødt er markeret en såkaldt throws clause. En throws clause fortæller at lavNogetIO() muligvis kaster en IOException. Vi har tidligere haft metoder, som har kastet exceptions uden at der skulle være en sådan throws clause. Det skyldes at en RuntimeException (og subklasser af denne) ikke behøver at blive anført i en throws clause — det gør Java selv.
Med mindre der er tale om en RuntimeException skal vi derfor altid anføre en throws clause. I virkeligheden er det sjældent man vil gribe en RuntimeException, idet de normalt ikke opstår, hvis programmet er debugget ordentligt. Derfor vil man næsten altid se throws clauses, hvis en metode ikke håndterer en exception.
Hvis metoden kan kaste flere forskellige exceptions, anføres disse adskilt af komma. F.eks.:
Source 12:
throws med flere exceptions
public ... f( ... ) throws EnException, EnAndenException {
  ...
}
6. Nestede try-blokke
Det er muligt at neste try/catch-blokke, som det er gjort i følgende kode:
Source 13:
Nested try-blokke
try {
  // ydre try

  try {
    // indre try
  }
  catch ( ... e ) {
    // indre catch
  }
}
catch ( ... e ) {
  // ydre catch
}
Det giver mulighed for at gribe en exception inde i den indre try-blok uden at den når ud til den ydre catch-blok.
Det er en mulighed der sjældent bruges. Det er godt det samme, for det bryder med det grundlæggende designmål for exceptions (fra figur 1), med at samle "almindelig" kode og fejlhåndtering for sig.
7. Finally-blok
Når en exception kastes, afbrydes udførelsen af metoden. Hvis exception kastes i en try-blok kan den fanges af en passende catch-blok, men selve metoden er sat ud af spillet - den får ikke mulighed for at foretage sig mere.
Uanset om der kastes en exception eller ej I forbindelse med try/catch er det muligt at føje en extra blok til - en finally-blok. En finally-blok vil altid blive udført. Det betyder at indholdet af finally-blokken udføres uanset om der kastes en exception eller ej. Det er kode, som også vil blive udført under normale omstændigheder - når der ikke kastes en exception.
Man kan illustrere det med følgende pseudokode:
Source 14:
Finally-blok
try {
  // udføres indtil der evt. sker en exception
}
catch ( ... e ) {
  // udføres ved en exception
}
finally {
  // udføres uanset om der var en exception eller ej
}
Man anvender normalt en finally-blok, når man vil sikre sig, at en resource som er blevet allokeret i try-blokken frigives inden metoden forlades.
8. Egne exceptions
Det er muligt at lave egne exceptions ved at nedarve fra Exception. Lad os se et eksempel:
Source 15:
Titanic synker
StortProblemException.java
public class StortProblemException extends Exception {
    
  public StortProblemException( String msg ) {
    super( msg );
  }
}
ExceptionTester.java
public class ExceptionTester {
  
  public void atlanten() throws StortProblemException {
    throw new StortProblemException( "Titanic synker" );
  }
  
  public void england() {
    try {
      atlanten();
    }
    catch ( StortProblemException e ) {
      e.printStackTrace();
    }
  }
}
Main.java
public class Main {
  
  public static void main( String[] args ) {
    ExceptionTester tester = new ExceptionTester();
    tester.england();
  }
}
StortProblemException: Titanic synker
  at ExceptionTester.atlanten(ExceptionTester.java:4)
  at ExceptionTester.england(ExceptionTester.java:9)
  at Main.main(Main.java:5)
Vi laver her en konstruktor til StortProblemException, der giver mulighed for at knytte en tekstuel beskrivelse til denne exception.
9. Flow-kontrol med exceptions
Gør det ikke! Lad det være sagt med det samme: Dette er et "gør det ikke"-afsnit.
Man kan lave nogle skæge former for flow-kontrol med exceptions, der udbygger ens forståelse af dem, men gør det ikke! Exceptions er ikke lavet til dette formål, og de går koden vanskeligere at læse.
Et klassisk eksempel på flowkontrol med exceptions er følgende:
Source 16:
Flow-control med exception
Main.java
public class Main {

  public static void main( String[] args ) {
    int[] tabel = { 5, 2, 7, 3, 9, 8, 6, 0, 1, 4 };
    int sum=0;
    
    try {
      for ( int i=0; true; i++ )
        sum += tabel[i];
    }
    catch ( ArrayIndexOutOfBoundsException e ) {
      System.out.println( "Sum = " + sum );
    }
  }
}
Sum = 45
Her undlader vi bevidst at checke om i bliver for stort et index. I stedet afventes den sikre exception, som kommer når i bliver for stor.
Ét check i stedet for to Hver gang vi laver et opslag i et array laver systemet selv et check. Ved at lade iterationen stoppe ved en exception, bliver der kun ét check, selve index-checket ved opslag. Hvis vi selv lavede et check med i < tabel.length, vil det være dobbelt arbejde.
Det er meget fristende at bruge exceptions til flowkontrol - specielt hvis man er fikseret på effektivitet, men det har en pris: Det er meget ulæseligt.
Repetitionsspørgsmål
1 Hvilken struktur har et program uden exceptions, hvis det skal håndtere fejl?
2 Hvilken struktur har et program med exceptions?
3 Hvorfor er division med nul en atypisk exception?
4 Hvad er en try-blok?
5 Hvad er en catch-blok?
6 Hvad er sammenhængen mellem en try-blok og en catch-blok?
7 Alle exceptions har en metode getMessage(). Hvad bruges denne metode til?
8 Hvad gør metoden printStackTrace().
9 Hvad skyldes det, at man ikke altid får et linienummer i forbindelse med printStackTrace()?
10 Hvad sker der, når en exception kastes i en metode, men ikke gribes i metoden?
11 Hvem kaster RuntimeExceptions?
12 Hvad er en exception konkret i Java?
13 De fleste exceptions har en konstruktor, der tager en tekststreng som parameter. Hvad betyder denne parameter og hvordan får man fat i teksstrengen igen?
14 Hvornår kastes en NulPointerException?
15 Hvornår kastes en ArrayIndexOutOfBoundsException?
16 Hvordan fungerer flere catch-blokke i forbindelse med en try-blok?
17 Hvilken betydning har subklasser af exceptions i forbindelse med en catch-blok?
18 Hvordan kan man kaste en exception videre, fra en catch-blok?
19 Hvornår bruges en throws clause?
20 Hvad er nestede try-blokke, og hvorfor bør de så vidt muligt undgåes?
21 Hvad er en finally-blok?
22 Hvordan laver man egne exceptions?
23 Hvorfor bør man ikke lave flow-kontrol med exceptions?
24 Hvorfor kan det være fristende at lave flow-kontrol med exceptions?
Svar på repetitionsspørgsmål
1 Koden er en blanding af skiftevis "almindelig" kode og fejlhåndtering.
2 Den almindelige kode og fejlhåntering er adskildt, og dermed ikke sammenflettet.
3 Fordi de fleste exceptions, man kommer til at arbejde med, vedrører resourcer.
4 Indholdet af en try-blok kaster muligvis en exception.
5 En catch-blok indeholder fejlhåndtering.
6 En try-blok har altid en catch-blok og omvendt. En catch-blok laver fejlhåndtering for den try-blok den er tilknyttet.
7 Metoden returnerer en tekstuel beskrivelse af exception.
8 Den udskriver kalde-sekvensen, der førte frem til det sted exception opstod.
9 Fordi JVM i en vis udstrækning oversætter programmet til maskinkode, vha. JIT-compileren, mens det afvikles.
10 Så kastes den videre tilbage til den metode der kaldte den.
11 De kastes af JVM.
12 En exception er et objekt.
13 Tekststrengen er en tekstuel beskrivelse af exception, og man kan få den igen ved et kald af getMessage.
14 Når man forsøger at sende en request via en reference som ikke refererer til et objekt (er null).
15 Når man forsøger at tilgå en indgang i et array med et index, der ligger udenfor det erklærede interval. Dvs. når index er enten for stort eller lille.
16 Efter "Først til mølle"-princippet. Exception bliver kun grebet af én catch-blok, nemlig den første, hvis parameter passer til den kastede exception.
17 En catch-blok kan gribe en exception, der er en instans af dens formelle parameters type eller en subklasse af denne.
18 Ved sætningen throw e;, hvis den grebne exception benævnes e.
19 Når en metode kaster en exception, som ikke er en RuntimeException. At en metode kaster en exception vil sige, at den ikke selv håndterer en kastet exception.
20 Det er en try-blok inden i en anden try-blok. Det bør så vidt muligt undgåes, da det forringer læsbarheden, samt strider mod det grundlæggende designmål for exceptions (se evt. figur 1).
21 En blok, der kan komme efter catch-blokkene. Indholdet af finally-blokken bliver altid udført, uanset om der kastes en exception eller ej.
22 Ved at nedarve fra Exception (eller en subklasse af denne).
23 Fordi det forringer læsbarheden betydeligt.
24 Fordi man i visse situationer kan gøre koden hurtigere.