Innholdsoversikt for programutvikling
Exception
-typerthrow
-instruksjonenNå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å.
En unormal situasjon av type feilsituasjon er så alvorlig at den ikke lar seg reparere. Programutførelsen må stoppe opp.
Feilsituasjoner lar seg altså ikke reparere. Men vi skal nå lære hvordan vi kan programmere oss ut av unntakssituasjoner.
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.
Når en unntakssituasjon oppstår, har vi følgende muligheter til å forholde oss til situasjonen:
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 }
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.
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.
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.
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 }
Exception
-typerNoen 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.
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.
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 }
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
-instruksjonenUnntaksobjekter 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:
Object
Throwable
Error
Exception
RunTimeException
ArithmeticException
NullPointerException
IndexOutOfBoundsException
ArrayIndexOutOfBoundsException
StringIndexOutOfBoundsException
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.
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.
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.
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