Innholdsoversikt for programutvikling

Unntakshåndtering (exceptions)

Innledning

Når et javaprogram kjører, kan det oppstå forskjellige unormale situasjoner. Vi kan kategorisere dem i to typer, avhengig av hvor alvorlige de er:

En unntakssituasjon er en type unormal situasjon som ikke er verre enn at den lar seg håndtere ved at vi på det stedet i programmet der situasjonen kan tenkes å oppstå legger inn passende programkode som gjør at programmet kan kjøre videre på en fornuftig måte i tilfelle situasjonen skulle oppstå.

Eksempler på unntakssituasjoner

En unormal situasjon av type feilsituasjon er så alvorlig at den ikke lar seg reparere. Programutførelsen må stoppe opp.

Eksempel på feilsituasjon

Feilsituasjoner lar seg altså ikke reparere. Men vi skal nå lære hvordan vi kan programmere oss ut av unntakssituasjoner.

Virkemåte for unntakssituasjoner (exceptions)

En unntakssituasjon genererer et Exception-objekt. Objektet inneholder informasjon om hvor situasjonen oppstod og hvilken type unntakssituasjon det er. Vi sier at unntakssituasjonen kaster ut (throws) Exception-objektet. Programmereren kan i programmet legge inn kode som fanger opp (catches) Exception-objektet og behandler situasjonen. Exception-klassen er imidlertid superklasse for mange forskjellige subklasser, hver av dem for sin spesielle type unntakssituasjon. I praksis er det derfor et subklasseobjekt som blir kastet ut. Vi skal se litt nærmere på klassehierarkiet seinere.

Muligheter for håndtering

Når en unntakssituasjon oppstår, har vi følgende muligheter til å forholde oss til situasjonen:

Eksempel 1: Divisjon med null

Nedenfor er gjengitt det lille programmet Zero.java som foretar provosert divisjon med null, noe som selvsagt ikke bør gjøres! Hensikten her er bare å se på feilmeldingen som dette resulterer i og hva den forteller oss. Prøver vi å kjøre programmet, vil det bli skrevet ut en feilmelding. Den kan se litt forskjellig ut, avhengig av hvilken java-versjon programmet blir kjørt med og hvilket verktøy som blir brukt for å kjøre programmet. En mulighet for feilmelding er som følger:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Zero.main(Zero.java:9)

Av feilmeldingen ser vi at feilsituasjonen (det vil si Exception-objektet som er blitt kastet ut) er av type ArithmeticException, samt at det skyldes divisjon med null. Det står også at feilen har oppstått i Zero-klassens main-metode på linje 9 i fila Zero.java. Det er nettopp der vi har instruksjonen

      System.out.println (teller / nevner);

Slike feilmeldinger gir oss derfor svært nyttig informasjon, slik at vi kan få rettet opp programmet og unngå feilmeldingene. Målet er at et program skal virke på en tilfredsstillende måte i alle situasjoner. Istedenfor å skrive ut feilmeldinger på tilsvarende måte som det skjedde her, bør programmet føre en dialog med brukeren. I tilfelle unntakssituasjoner oppstår, bør brukeren få forklarende meldinger om hva som er galt og hva som må gjøres for at programmet skal virke riktig. For å få det til, må vi vite hvordan vi skal behandle slike unntakssituasjoner for å kunne føre ønsket brukerdialog. Det er nettopp det som er temaet for dette notatet.

 1 public class Zero
 2 {
 3   //  Genererer en exception.
 4    public static void main (String[] args)
 5    {
 6       int teller = 10;
 7       int nevner = 0;
 8
 9       System.out.println (teller / nevner);
10    }
11 }

Oppfanging og behandling av unntakssituasjoner

For å kunne behandle en unntakssituasjon, må vi legge inn kode med følgende struktur (uttrykt i pseudokode):

  try
  {
    < instruksjoner som kan kaste ut (throw) et unntaksobjekt av en
              eller annen type >
  }
  catch ( Unntaksklasse1 parameter1 )
  {
    < instruksjon(er) >
  }
  catch ( Unntaksklasse2 parameter2 )
  {
    < instruksjon(er) >
  }
  catch ...     // så mange catch vi trenger
  {
    ...
  }
  finally       // finally-delen er frivillig å ta med
  {
    < instruksjon(er) >
  }

En catch-blokk, som følger rett etter en try-blokk, definerer hvordan en bestemt type unntakssituasjon skal håndteres. Den kalles derfor en unntakshåndterer (exception handler). Etter en try-blokk må det følge én eller flere catch-blokker og/eller en finally-blokk. Det trenger altså ikke være noen finally-blokk dersom det er minst én catch-blokk. Og motsatt trenger det ikke være noen catch-blokk dersom det er en finally-blokk.

Virkning

En try-catch-finally-bolk som skissert ovenfor virker på følgende måte:

Når programkontrollen kommer til en try-blokk, så utføres instruksjonene inne i denne etter tur. Dersom det ikke blir kastet ut noe unntaksobjekt under utførelsen, går programkontrollen til første instruksjon etter alle påfølgende catch-blokkene. Dette er den normale programutførelsen.

Dersom det blir kastet ut et unntaksobjekt under utførelsen av try-blokka, går programkontrollen direkte til catch-blokkene. Den første av disse som har parameter som er typekompatibel med unntaksobjektet blir utført, men ingen av de andre. Typekompatibel betyr at unntaksobjektet er av samme type eller av en subklassetype. En viktig konsekvens av dette er at i catch-rekkefølgen må parametre av subklasse-typer stå foran parametre av tilhørende superklasse-typer, ellers vil de aldri bli nådd.

Når instruksjonene i catch-blokka er utført, går programkontrollen videre til første instruksjon etter hele try-catch-bolken. Den går ikke tilbake til try-blokkka!

Dersom en finally-blokk er inkludert i en try-instruksjon, så blir koden i den utført etter at try-catch-delen er utført. Dette skjer uavhengig av hvordan fullførelsen ble gjort, om den ble gjort normalt, ved en Exception, eller NB!, ved en return eller break. Vi kan altså være sikre på at finally-delen alltid blir utført. Vanligvis brukes finally-delen til intern "opprydding" eller til å frigjøre ressurser, for eksempel lukke filer.

Merknad

Fra og med javaversjon 7 er det kommet en ny type try-instruksjon: try-med-ressurser. Denne erstatter i stor grad bruk av finally-del, i og med at den blant annet sikrer at filer vil bli lukket etter bruk. Dette vil bli omtalt i notatet Filbehandling.

Eksempel 2

Programmet Zero2.java som er gjengitt nedenfor viser hvordan unntaksobjektet som ble kastet ut ved den provoserte divisjonen med null som vi hadde i forrige eksempel kan fanges opp ved bruk av try-catch. Instruksjonen som kan resultere i unntakssituasjon er lagt inn i en try-blokk. Den påfølgende catch kan fange opp den type Exception som vi så ble generert i eksemplet foran. Når vi kjører programmet, får vi i dette tilfelle skrevet ut meldingen

  Forsøk på divisjon med null.

istedenfor den feilmeldingen vi fikk i forrige program. Istedenfor å la meldingen bli skrevet ut på det sted som blir bestemt av kjøresystemet, kunne vi selvsagt valgt å legge inn kode for å få skrevet ut meldingen i en dialogboks.

 1 public class Zero2
 2 {
 3   //  Genererer en exception og fanger den opp.
 4    public static void main (String[] args)
 5    {
 6       int teller = 10;
 7       int nevner = 0;
 8
 9       try
10       {
11         System.out.println (teller / nevner);
12       }
13       catch ( ArithmeticException e )
14       {
15         System.out.println( "Forsøk på divisjon med null." );
16       }
17    }
18 }

Alternativ oppfanging av multiple Exception-typer

Noen ganger har vi en programblokk som kan tenkes å kaste ut unntaksobjekter av flere forskjellige typer. Som beskrevet ovenfor, kan vi da ha flere catch-blokker etter hverandre, slik at vi kan få fanget opp alle de forskjellige typene. Kanskje ønsker vi i så fall den samme behandlingsmåten for alle de forskjellige Exception-typene, slik at de forskjellige catch-blokkene da vil inneholde den samme koden. I slike tilfelle er det fra og med javaversjon 7 tillatt å la samme catch-blokk behandle flere forskjellige Exception-typer. En slik catch-blokk har følgende struktur:

  catch (Exceptiontype1 | Exceptiontype2 e) //eventuelt flere typer
  {
    < ønsket kode for behandling av situasjonen >
  }

Vi skiller altså med loddrette streker de forskjellige Exception-typene som catch-blokka skal kunne behandle. Denne nye typen alternativ kode er både kortere og mer effektiv enn å bruke flere catch-blokker etter hverandre med gjentakelse av den samme koden. Det er imidlertid en begrensning at de Exception-typene vi lister opp ikke kan stå i relasjon til hverandre ved at én type er subtype av en annen.

Når bør unntakshåndtering brukes?

De to eksemplene foran hadde ikke annen hensikt enn å framprovosere en unntakssituasjon, vise resultatet av den, og vise hvordan den kunne behandles. Situasjonen kunne lett vært unngått ved å legge inn en if-test. Du kan kanskje få inntrykk av at du nå skal begynne å bruke try-catch istedenfor if-tester. Men det er ikke tilfelle. Unntakshåndtering med try-catch bør ikke brukes på situasjoner som vi normalt forventer oss kan oppstå, for eksempel divisjon med null. For slike situasjoner bør det brukes vanlig testing med if-setninger. Det er både mer effektivt og gir klarere kode. Men på situasjoner som normalt ikke skal oppstå, eller som det er vanskelig å fange opp med enkel kode, bør unntakshåndtering brukes. Dette kan for eksempel dreie seg om feil i tallformat når en tekststreng skal konverteres til en tallverdi.

Hvilke situasjoner som kan forventes å oppstå, og hvilke som ikke er forventet, kan være noe uklart. Men poenget er at en ikke skal misbruke mekanismen med unntakshåndtering som en rapporteringsmåte for ting som kan forventes.

Eksempel 3

I programmet Konverteringstest.java som er gjengitt nedenfor, kan brukeren skrive inn to hele tall. Programmet skal dividere det første tallet med det andre og skrive ut resultatet (som et formatert desimaltall). Når dette skal utføres, kan vi tenke oss to potensielle feilsituasjoner. Den første er at én eller begge av sifferstrengene som blir lest inn og som programmet skal konvertere til en heltallsverdi, har galt format, det vil si inneholder noe annet enn bare sifre. Den andre andre mulige feilsituasjonen er at nevneren for divisjonen (det vil si det andre tallet) er lik 0.

Dersom vi vil behandle den første av disse to mulige feilsituasjonene på tradisjonell måte med bruk av if-setninger, vil det kreve en god del omtanke for at skal virke riktig og fange opp alle feilmuligheter. Dette vil derfor være en situasjon som det er greit å bruke et try-catch-opplegg på. I programmet er det gjort ved at instruksjonene for konvertering av innleste sifferstrenger til tallverdi er lagt inn i en try-blokk. (Selve innlesingsinstruksjonene av sifferstrengene kunne ligget foran try-blokka.) Under forsøk på konvertering til tallverdi vil det bli kastet ut et unntaksobjekt av type NumberFormatException i tilfelle konverteringen ikke lar seg gjennomføre. Det er derfor denne parametertypen vi bruker i den etterfølgende catch. I tilfelle catch'en slår til, skriver vi i en dialogboks melding til brukeren om at det er feil i tallformat og at det må skrives inn to hele tall. Siden programkontrollen ville gå rett til catch-blokka i tilfelle det oppsto konverteringsproblemer, vil det ikke bli forsøkt å utføre divisjon i det hele tatt i dette tilfelle.

Dersom innlesing og konvertering går uten at feil oppstår, vil det være en mulighet for at det andre tallet, som det skal divideres med, er lik 0. Dette er det lett å teste på i en vanlig if-setning og er derfor i programmet gjort nettopp på den måten. Er nevneren lik 0, gir vi en passe melding om det til brukeren. I motsatt fall kan divisjonen gjennomføres og resultatet skrives ut. Vi har dermed fått et robust program som virker på en fornuftig måte i alle situasjoner.

 1 //Viser hvordan feil i tallformat kan sjekkes og unngås.
 2 import java.text.DecimalFormat;
 3 import javax.swing.*;
 4 import java.awt.*;
 5 import java.awt.event.*;
 6 public class Konverteringstest extends JFrame
 7 {
 8   private JTextField input1, input2, output;
 9
10    public Konverteringstest()
11    {
12       super( "Demonstrerer Exceptions" );
13
14       Container c = getContentPane();
15       c.setLayout( new FlowLayout() );
16       c.add( new JLabel( "Skriv teller " ) );
17       input1 = new JTextField( 10 );
18       c.add( input1 );
19       c.add( new JLabel( "Skriv nevner og trykk Enter " ) );
20       input2 = new JTextField( 10 );
21       c.add( input2 );
22       input2.addActionListener( new Inputlytter() );
23       c.add( new JLabel( "RESULTAT " ) );
24       output = new JTextField( 10 );
25       c.add( output );
26       setSize( 300, 150 );
27       setVisible( true );
28    }
29
30    private class Inputlytter implements ActionListener
31    {
32      public void actionPerformed(ActionEvent e)
33      {
34        DecimalFormat precision3 = new DecimalFormat("0.000");
35        output.setText("");
36
37        try
38        {
39          int tall1 = Integer.parseInt(input1.getText());
40          int tall2 = Integer.parseInt(input2.getText());
41
42          if (tall2 == 0)
43            JOptionPane.showMessageDialog(
44                    Konverteringstest.this,
45                    "Kan ikke dividere med null!",
46                    "Forsøk på divisjon med null",
47                    JOptionPane.ERROR_MESSAGE);
48          else
49          {
50            double resultat = (double) tall1 / tall2;
51            output.setText(precision3.format(resultat));
52          }
53        }
54        catch (NumberFormatException nfe)
55        {
56          JOptionPane.showMessageDialog(
57                  Konverteringstest.this,
58                  "Du må skrive inn to heltall",
59                  "Feil i tallformat", JOptionPane.ERROR_MESSAGE);
60        }
61      }
62    }
63
64    public static void main( String args[] )
65    {
66       Konverteringstest vindu = new Konverteringstest();
67       vindu.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
68    }
69 }

Merknad om dialogbokser

En dialogboks er ikke noe selvstendig vindu, men er vanligvis knyttet til et annet vindu som det er vanlig å kalle foreldervinduet. Første parameter i kallet på metoden showMessageDialog ovenfor er en referanse til dette foreldervinduet, som i dette tilfelle er definert av klassen Konverteringstest. Siden kallet på metoden skjer i en indre klasse til denne klassen, må vi for å referere til det skrive Konverteringstest.this. Virkningen av dette når vi kjører programmet vil være at dialogboksen vil legge seg midt oppå sitt foreldervindu, i tilfelle boksen blir vist.

throw-instruksjonen

Unntaksobjekter blir kastet ut som følge av at det utføres en throw-instruksjon. Den har følgende struktur:

  if ( < er det noe som ikke er oppfylt? > )
  {
    < opprett unntaksobjekt av relevant type >
    throw unntaksobjekt;
  }

Unntaksobjektet som blir kastet ut kan ikke være av en hvilken som helst type. De predefinerte typene som kan brukes finnes i javas klassebibliotek og ligger slik i klassehierarkiet:

Hver ny innrykket liste av klasser er her å oppfatte som subklasser til siste klasse i den foregående liste.

Unntaksobjekter av type RunTimeException og dens subklasser kan kastes ut uten videre av alle metoder ved bruk av en throw-instruksjon. Unntaksobjekter av disse typene kalles usjekkede unntak (engelsk: unchecked exceptions). Betegnelsen kommer av at kompilatoren ikke sjekker om en metode kan kaste ut slike unntak, eller om den fanger opp slike. Alle andre typer av unntaksobjekter kalles sjekkede unntak (checked exceptions). Dersom slike ikke blir fanget opp av en try-catch-setning i samme metode som kaster dem ut, må det spesifiseres i metodedefinisjonen at de kan kastes ut. Det gjøres med en throws-setning ved at metodedefinisjonen skrives slik at den har følgende struktur:

  datatype metodenavn( < parameterliste > )
      throws Unntakstype1, Unntaksype2    // én eller flere typer
  {
    .
    .
    .
    if ( ... )
      throw new Unntakstype1( ... );  // programkontrollen går tilbake
                                      // til kallstedet
    .
    .
    .
    if ( ... )
      throw new Unntaksype2( ... );
    .
    .
    .
    < eventuelt kall på en metode som kan kaste ut en av de
              spesifiserte typer unntaksobjekt >
  }

Som type for unntaksobjekt kan vi i tillegg til alle predefinerte typer bruke egendefinerte typer. Egendefinerte typer må imidlertid være subklasser til Exception-klassen eller en av dens subklasser.

Vandring av unntakstilstander

Dersom et unntaksobjekt blir kastet ut i en metode og ikke blir fanget opp av en catch-instruksjon i metoden, går programkontrollen umiddelbart tilbake til det stedet der metodekallet ble gjort. Dersom objektet ikke blir fanget opp der heller, går programkontrollen tilbake til det stedet der denne metoden ble kalt, og så videre skritt for skritt. Dette kan fortsette inntil programkontrollen kommer tilbake til main-metoden. Dersom unntaksobjektet ikke blir fanget opp der heller, vil det bli skrevet ut en feilmelding, slik vi har sett eksempel på.

Ut fra kjennskapet til hvordan unntaksobjekter blir sendt rundt i programmet, må vi velge det sted vi synes er mest passende til å fange dem opp, eventuelt velge å ikke fange dem opp i det hele tatt. Det normale bør være at unntaksobjekter som vi vet hvordan vi skal behandle, fanger vi opp på det stedet der de blir kastet ut. De unntaksobjektene som vi ikke vet hvordan vi skal behandle, sender vi videre til kallstedet for metoden.

Hensikten med unntakshåndtering er selvsagt at programutførelsen skal kunne fortsette på en fornuftig måte, som om unntakssituasjonen ikke hadde oppstått. I mange tilfelle vil unntakshåndteringen gå ut på å gi en passende melding til brukeren om hva som var feil, og så gi brukeren anledning til å respondere på en passende måte, for eksempel ved å gi ny input til programmet.

Bruk av printStackTrace og getMessage

I klassen Throwable, som alle exceptionklasser arver fra, er det definert en metode printStackTrace. I tilfelle det blir kastet ut et unntaksobjekt, kan vi derfor når vi fanger opp dette bruke det til å gjøre kall på printStackTrace. Dersom vi ikke behandler unntakssituasjonen, vil imidlertid javasystemets egen unntakshåndterer gjøre kall på printStackTrace. Det er derfor resultatet av dette kallet vi ser som feilmelding på skjermen når det oppstår en unntakssituasjon som vi ikke har fanget opp med egen programkode.

Når printStackTrace blir utført, gjør den først kall på unntaksobjektets toString-metode. Denne returnerer en streng med følgende innhold:

KlassenavnForUnntaksobjekt: < Resultatet av getMessage-metoden
                                                                      for unntaksobjektet >

Deretter listes opp, i motsatt rekkefølge, metodekallene som førte til at unntakssituasjonen oppsto. På grunn av denne motsatte rekkefølgen vil det være blant de første linjene i utskriften at vi vil finne henvisning til hvor i våre egne programfiler unntakssituasjonen har oppstått. (Se eksempel 4 nedenfor.)

Metoden getMessage returnerer den eventuelle strengen som var konstruktørparameter da unntaksobjektet ble opprettet.

Eksempel 4

Vi endrer catch-setningen i forrige eksempel ved å tilføye et kall på printStackTrace, slik at catch-setningen har følgende innhold:

       catch (NumberFormatException nfe)
       {
         nfe.printStackTrace();
         JOptionPane.showMessageDialog(
		  Konverteringstest.this,
		  "Du må skrive inn to heltall",
                  "Feil i tallformat", JOptionPane.ERROR_MESSAGE);
       }

Dersom vi kjører programmet og skriver inn et blankt tegn etter sifferet 2 når vi skriver inn nevner, vil det bli skrevet ut følgende feilmelding:

java.lang.NumberFormatException: For input string: "2 "
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:492)
	at java.lang.Integer.parseInt(Integer.java:527)
	at Konverteringstest$Inputlytter.actionPerformed(Konverteringstest.java:40)
	at javax.swing.JTextField.fireActionPerformed(JTextField.java:508)
	at javax.swing.JTextField.postActionEvent(JTextField.java:721)
	at javax.swing.JTextField$NotifyAction.actionPerformed(JTextField.java:836)
	at javax.swing.SwingUtilities.notifyAction(SwingUtilities.java:1661)
	at javax.swing.JComponent.processKeyBinding(JComponent.java:2879)
	at javax.swing.JComponent.processKeyBindings(JComponent.java:2926)
	at javax.swing.JComponent.processKeyEvent(JComponent.java:2842)
	at java.awt.Component.processEvent(Component.java:6281)
	at java.awt.Container.processEvent(Container.java:2229)
	at java.awt.Component.dispatchEventImpl(Component.java:4860)
	at java.awt.Container.dispatchEventImpl(Container.java:2287)
	at java.awt.Component.dispatchEvent(Component.java:4686)
	at java.awt.KeyboardFocusManager.redispatchEvent(KeyboardFocusManager.java:1908)
	at java.awt.DefaultKeyboardFocusManager.dispatchKeyEvent(DefaultKeyboardFocusManager.java:752)
	at java.awt.DefaultKeyboardFocusManager.preDispatchKeyEvent(DefaultKeyboardFocusManager.java:1017)
	at java.awt.DefaultKeyboardFocusManager.typeAheadAssertions(DefaultKeyboardFocusManager.java:889)
	at java.awt.DefaultKeyboardFocusManager.dispatchEvent(DefaultKeyboardFocusManager.java:717)
	at java.awt.Component.dispatchEventImpl(Component.java:4730)
	at java.awt.Container.dispatchEventImpl(Container.java:2287)
	at java.awt.Window.dispatchEventImpl(Window.java:2713)
	at java.awt.Component.dispatchEvent(Component.java:4686)
	at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:707)
	at java.awt.EventQueue.access$000(EventQueue.java:101)
	at java.awt.EventQueue$3.run(EventQueue.java:666)
	at java.awt.EventQueue$3.run(EventQueue.java:664)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
	at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:87)
	at java.awt.EventQueue$4.run(EventQueue.java:680)
	at java.awt.EventQueue$4.run(EventQueue.java:678)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
	at java.awt.EventQueue.dispatchEvent(EventQueue.java:677)
	at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:211)
	at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:128)
	at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:117)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:113)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:105)
	at java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

På utskriften ovenfor vil du se at det er på linje 5 at det er henvisning til vår programfil, nærmere bestemt til linje 40 i den. Det er nettopp der det er forsøk på konvertering til heltall av den strengen som ble lest inn for nevneren.

Innholdsoversikt for programutvikling

Copyright © Kjetil Grønning og Eva Hadler Vihovde, revidert 2015