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

14 Undtagelser 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 4, Definition af klasser (kapitel 5, Nedarvning 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 undtagelse (eng.: exception) 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:

import java.util.*;

public class SimpelUndtagelse
{
  public static void main(String[] arg)
  {
    System.out.println("Punkt A");       // punkt A
    ArrayList l = new ArrayList();
    System.out.println("Punkt B");       // punkt B
    l.get(5);
    System.out.println("Punkt C");       // punkt C
  }
}

Punkt A
Punkt B
java.lang.ArrayIndexOutOfBoundsException: 5 >= 0
  at java.util.ArrayList.get(ArrayList.java:417)
  at SimpelUndtagelse.main(SimpelUndtagelse.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, når der opstår en undtagelse

I dette kapitel vil vi illustrere, hvordan undtagelser opstår og hvordan de håndteres. Af plads- og overskuelighedshensyn er eksemplerne ret små og undtagelseshå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å undtagelser 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 undtagelser

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

Der opstår en undtagelse af typen NullPointerException, hvis man kalder metoder på en variabel, der ingen steder refererer hen (en objektreference, der er null):

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

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

Hvis man laver aritmetiske udregninger, kan der opstå undtagelsen ArithmeticException, 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 SimpelUndtagelse.main(SimpelUndtagelse.java:7)

ClassCastException opstår, hvis man prøver at typekonvertere en objektreference til en type, som objektet ikke er, f.eks. 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 SimpelUndtagelse.main(SimpelUndtagelse.java:6)

14.2 At fange og håndtere undtagelser

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

  try 
  {
    ...                     // programkode hvor der er en risiko
    ...                     // for at en undtagelse opstår
  }
  catch (Undtagelsestype u) // undtagelsen der skal fanges, f.eks. Exception
  {
    ...                     // kode som håndterer fejl af
    ...                     // typen Undtagelsestype
  }
  ...                       // dette udføres både hvis ingen undtagelse opstod
  ...                       // og hvis der opstod fejl af typen Undtagelsestype

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

Undtagelsestypen bestemmer, hvilke slags undtagelser der fanges1.

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

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

import java.util.*;
public class SimpelUndtagelse2
{
  public static void main(String[] arg)
  {
    System.out.println("Punkt A");            // pkt. A
    try 
    {
      ArrayList l = new ArrayList();
      System.out.println("Punkt B");           // pkt. B
      l.get(5);
      System.out.println("Punkt C");           // pkt. C
    }
    catch (Exception u)
    {
      System.out.println("Der opstod en undtagelse!");
    }
    System.out.println("Punkt D");             // pkt. D
  }
}

Punkt A
Punkt B
Der opstod en undtagelse!
Punkt D

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

14.2.1 Undtagelsesobjekter og deres stakspor

En undtagelse er, ligesom alt andet i Java, repræsenteret ved et objekt. En reference til dette undtagelses-objekt overføres som parameter til catch-blokken.

Objektet har nyttige informationer om fejlen. Metoden printStackTrace() udskriver et stakspor (eng.: stack trace), der beskriver de metodekald, der førte til, at undtagelsen opstod:

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

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

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

14.3 Undtagelser med tvungen håndtering

Indtil nu har oversætteren accepteret vores programmer, hvad enten vi håndterede eventuelle undtagelser 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 kaster disse undtagelser, skal han fange dem.

14.3.1 Fange undtagelser 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å undtagelsen IOException2.

Oversætteren tvinger os til at tage højde for den mulige undtagelse:

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 undtagelse IOException; den skal fanges, eller det skal erklæres, at den bliver kastet":

Vi er altså tvunget til enten at 1) fange undtagelsen ved at indkapsle koden i en try-catch-blok, f.eks.:

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

Undtagelser 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 undtagelser videre

Det har konsekvenser at sende undtagelser videre, for da skal kalderen 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():

import java.io.*;

public class Tastatur
{
  private BufferedReader ind;

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

  public String læsLinje()
  {
    try {
      String linje = ind.readLine();
      return linje;
    } catch (IOException u)
    {
      u.printStackTrace();
    }
    return null;
  }

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

Herover fanger vi undtagelsen 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() undtagelserne 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 undtagelser:

  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 TastaturKasterUndtagelser
{
  private BufferedReader ind;

  public TastaturKasterUndtagelser()
  {
    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 undtagelser 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 undtagelse 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å TastaturKasterUndtagelser altså siges at være bedst, selvom den altså giver kalderen mere arbejde.

14.4 Præcis håndtering af undtagelser

Det kan have væsentlige konsekvenser, på hvilket niveau undtagelserne 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.

public class SumMedTastatur
{
  public static void main(String[] arg)
  {
    Tastatur t = new Tastatur();
    boolean stop = false;
    try 
    {
      while (!stop)
      {
        System.out.print("Hvor mange tal vil du lægge sammen? ");
        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);
        System.out.print("Vil du prøve igen (j/n)? ");
        if ("n".equals(t.læsLinje())) stop = true; // undersøg om det er "n"
      }
    }   catch (Exception u) {
      System.out.println("Der opstod en undtagelse!");
      u.printStackTrace();
    }
  }
}

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 undtagelse!
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 undtagelse: 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 undtagelse 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 undtagelse 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 undtagelser

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

    try {
      ...
    }
    catch (NumberFormatException u1)
    {
      System.out.println("Fejl i fortolkningen af inddata");
    } 
    catch (IOException u2)
    {
      System.out.println("Inddata kunne ikke læses:"+u2);
    }
    catch (NullPointerException u3)
    {
      u3.printStackTrace();
    }

Alle undtagelses-klasser arver fra Exception og man kan fange enhver undtagelse ved, at fange deres fælles superklasse. Fejlhåndteringen bliver så generel, ligegyldigt hvilken type undtagelse 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 undtagelser (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 undtagelsestyper

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 undtagelser 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 2.3.1, 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/ - af Jacob Nordfalk.
Licens og kopiering under Åben Dokumentlicens (ÅDL) hvor intet andet er nævnt (82% af værket).

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