Indhold:
Forstå stakspor
Fange undtagelser og udskrive stakspor
Sende undtagelser videre og håndtere dem det rigtige sted
Kapitlet forudsættes i resten af bogen, og evnen til at kunne læse et stakspor er vigtigt, 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!".
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)
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) // Undtagelsestype er 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 sådan ud:
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.
En undtagelse er ligesom alt andet i Java repræsenteret ved et objekt, og en reference til dette 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 linie 11.
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.:
læsning og skrivning af filer (kaster bl.a.: FileNotFoundException, IOException)
netværkskommunikation (UnknownHostException, SocketException, IOException)
databaseforespørgsler (SQLException)
indlæsning af klasser (ClassNotFoundException)
Når programmøren kalder metoder, der kaster disse undtagelser, skal han fange dem.
Som eksempel vil vi indlæse en linie 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 linie; linie = ind.readLine(); System.out.println("Du skrev: "+linie); } }
Metoden readLine() læser en linie 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 linie = ind.readLine();
Fejlmeddelelsen ville på dansk lyde: "I TastaturbrugerFejl.java linie 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 { linie = ind.readLine(); System.out.println("Du skrev: "+linie); } 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)
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 linie fra tastaturet med læsLinie() eller læse en linie og omsætte den til et tal med læsTal(): (xxx typogra kode m linienr her)
import java.io.*; public class Tastatur { private BufferedReader ind; public Tastatur() { ind = new BufferedReader(new InputStreamReader(System.in)); } public String læsLinie() { try { String linie = ind.readLine(); return linie; } catch (IOException u) { u.printStackTrace(); } return null; } public double læsTal() { String linie = læsLinie(); return Double.parseDouble(linie); } }
Herover fanger vi undtagelsen IOException ved dens "rod" i læsLinie().
Den kunne gøres simplere ved at fjerne håndteringen og erklære IOException kastet:
public String læsLinie() throws IOException { String linie = ind.readLine(); return linie; }
Nu sender læsLinie() 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 linie = læsLinie(); return Double.parseDouble(linie); } 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æsLinie() throws IOException { String linie = ind.readLine(); return linie; } public double læsTal() throws IOException { String linie = læsLinie(); return Double.parseDouble(linie); } }
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æsLinie() kaldt fra læsTal()?
Jo, læsLinie() 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.
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. (xxx typogra kode m linienr: ændr til Nummerering 2)
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æsLinie())) 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 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? 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 linie 18 kaldte læsTal(), der i linie 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æsLinie())) 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
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? 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.
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 også fange enhver undtagelse ved at fange deres fælles superklasse.
Fejlhåndteringen bliver så generel, ligegyldigt hvilken type undtagelse der opstod:
try { ... } catch (Exception u) { System.out.println("Fejl:"); u.printStackTrace(); }
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å linierne).
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?
Ret programmet, så det også tæller antallet af gange, en sum blev korrekt afsluttet.
Æ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).
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.
3Selvom det er mindre væsentligt, kan man også se, at parseDouble() faktisk har kaldt en anden metode, nemlig readJavaFormatString().