Skansholm: Kapitel 11
Engelska: exceptions
Skansholm: exceptionella händelser
Undantag är fel som genereras vid körning. Exempel: om man försöker öppna en fil som inte finns, dividera med noll, indexera utanför en array.
Det finns flera skäl till att ta upp undantag:
Varför vill man inte hantera felsituationer med de vanliga kontrollkonstruktionerna (if-else osv)? Betrakta följande exempel:
Anta att du vill skriva ett program som
Alla operationer kan gå snett! Till exempel: filen kanske inte finns, den kanske inte går att läsa, den kanske är på fel format, operationen på innehållet misslyckas.
Man kan skriva kod för att hantera alla tänkbara felsituationer ...
h = open_file(...) if (h.error) { // hantera felet } else { // fortsätt att läsa filen ... }
men kontrollflödet blir komplicerat. Det är också svårt att testa koden eftersom man måste provköra med alla kombinationer av felsituationer. Därför har Java (och de flesta andra moderna programspråk) särskilda mekanismer för att hantera fel som uppkommer vid körning.
Låt oss titta på ett enkelt program med fel.
class A {} class B extends A {} class Exce2 { static void main (String [] arg) { A x = new A(); B y = (B)x; System.out.println("Hej hopp"); } }
Vad är felet?
Vid körning får vi följande resultat.
$ java Exce2 Exception in thread "main" java.lang.ClassCastException: A at Exce2.main(Exce2.java:8)
(Körningen avbryts innan utskriften "Hej hopp")
Notera felmeddelandet:
java.lang.ClassCastException
Här ser vi typ av fel, dvs ett misslyckat försök att göra typkonvertering
A
. Här anges vilken klass det objekt vi ville konvertera tillhörde.at Exce2.main
Här ser vi klass och metod.
Exce2.java:8
Och till sist får vi veta filnamn och radnummer i filen.
Om du får ett långt felmeddelande när du arbetar med en uppgift är det ofta en bra idé att studera meddelandet i detalj. Det kan ge värdefull information om vad som orsakade felet.
public class ExceNull { private void method() {} public static void main(String[] arg) { ExceNull n = null; n.method(); } } harpo$ javac ExceNull.java harpo$ java ExceNull Exception in thread "main" java.lang.NullPointerException at ExceNull.main(ExceNull.java:7)
En nullpointerexception. Den lokala variabeln n
har värdet null
,
därför är metodanropet ej giltigt.
(Det kan vara värt att titta en gång extra på det här exemplet—nullpointerexceptions är mycket vanliga.)
class A { public int m(int x) { return 100/x; } } public class Exce { public static void main (String[] arg) { int x = Integer.parseInt(arg[0]); A a = new A(); int z = a.m(x); System.out.println("100/"+x+" = "+ z); }}
Här har vi flera testkörningar med flera olika felmeddelanden.
harpo$ javac Exce.java harpo$ java Exce Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0 at Exce.main(Exce.java:10)
Arrayen arg
är av längd 0, så det är inte möjligt att indexera den.
harpo$ java Exce kaka Exception in thread "main" java.lang.NumberFormatException: For input string: "kaka" at java.lang.NumberFormatException .forInputString(NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:447) at java.lang.Integer.parseInt(Integer.java:497) at Exce.main(Exce.java:10)
Strängen "kaka" kan förstås inte konverteras till ett giltigt tal.
harpo$ java Exce13 Exception in thread "main" java.lang.NoClassDefFoundError: Exce13
Här skrev jag fel klassnamn på kommandoraden. Notera att felmeddelandet ger en god beskrivning av vad som är fel.
harpo$ java Exce 12 100/12 = 8 harpo$ java Exce 13 100/13 = 7 harpo$ java Exce 0 Exception in thread "main" java.lang.ArithmeticException: / by zero at A.m(Exce.java:3) at Exce.main(Exce.java:14) harpo$
I den sista körningen gör vi division med noll. Notera att felmeddelandet talar om vad som är fel. Det innehåller en representation av call-stack, dvs kedjan av metodanrop som ledde fram till felet.
; java.lang.integer
.
Vi ska titta på några rimliga frågor:
Vi kan placera undantagen i tre grupper:
Error
Fel som vanligtvis inte kan hanteras av programmet. Till exempel: att maskinen har slut på minne.
RuntimeException
Exempel: division med noll, fel i arrayindexering, fel i
typkonvertering, access av null
-objekt.
Fel av dessa typer kan förstås uppstå i många situationer. Man brukar inte skriva kod för att hantera såna fel.
Fel som rimligtvis bör hanteras av programmet, till exempel försök att öppna en fil som inte finns.
Så här ser klasstrukturen för undantag ut:
------------- | Object | ------------- \ ------------- | Throwable | ------------- / \ ------------- ------------- | Error | | Exception | ------------- ------------- / \ ------------- Kontrollerade | Runtime- | undantag | Exception | ------------- / \
Notera att
Throwable
.Throwable:
java.lang.Error
och java.lang.Exception
.
Throwable
"Exception
".java.lang.Exception
har en viktig subklass:
java.lang.RuntimeException
Vi klassificerar objekt i subklasserna till Throwable
enligt följande
regler:
Throwable
tillhör gruppen Error
om det tillhör en klass som ärver från java.lang.Error
Error
används för ovanliga fel som är svåra att hantera. Jag kommer
inte att diskutera dem vidare.
Runtime Exception
om det tillhör en klass
som ärver från java.lang.RuntimeException
IOException
.Klassen RuntimeException
har en konstruktor som tar Throwable
som
argument. Man kan använda denna för att "konvertera" ett kontrollerat
undantag till ett i klassen RuntimeException
.
(Skansholm säger: "Generera exceptionella händelser")
class Undantag extends Exception {} class A { void m() throws Undantag { throw new Undantag(); } }
Första raden definierar en ny typ av undantag.
Vi måste deklarera att metoden m
kastar undantag (och
vilken typ av undantag).
Detta gäller för alla kontrollerade undantag.
Undantag skapas med new
(som alla andra objekt).
try { ... } catch (U1 e) { hantera felet } catch (U2 e) { hantera felet } finally { avsluta }
Satsen som börjar med
try { ... }
...
(kroppen) ochU1
och U2
och(Klausulerna testas i ordning. Om U2
är en subklass till U1
kommer den
andra klausulen aldrig att köras.)
class Exce6 { static int div (int x, int y) { int r; try { r = x / y; } catch (ArithmeticException e) { r = 0; } return r; }
static void main (String [] arg) { int x = Integer.parseInt(arg[0]); int y = Integer.parseInt(arg[1]); int z = div(x,y); System.out.println(z); } }
harpo$ java Exce6 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0 at Exce6.main(Exce6.java:13) harpo$ java Exce6 100 7 14 harpo$ java Exce6 100 0 0 harpo$
Använd endast undantag för att hantera fel. Om programmet måste hantera en viss situation som väntas uppträda vid normal användning är det bättre att lösa detta med de vanliga kontrollstrukturerna.
Exempel: Om variabeln ref
kan vara null
skulle man kunna skriva såhär:
try { System.out.println(ref.toString()); } catch (NullPointerException e) System.out.println("ref is null"); }
men det är nog bättre att skriva så:
if (ref != null) System.out.println(ref.toString()); else System.out.println("ref is null"); }
Med andra ord: Istället för att kasta ett undantag om variabeln ref
är
null
, testa med en if-sats.
Det finns flera skäl till att man endast bör använda undantag i felsituationer:
Titta på exemplet Exce6.java
igen. I det fallet är det bättre att
testa om andra argumentet är 0.
Vad händer om en metod inte fångar ett kontrollerat undantag?
Undantaget passas vidare till anroparen.
I detta fall måste anroparen också deklarera undantaget.
De kontrollerade undantagen måste hanteras av programmet. Om du öppnar en fil måste du tala om vad som ska hända om filen inte finns.
Om en viss metod kan kasta ett kontrollerat undantag måste varje metod som anropar den antingen
Tanken är att den som skriver programmet ska tvingas tänka på att (till exempel) en fil kanske inte finns där vi väntar oss. Ibland frestas man dock att fånga undantagen på det allra enklaste sättet, till exempel med
try ... catch (IOException e) { }
eller (ännu värre)
try ... catch (Exception e) { }
Det här är en katastrofalt dålig lösning! Den har faktiskt förekommit i läroböcker. Det händer till och med att jag ser sån kod i lösningar till inlämningsuppgifter :-).
Problemet är att om man fångar felet utan att göra något med det finns det ju inget sätt att upptäcka att felet inträffat. Detta kan leda till en besvärlig debuggingsituation där ett programmeringsfel ej upptäcks. En god princip är att alltid försöka göra buggar synliga.
Om man måste hantera ett kontrollerat undantag men inte vet vad man
ska göra av det är det enklaste att kasta det vidare. Klassen
RuntimeException
har en konstruktor som tar Throwable
som argument, så
man kan alltid skriva
try ... catch (IOException e) { throw new RuntimeException(e); }
Det här är kanske inte en idealisk lösning, men felet blir synligt och det är viktigast.
En try-catch kan skrivas med en gren med nyckelordet finally
:
try { ... } catch (U1 e) { hantera felet } catch (U2 e) { hantera felet } finally { avsluta }
När körs finally
-delen?
En try
kan terminera på tre olika sätt:
try
-satsen terminerar normalt
körs finally-delen efteråt.Med andra ord: Finally-delen körs alltid.
finally
kan användas för att (till exempel) stänga filer.
Exception
.try-catch
) eller deklareras med throws.
Vissa undantag (kontrollerade undantag, eller checked exceptions) måste deklareras (eller hanteras); om en metod kan kasta ett sådant måste den deklarera det.
Använd inte try-catch
för att dölja fel. En catch med tom kropp kan
försvåra debugging.
Använt inte try-catch
i det normala kontrollflödet. Att fånga
exceptions är relativt kostsamt, ger svårläst kod och kan leda till
att andra undantag än de du tänkt dig fångas.