Undantag i Java

Ett inledande exempel

Följande klass representerar ett lager av objekt:

import java.io.*; /** * Class for storing Integer objects. */ public class Storage { private Integer[] objects; /** * Create a storage with room for 5 Integers. */ public Storage() { objects = new Integer[5]; }
/** * Get a value from the storage. * @param ind Index of the requested value. */ public int getValue(int ind) { return objects[ind].intValue(); }



Vi använder metoden intValue i klassen Integer
för att hämta värdet.
/** * Store a value at a specified position in the storage, * if it is empty. * * @param ind Position on which the value will be stored * @param val Value to store */ public void put(int ind, int val) { if (objects[ind] == null) { objects[ind] = val; } } Vi lagrar bara ett värde om den aktuella platsen är tom.
/** * @return String representation of the storage. */ public String toString() { String res = ""; for (int i=0; i<objects.length; i++) { res += objects[i] + " "; } return res; }
public static void main(String[] args) { Storage s = new Storage(); int i = 3; System.out.println("Value on position " + i + ": " + s.getValue(i)); System.out.println(s.toString()); } } Vi försöker hämta ett värde från en tom plats i lagret.

För enkelhets skull har vi valt Integer-objekt, men det hade lika gärna kunnat vara IKEA-möbler, provglas, nudelpaket eller något annat som man kan behöva lagra. I konstruktorn skapar vi ett tomt lager och försöker hämta ett värde från detta. Därefter skriver vi ut strängrepresentationen av hela klassen. Testkör detta program!

Programmet bör ge ett meddelande i stil med:

Exception in thread "main" java.lang.NullPointerException at Storage.getValue(Storage.java:23) at Storage.main(Storage.java:53)

Man kan också tänka sig att vi försöker hämta värdet på ett element som ligger utanför arrayen:

public static void main(String[] args) { Storage s = new Storage();
int i = 5; Nu försöker vi komma åt en plats som ligger utanför lagret istället.
System.out.println("Value on position " + i + ": " + s.getValue(i)); System.out.println(s.toString()); }

Testa detta! Du bör nu få en utskrift i stil med:

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 5 at Storage.getValue(Storage.java:23) at Storage.main(Storage.java:53)

Du har förmodligen sett denna typ av meddelande förut (kanske med fel som NumberFormatException eller IOException istället för NullPointerException eller ArrayIndexOutOfBoundsException).

Undantag

Det som hände i det första exemplet är att vi försöker anropa en metod i ett objekt men referensen (objects[3]) är null. I det andra fallet försökte vi komma åt ett arrayelement som låg utanför arrayen. (Kom ihåg att det högsta indexet i en array av längd 5 är 4!) I båda fallen vore det missvisande om metoden returnerade ett heltal - hur ska vi då veta att det blev fel?! Istället signalerar programmet att något har blivit fel genom att kasta ett undantag ("exception" på engelska).

Undantag kan vara av olika typer. I det här fallet var de två undantagen av typen NullPointerException respektive ArrayIndexOutOfBoundsException, men det finns många fler typer.

Ett undantag är ett objekt som representerar det fel som uppkom. Typen på undantaget indikerar alltså vilken typ av fel det rör sig om. Ett undantag innehåller även information om var felet uppkom. I exemplen ovan kan vi se att felet uppstod på rad 23 i metoden getValue i filen Storage.java. Denna metod, i sin tur, anropades på rad 53 i main-metoden i Storage.java.

Objektet innehåller även mer detaljerad information om vad som blev fel innan undantaget kastades. Mer om detta senare.

Vad betyder det att ett undantag kastas?

Man kan tänka på ett undantag som kastas på samma sätt som man kan tänka på ett returvärde som returneras. Om vi hade skickat in ett värde mellan 0 och 4 till metoden getValue och motsvarande element hade varit initierat, hade metoden "skickat tillbaka" värdet av Integer-ojbektet som låg lagrat på den aktuella platsen i arrayen. När vi skickar in ett index för vilket det inte finns något Integer-objekt, skickar den istället tillbaka ett undantag.

Undantaget kastas (skickas) till den metod som anropade getValue, i det här fallet main i Storage. Antag att vi anropar getValue via en annan metod, till exempel så här:

import java.io.*; /** * Class for storing Integer objects. */ public class Storage { private Integer[] objects; /** * Create a storage with room for 5 Integers. */ public Storage() { objects = new Integer[5]; } Som ovan
public int average() { int sum = 0; for (int i=0; i<objects.length; i++) { sum += getValue(i); } int averageValue = sum / objects.length; return averageValue; } Ny metod, som anropar getValue.
/** * Get a value from the storage. * * @param ind Index of the requested value. */ public int getValue(int ind) { return objects[ind].intValue(); } /** * Store a value at a specified position in the storage, * it is empty. * * @param ind Position on which the value will be stored * @param val Value to store */ public void put(int ind, int val) { if (objects[ind] == null) { objects[ind] = val; } } /** * @return String representation of the storage. */ public String toString() { String res = ""; for (int i=0; i<objects.length; i++) { res += objects[i] + " "; } return res; } public static void main(String[] args) { Storage s = new Storage(); Som ovan
System.out.println("Average value: " + s.average()); Vi anropar den nya metoden istället för att anropa getValue direkt.
System.out.println(s.toString()); } }
Då kommer getValue kasta undantaget till average, som i sin tur kastar det vidare till main. Det ger följande utskrift:
Exception in thread "main" java.lang.NullPointerException at Storage.getValue(Storage.java:32) at Storage.average(Storage.java:20) at Storage.main(Storage.java:61)

Man kan alltså säga att ett undantag ersätter ett returvärde. Vidare kan ett undantag kastas från en metod oavsett vilken returtyp den har, även från void-metoder. Ibland måste man dock ange i metoddeklarationen att metoden kan kasta ett undantag. Mer om detta senare.

Att fånga ett undantag

I ovanstående exempel har programmet avbrutits när vi har råkat orsaka ett undantag. (Notera att den sista utskriften i main aldrig görs!) Det är inte alltid den effekt man vill ha av ett undantag, och det finns ett sätt att hantera undantaget så att programmet forsätter att köra: Man fångar undantaget.

För att kunna fånga ett undantag, måste man placera det metodanrop som kan kasta ett undantag i ett så kallat try-block och ange vilka undantag man vill kunna hantera (fånga) i en catch-sats. I catch-blocket anger man hur undantaget ska hanteras, det vill säga vad som ska hända när man har fångat ett undantag:

public static void main(String[] args) { Storage s = new Storage(); int i = 3; Som ovan
try { System.out.println("Value on position " + i + ": " + s.getValue(i)); } catch (NullPointerException e) { System.out.println("No object is stored on position " + i + "!"); } Här anropar vi getValue direkt igen, och fångar eventuella NullPointerExceptions.
System.out.println(s.toString()); }

Nu kommer mainmetoden att skriva ut ett felmeddelande och därefter fortsätta om den får tillbaka ett NullPointerException från getValue. Resultatet av en körning blir följande utskrift:

No object is stored on position 3! null null null null null

"null null null null null" är lagrets strängrepresentation, som skrivs ut på sista raden i main.

Om vi skulle försöka hämta värdet på ett element som ligger utanför arrayen skulle programmet fortfarande avbrytas med en likadan utskrift som i det inledande exemplet, men vi kan välja att fånga även ArrayIndexOutOfBoundsExceptions:

public static void main(String[] args) { Storage s = new Storage(); int i = 5; try { System.out.println("Value on position " + i + ": " + s.getValue(i)); } catch (NullPointerException e) { System.out.println("No object is stored on position " + i + "!"); Som ovan.
} catch (ArrayIndexOutOfBoundsException e) { System.out.println("Invalid array index: " + i + "!"); } Fånga även ArrayIndexOutOfBoundsExceptions.
> System.out.println(s.toString()); }

Det här programmet kommer att ge ett felmeddelande och sedan fortsätta köra vid båda typen av fel. (Testa detta själv!)

OBS! Fånga inte fel som du inte kan hantera! Att t ex skriva catch (Exception e) {} är att be om STORA problem! Varför?

Om vi i ovanstående exempel inte hade gett något felmeddelande för ett index som det inte går att hämta ett värde för, hade användaren inte fått veta vad som gick fel och varför. Däremot kommer hen förmodligen notera att programmet inte beter sig som förväntat, men får ingen förklaring till varför. Den programmerare som ska felsöka programmet kommer också att ha väldigt svårt att hitta ett sådant fel. (Okej, kanske inte ett så här kort program, men det är ytterst sällan som program som faktiskt används till något är så korta!)

Nyckelordet throws i metoddeklarationen

I ovanstående exempel har undantagen varit av typerna ArrayIndexOutOfBoundsException respektive NullPointerException, vilka i sin tur ärver från (är!) RuntimeExceptions. En metod som kastar ett RuntimeException behöver inte ange det i sin deklaration. Om man däremot kastar andra typer av undantag, behöver det stå i metodhuvudet. Ett exempel är FileNotFoundException som kastas när man försöker öppna en fil som inte finns, som i detta kodexempel.

import java.io.*; public class FileNotFoundDemo {
/** * Open the file whose name is given as argument to the method. * * @param fileName Path to file to open */ static void openFile(String fileName) throws FileNotFoundException { BufferedReader reader = new BufferedReader(new FileReader(fileName)); //... } Öppna en fil med ett givet namn.
public static void main(String[] args) throws FileNotFoundException { openFile("non-existent_file.txt"); } } non-exixtent_file.txt finns givetvis inte!

I och med att ett FileNotFoundException kan kastas och vi inte fångar det i metoden openFile, eller main för den delen, måste metoddeklarationerna innehålla ett "throws FileNotFoundException" som i exemplet.

Metoden getMessage

Som nämndes ovan innehåller ett undantag mer information än bara sin typ, till exempel en sträng som (oftast) talar om på vilket sätt det blev fel när undantaget kastades. Denna sträng kan man hämta med metoden getMessage. Om vi fångar undantaget i openFile ovan, kan vi hämta och skriva ut det meddelande som finns lagrat i undantaget, exempelvis såhär:

static void openFile(String fileName) { Eftersom vi fångar undantaget, kastas det inte lägre, så det behövs inget "throws" i metodhuvudet.
try { BufferedReader reader = new BufferedReader(new FileReader(fileName)); //... Som ovan.
} catch (FileNotFoundException e) { System.out.println("Error while opening file: " + e.getMessage()); } Fånga undantaget och skriv ut dess meddelande.
}

Kör vi detta program, kommer följande att skrivas ut:

Error while opening file: non-existent_file.txt (No such file or directory)

getMessage returnerade alltså strängen "non-existent\_file.txt (No such file or directory)".

Att kasta ett undantag

Låt oss återvända till exemplet med det Integer-lagret. Det är uppenbarligen inte lönt att försöka hämta saker ur ett tomt lager, så låt oss lagra några element först:

public static void main(String[] args) { Storage s = new Storage();
System.out.println(s.toString()); s.put(2, 67); System.out.println(s.toString()); s.put(3, 2549); System.out.println(s.toString()); s.put(2, 35); System.out.println(s.toString()); Vi lägger in några värden i vårt lager.
int i = 3; try { System.out.println("Value on position " + i + ": " + s.getValue(i)); } catch (NullPointerException e) { System.out.println("No object is stored on position " + i + "!"); } catch (ArrayIndexOutOfBoundsException e) { System.out.println("Invalid array index: " + i + "!"); } System.out.println(s.toString()); } Som ovan.

Nu finns det ett element på plats 3 och vi slipper få ett felmeddelande när vi anropar getValue. Testkör detta program!

Säkert noterar du att lagret ser likadant ut efter tredje inläggningen som det gjorde efter den andra. Det tredje värdet "tappas bort", vilket inte är särskilt sjysst mot användaren. Vi bör åtminstone tala om för henom att vi ignorerar värdet. Låt oss kasta ett undantag! Varför inte ett RuntimeException?!

public void put(int ind, int val) { if (objects[ind] == null) { objects[ind] = val; Som ovan.
} else { throw new RuntimeException("Cannot insert value on position " + ind + ", which already stores the value " + objects[ind] + "."); Vi kastar ett undantag för att indikera att platsen är upptagen.
} }

Vi anropar alltså RuntimeExceptions konstruktor (new RuntimeException(...)). Undantag är ju objekt! Som argument till konstruktorn ger vi en sträng som förklarar vad det var som blev fel. Det är denna sträng som kommer att returneras av metoden getMessage i undantaget vi just skapade. Nyckelordet throw betyder att undantaget ska kastas. (Jämför hur man använder nyckelordet return!)

Lika gärna som att skriva

throw new RuntimeException("Cannot insert value on position " + ind + ", which already stores the value " + objects[ind] + ".");
hade vi alltså kunna skriva
RuntimeException e = new RuntimeException("Cannot insert value on position " + ind + ", which already stores the value " + objects[ind] + "."); throw e;

Att skriva egna undantag

Det uppenbara svaret på frågan ovan ("Varför inte ett RuntimeException?") är att typen RuntimeException inte säger särskilt mycket om vad som blev fel. Typen på undantaget är värdefull information för den som försöker använda vår kod. Kanske vill hen hantera olika undantag på olika sätt. Kanske hanterar hen inte undantag, men blir hjälpt av att typen på undantaget (som ju skrivs ut i samband med att programmet avbryts när vi inte hanterar ett undantag) indikerar vilken typ av fel som uppstod.

Lyckligtvis är vi inte begränsade till att använda undantag som redan finns definierade i språket, utan kan definiera en egen lagom informativ typ. Låt oss skriva ett StorageException som vi kastar:

public class StorageException extends RuntimeException { public StorageException(String msg) { super(msg); } } Vår egen undantagsklass!

Det här är kanske den kortaste klass du har sett hittills och man kan fråga sig om den gör något över huvud taget. Jodå, det gör den! I och med att den ärver från RuntimeException gör den precis allt som RuntimeException gör. Det enda som skiljer de två undantagen åt är deras typ. Metoden super som anropas i konstruktorn är helt enkelt föräldraklassens (RuntimeExceptions) konstruktor (på samma sätt som i nätlektionen om grafiska användargränssnitt). Om detta känns otydligt nu, kommer det förmodligen att bli tydligare när vi går igenom arv senare i kursen!

Nu kan vi kasta undantaget i put:

public void put(int ind, int val) { if (objects[ind] == null) { objects[ind] = val; } else {
throw new StorageException("Cannot insert value on position " + ind + ", which already stores the value " + objects[ind] + "."); Vi kastar vårt eget undantag!
} }

Testkör detta program och notera att undantaget kastas. Modifiera koden så att du fångar undantaget och hanterar det på något lämpligt sätt!

Till kalkylatorn kommer ni att få skriva två egna undantagsklasser som representerar syntaxfel respektive evalueringsfel. Dessa två typer av fel ska nämligen inte hanteras på exakt samma sätt av programmet. Om det verkar onödigt omständigt att definiera egna undantag nu, kommer nyttan förhoppningsvis att bli uppenbar lite senare i kursen.

Tillbaka


Valid CSS!