javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen

14 Exceptions og køretidsfejl

Indhold:

Kapitlet forudsættes i resten af bogen og evnen til at kunne læse et stakspor er vigtig, når man skal finde fejl i sit program.

Forudsætter kapitel Fejl: Henvisningskilde ikke fundet, Fejl: Henvisningskilde ikke fundet (kapitel Fejl: Henvisningskilde ikke fundet, Fejl: Henvisningskilde ikke fundet er en fordel).

Som programmør skal man tage højde for fejlsituationer, som kan opstå, når programmet udføres. Det gælder f.eks. inddata fra brugeren, der kan være anderledes, end man forventede (brugeren indtaster f.eks. bogstaver et sted, hvor programmet forventer tal) og adgang til ydre enheder, som kan være utilgængelige, f.eks. filer, printere og netværket.

Hvis programmet prøver at udføre en ulovlig handling, vil der opstå en  exception (en undtagelse) og programudførelsen vil blive afbrudt på det sted, hvor undtagelsen opstod.

Lad os undersøge nærmere, hvad der sker. Herunder prøver vi at indeksere ud over en listes grænser:

  1. import java.util.ArrayList;
  2. 
      
  3. public class SimpelException
  4. {
  5.   public static void main(String[] arg)
  6.   {
  7.     System.out.println("Punkt A");       // punkt A
  8.     ArrayList l = new ArrayList();
  9.     System.out.println("Punkt B");       // punkt B
  10.     l.get(5);
  11.     System.out.println("Punkt C");       // punkt C
  12.   }
  13. }

Punkt A
Punkt B
java.lang.ArrayIndexOutOfBoundsException: 5 >= 0
  at java.util.ArrayList.get(ArrayList.java:417)
  at SimpelException.main(SimpelException.java:10)
Exception in thread "main"

Når vi kører programmet, kan vi se, at det stopper mellem punkt B og C med en fejl:

java.lang.ArrayIndexOutOfBoundsException: 5 >= 0

Den efterfølgende kode udføres ikke og vi når aldrig punkt C.

Programudførelsen afbrydes der, hvor der opstår en exception

I dette kapitel vil vi illustrere, hvordan exceptions opstår og hvordan de håndteres. Af plads- og overskuelighedshensyn er eksemplerne ret små og exceptionhåndtering derfor ikke specielt nødvendig. Man skal forestille sig større situationer, hvor der opstår fejl, der ikke lige er til at gennemskue (i dette eksempel kunne der være meget mere kode ved punkt B).

Man kan tænke på exceptions som en slags protester. Indtil nu har vi regnet med, at objekterne pænt "parerede ordre", når vi gav dem kommandoer eller spørgsmål (kaldte metoder). Fra nu af kan metoderne "spænde ben" og afbryde programudførelsen, hvis situationen er uacceptabel.

Det er det, som get(5) på den tomme ArrayList gør: Som svar på "giv mig element nummer 5" kaster den ArrayIndexOutOfBoundsException og siger "5 >= 0", dvs. "det kan jeg ikke, for 5 er større end antallet af elementer i listen, som er 0!".

14.1 Almindelige exceptions

Udover ArrayIndexOutOfBoundsException som beskrevet ovenfor kan der opstå en række andre fejlsituationer. De mest almindelige er kort beskrevet nedenfor.

Der opstår en NullPointerException, hvis man kalder metoder på en variabel, der er null (en reference/pointer, der ikke peger på et objekt):

    ArrayList l = null;
    l.add("x");

Exception in thread "main" java.lang.NullPointerException
        at SimpelException.main(SimpelException.java:6)

Hvis man laver aritmetiske udregninger, kan ArithmeticException opstå, f.eks. ved division med nul:

    int a = 5;
    int b = 0;
    System.out.print(a/b);

Exception in thread "main" java.lang.ArithmeticException: / by zero
        at SimpelException.main(SimpelException.java:7)

ClassCastException opstår, hvis man prøver at typekonvertere en objektreference til en type, som objektet ikke er. F.eks. hvis man caster en Gade til et Rederi:

    Felt f = new Gade("Gade 2", 10000, 400, 1000);
    Rederi r = (Rederi) f;

Exception in thread "main" java.lang.ClassCastException: Gade
        at SimpelException.main(SimpelException.java:6)

14.2 At fange og håndtere exceptions

Exceptions kan fanges og håndteres. Det gøres ved at indkapsle den kritiske kode i en try-blok og behandle eventuelle exceptions i en catch-blok:

  try 
  {
    ...                     // programkode hvor der er en risiko
    ...                     // for at en exception kastes
  }
  catch (Exceptiontype u)   // den slags exceptions, der skal fanges
  {
    ...                     // kode som håndterer fejl af
    ...                     // typen Exceptiontype
  }
  ...                       // dette udføres både der ikke opstod exceptions
  ...                       // og hvis der opstod fejl af typen Exceptiontype

Når programmet kører normalt, springes catch-blokken over. Hvis der opstår exceptions i try-blokken, hoppes ned i catch-blokken, der håndterer fejlen og derefter udføres koden efter catch.

Exceptiontypen bestemmer, hvilke slags exceptions der fanges1.

Man kan fange alle slags ved at angive en generel undtagelse, f.eks. Exception, eller kun fange en bestemt slags exceptions, f.eks. ArrayIndexOutOfBoundsException.

Ser vi på vores ArrayList-eksempel igen, kunne det med exceptionhåndtering se ud som:

  1. import java.util.ArrayList;
  2. public class SimpelException2
  3. {
  4.   public static void main(String[] arg)
  5.   {
  6.     System.out.println("Punkt A");               // punkt A
  7.     try 
  8.     {
  9.       ArrayList l = new ArrayList();
  10.       System.out.println("Punkt B");           // punkt B
  11.       l.get(5);
  12.       System.out.println("Punkt C");           // punkt C
  13.     }
  14.     catch (Exception u)
  15.     {
  16.       System.out.println("Der opstod en exception!");
  17.     }
  18.     System.out.println("Punkt D");             // punkt D
  19.   }
  20. }

Punkt A
Punkt B
Der opstod en exception!
Punkt D

Læg mærke til, at punkt C (der ligger i try-blokken, efter at exceptionen opstod) ikke bliver udført. Punkt D (efter catch-blokken) bliver udført under alle omstændigheder.

14.2.1 Exceptions og deres stakspor

En exception er, ligesom alt andet i Java, repræsenteret ved et objekt. En reference til dette Exception-objekt overføres til catch-blokken og det har nyttige informationer om fejlen.

Metoden printStackTrace() udskriver et stakspor (eng.: stack trace), der beskriver de metodekald, der førte til, at den opstod:

    ...
    catch (Exception e)
    {
      System.out.println("Der opstod en exception!");
      e.printStackTrace();
    }
    ...

Punkt A
Punkt B
Der opstod en exception!
java.lang.ArrayIndexOutOfBoundsException: 5 >= 0
        at java.util.ArrayList.get(ArrayList.java:441)
        at SimpelException2.main(SimpelException2.java:11)
Punkt D

Staksporet er nyttigt, når man skal finde ud af, hvordan fejlen opstod. Det viser præcist, at fejlen opstod i get()-metoden i ArrayList, som blev kaldt fra SimpelException2.java i main()-metoden linje 11.

14.3 Exceptions med tvungen håndtering

Indtil nu har compileren accepteret vores programmer, hvad enten vi håndterede eventuelle exceptions eller ej, dvs. det var helt frivilligt, om vi ville tage højde for de mulige fejlsituationer.

Imidlertid er der nogle handlinger, der kræver håndtering, bl.a.:

Når programmøren kalder metoder, der kan kaste disse exceptions, skal han fange dem.

14.3.1 Fange exceptions eller sende dem videre

Som eksempel vil vi indlæse en linje fra tastaturet og udskrive den på skærmen:

import java.io.*;
public class TastaturbrugerFejl 
{
  public static void main(String[] arg) 
  {
    BufferedReader ind = new BufferedReader(new InputStreamReader(System.in));
    String linje;
    linje = ind.readLine();
    System.out.println("Du skrev: "+linje);
  }
}

Metoden readLine() læser en linje fra datastrømmen (tastaturet), men når den udføres, kan der opstå en IOException2.

Compileren tvinger os til at tage højde for den mulige undtagelse i programudførslen:

TastaturbrugerFejl.java:8: unreported exception java.io.IOException; must be caught or declared to be thrown
    linje = ind.readLine();

Fejlmeddelelsen ville på dansk lyde: "I TastaturbrugerFejl.java linje 8 er der en uhåndteret IOException; den skal fanges, eller det skal erklæres, at den bliver kastet":

Vi er altså tvunget til:

enten 1) at fange fejlen ved at indkapsle den farlige kode i en try-catch-blok:

    try {
      linje = ind.readLine();
      System.out.println("Du skrev: "+linje);
    } catch (Exception u) {
      u.printStackTrace();
    }

eller 2) erklære, at den bliver kastet, dvs. at den kan opstå i main()-metoden. Det gør man med ordet throws:

  public static void main(String[] arg) throws IOException

Det sidste signalerer, at hvis undtagelsen opstår, skal metoden afbrydes helt og kalderen må håndtere fejlen (i dette tilfælde er det systemet, der har kaldt main(), men oftest vil det være os selv).

Exceptions med tvungen håndtering skal enten fanges (med try-catch i metodekroppen) eller sendes videre til kalderen (med throws i metodehovedet)

14.3.2 Konsekvenser af at sende exceptions videre

Det har konsekvenser at sende exceptions med tvungen håndtering videre, for da er kalderen tvunget til at håndtere dem.

Eksempel: Lad os sige, at vi har uddelegeret læsningen fra tastaturet til en separat Tastatur-klasse, der kan læse en linje fra tastaturet med læsLinje() eller læse en linje og omsætte den til et tal med læsTal():

  1. import java.io.*;
  2. 
      
  3. public class Tastatur
  4. {
  5.   private BufferedReader ind;
  6. 
      
  7.   public Tastatur()
  8.   {
  9.     ind = new BufferedReader(new InputStreamReader(System.in));
  10.   }
  11. 
      
  12.   public String læsLinje()
  13.   {
  14.     try {
  15.       String linje = ind.readLine();
  16.       return linje;
  17.     } catch (IOException u)
  18.     {
  19.       u.printStackTrace();
  20.     }
  21.     return null;
  22.   }
  23. 
      
  24.   public double læsTal()
  25.   {
  26.     String linje = læsLinje();
  27.     return Double.parseDouble(linje);
  28.   }
  29. }

Herover fanger vi IOException ved dens "rod" i læsLinje().

Den kunne gøres simplere ved at fjerne håndteringen og erklære IOException kastet:

  public String læsLinje() throws IOException
  {
    String linje = ind.readLine();
    return linje;
  }

Nu sender læsLinje() exceptionsne videre, så nu er det kalderens problem at håndtere den.

Vi kalder selv metoden fra læsTal(), så her er vi nu enten nødt til at fange eventuelle exceptions:

  public double læsTal()
  {
    try {
      String linje = læsLinje();
      return Double.parseDouble(linje);
    } catch (IOException u)
    {
      u.printStackTrace();
    }
    return 0;
  }

... eller igen sende dem videre.

Herunder er Tastatur igen, men IOException kastes nu videre fra begge metoder.

import java.io.*;

public class TastaturKasterExceptions
{
  private BufferedReader ind;

  public TastaturKasterExceptions()
  {
    ind = new BufferedReader(new InputStreamReader(System.in));
  }

  public String læsLinje() throws IOException
  {
    String linje = ind.readLine();
    return linje;
  }

  public double læsTal() throws IOException
  {
    String linje = læsLinje();
    return Double.parseDouble(linje);
  }
}

Om man skal fange exceptions eller lade dem "ryge videre" afhænger af, om man selv kan håndtere dem fornuftigt, eller kalderen har brug for at få at vide, at noget gik galt.

Hvad sker der f.eks. i Tastatur, hvis der opstår en exception i læsLinje() kaldt fra læsTal()?

Jo, læsLinje() returnerer en null-reference til læsTal(), der sender denne reference til parseDouble(), der sandsynligvis "protesterer" med en NullPointerException, for man kan ikke konvertere null til et tal. Der opstår altså en følgefejl, fordi vi fortsætter, som om intet var hændt.

I dette tilfælde må TastaturKasterExceptions altså siges at være bedst, selvom den altså giver kalderen mere arbejde.

14.4 Præcis håndtering af exceptions

Det kan have væsentlige konsekvenser, på hvilket niveau exceptionsne fanges, selv inden for samme metode.

Lad os bruge Tastatur til at lave et lille regneprogram, der lægger tal sammen. Vi spørger først brugeren, hvor mange tal det skal være (med læsTal()) og derefter kan han taste tallene ind. Til sidst spørger vi, om han vil prøve igen.

  1. public class SumMedTastatur
  2. {
  3.   public static void main(String[] arg)
  4.   {
  5.     Tastatur t = new Tastatur();
  6.     boolean stop = false;
  7.     try 
  8.     {
  9.       while (!stop)
  10.       {
  11.         System.out.print("Hvor mange tal vil du lægge sammen? ");
  12.         double antalTal = t.læsTal();
  13.         double sum = 0;
  14. 
      
  15.         for (int i=0; i<antalTal; i=i+1)
  16.         {
  17.           System.out.print("Indtast tal: ");
  18.           sum = sum + t.læsTal();
  19.         }
  20.         System.out.println("Summen er: "+sum);
  21.         System.out.print("Vil du prøve igen (j/n)? ");
  22.         if ("n".equals(t.læsLinje())) stop = true; // undersøg om det er "n"
  23.       }
  24.     }   catch (Exception u) {
  25.       System.out.println("Der opstod en exception!");
  26.       u.printStackTrace();
  27.     }
  28.   }
  29. }

Hvor mange tal vil du lægge sammen? 2
Indtast tal: 1
Indtast tal: 2
Summen er: 3.0
Vil du prøve igen (j/n)? j
Hvor mange tal vil du lægge sammen? 3
Indtast tal: 1
Indtast tal: 3
Indtast tal: 5
Summen er: 9.0
Vil du prøve igen (j/n)? n

Brugeren taster og taster ... men hvad sker der, hvis han taster forkert?

Hvor mange tal vil du lægge sammen? 3
Indtast tal: 1
Indtast tal: 17xxøføf
Der opstod en exception!
java.lang.NumberFormatException: 17xxøføf
        at java.lang.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1182)
        at java.lang.Double.parseDouble(Double.java:190)
        at Tastatur.læsTal(Tastatur.java:27)
        at SumMedTastatur.main(SumMedTastatur.java:18)

Her opstod en anden exception: 17xxøføf kunne ikke konverteres til et tal. Igen er staksporet nyttigt til at finde fejlen (læst nedefra og op viser det, at main() i linje 18 kaldte læsTal(), der i linje 27 kaldte parseDouble(), der er en del af standardbiblioteket3).

Programmet afslutter, da try-catch-blokken er yderst. En smartere opførsel ville være, at den igangværende sum blev afbrudt og brugeren blev bedt om at starte forfra.

Det kan vi opnå ved at have try-catch inde i while-løkken:

public class SumMedTastatur2
{
  public static void main(String[] arg)
  {
    Tastatur t = new Tastatur();
    boolean stop = false;

    while (!stop)
    {
      System.out.print("Hvor mange tal vil du lægge sammen? ");
      try 
      {
        double antalTal = t.læsTal();
        double sum = 0;

        for (int i=0; i<antalTal; i=i+1)
        {
          System.out.print("Indtast tal: ");
          sum = sum + t.læsTal();
        }
        System.out.println("Summen er: "+sum);
      }   catch (Exception u) {
        System.out.println("Indtastningsfejl - " + u);
      }
      System.out.print("Vil du prøve igen (j/n)? ");
      if ("n".equals(t.læsLinje())) stop = true;
    }
  }
}

Hvor mange tal vil du lægge sammen? 5
Indtast tal: 1
Indtast tal: x2z
Indtastningsfejl - java.lang.NumberFormatException: x2z
Vil du prøve igen (j/n)? j
Hvor mange tal vil du lægge sammen? 3
Indtast tal: 1200
Indtast tal: 1
Indtast tal: 1.9
Summen er: 1202.9
Vil du prøve igen (j/n)? n

Hvis en exception opstår, smides den aktuelle sum væk og programmet spørger brugeren, om han vil prøve igen med en ny sum (efter catch-blokken). Svarer han ja, starter programmet forfra i while-løkken.

Med omhyggelig placering af try-catch-blokke kan man altså kontrollere, præcis hvordan programmet skal opføre sig i fejlsituationer:

Kode, hvori der kan opstå en exception og efterfølgende afhængig kode, bør være i samme try-catch-blok

I eksemplet ovenfor finder vi først antallet af tal med læsTal(). Hvis det går galt, giver det heller ikke mening at gå i gang med at udregne en sum, da vi ikke ved, hvor mange tal den skal bestå af.

14.5 Fange flere slags exceptions

Ovenfor har vi behandlet alle exceptions ens. Det er muligt at hægte flere catch-sætninger med hver sin type exception på samme try-blok.

    try {
      ...
    }
    catch (NumberFormatException e1)
    {
      System.out.println("Inddata kunne ikke læses: "+e1);
    } 
    catch (IOException e2)
    {
      System.out.println("Inddata kunne ikke læses: "+e2);
    }
    catch (NullPointerException e3)
    {
      e3.printStackTrace();
    }

Med Java 7 er det muligt at lade catch-blokke håndtere flere forskellige slags exceptions:

    try {
      ...
    }
    catch (NumberFormatException | IOException e12)
    {
      System.out.println("Inddata kunne ikke læses: "+e12);
    } 
    catch (NullPointerException e3)
    {
      e3.printStackTrace();
    }

Alle exception-klasser arver fra Exception og man kan fange enhver exception ved, at fange deres fælles superklasse. Fejlhåndteringen bliver så generel, ligegyldigt hvilken type exception der opstod (men husk at udskrive staksporet, så du kan se hvad der skete!)

    try {
      ...
    }
    catch (Exception u)
    {
      System.out.println("Fejl:");
      u.printStackTrace();
    }

14.6 Resumé

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

14.7 Opgaver

  1. Flyt try og catch i SumMedTastatur2 sådan, at programmet smider den aktuelle sum væk og prøver igen uden at spørge brugeren (gør det ved kun at bytte om på linjerne).

  2. Ret programmet, så det tæller antallet af gange, en sum blev påbegyndt.
    Det er klart, at man skal tælle en variabel op, men hvor skal optællingen placeres?

  3. Ret programmet, så det også tæller antallet af gange, en sum blev korrekt afsluttet.

  4. Ændr sådan, at programmet smider den aktuelle indtastning væk, men lader brugeren fortsætte med at regne på den samme sum (vink: Lav for-løkken om til en while-løkke og placér optællingen sådan, at den kun udføres, hvis indtastningen går godt).

14.8 Avanceret

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

14.8.1 Fange Throwable

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

14.8.2 Selv kaste exceptions (throw)

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

14.8.3 try - finally

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

14.8.4 Selv definere exceptiontyper

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

1Andre typer exceptions fanges ikke. Hvis de opstår, afbrydes programmet ligesom uden try-catch.

2Det er ikke så sandsynligt netop for tastaturindlæsning, men klasserne, vi bruger, er beregnet til at læse vilkårlige datastrømme, måske endda over netværket, og her vil IOException opstå, f.eks. hvis datastrømmen er blevet lukket, der ikke er mere data at læse, eller der er opstået en netværksfejl. Scanner-klassen, beskrevet i afsnit Fejl: Henvisningskilde ikke fundet, er mere velegnet til netop tastaturindlæsning.

3Selvom det er mindre væsentligt, kan man også se, at parseDouble() faktisk har kaldt en anden metode, nemlig readJavaFormatString().

javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen
http://javabog.dk/ - Forord af Jacob Nordfalk.
Licens og kopiering under Åben Dokumentlicens (ÅDL) hvor intet andet er nævnt (79% af værket).

Ønsker du at se de sidste 21% af dette værk (267325 tegn) skal du købe bogen. Så får du pæne figurer og layout, stikordsregister og en trykt bog med i købet.