Undantag (Exceptions)

Sven-Olof Nyström
OOP med Java våren -25
Informationsteknologi
Uppsala Universitet

Skansholm: Kapitel 11

Undantag

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:

Motivering

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.

Exempel 1

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:

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.

Exempel 2

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

Exempel 3

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.

Frågor om undantag.

Vi ska titta på några rimliga frågor:

Typer av undantag

Vi kan placera undantagen i tre grupper:

Klasstruktur

Så här ser klasstrukturen för undantag ut:

-------------
| Object    |
-------------
     \
      -------------
      | Throwable |
      -------------
        /        \
-------------   -------------
| Error     |   | Exception |
-------------   -------------
                /      \
                      -------------
       Kontrollerade  | Runtime-  |
       undantag       | Exception |
                      -------------
                          /   \

Notera att

Vilken typ av Throwable?

Vi klassificerar objekt i subklasserna till Throwable enligt följande regler:

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.

Att kasta ett undantag

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

Hantera undantag

try { ... }
catch (U1 e) {
    hantera felet
    }
catch (U2 e) {
    hantera felet
    }
finally {
    avsluta
}

Hantera, förklaring

Satsen som börjar med try { ... }

(Klausulerna testas i ordning. Om U2 är en subklass till U1 kommer den andra klausulen aldrig att köras.)

Try-catch, exempel

class Exce6 {
    static int div (int x, int y) {
        int r;
        try {
            r = x / y;
        }
        catch (ArithmeticException e) {
            r = 0;
        }
        return r;
    }

Try-catch, exempel (forts)

    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);
    }
}

Try-catch, körning

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$

Regel: undantag är för felsituationer

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.

Att passa ett kontrollerat undantag vidare

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.

Kontrollerade undantag

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

  1. fånga undantaget med en try-catch, eller
  2. deklarera att den i sin tur kan kasta undantaget.

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.

Finally

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:

Finally, fallanalys

Med andra ord: Finally-delen körs alltid.

finally kan användas för att (till exempel) stänga filer.

Undantag, sammanfattning

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.