10 Exception handling - Boom hoger onderwijs

advertisement
10 Exception handling
10.1
Inleiding
Onder de term exception handling (afhandelen van uitzonderingen) verstaan we het
op een systematische manier verwerken van fouten en andere uitzonderingssituaties die zich tijdens het draaien van een programma kunnen voordoen.
Stel dat je een applet maakt die om de invoer van een aantal cijfers vraagt waarvan
de applet het gemiddelde uitrekent. Om het gemiddelde uit te kunnen rekenen
moet er gedeeld worden. Een simpel voorbeeld van een exceptie die dan kan
optreden is delen door nul. Wanneer zoiets plaatsvindt in een Java-applet zal er een
foutmelding komen in het zogeheten Java console-venster waarover veel browsers
beschikken. Deze melding wordt waarschijnlijk niet door de gebruiker gezien en de
applet zal verder gewoon doordraaien of vastlopen. Dat is een nogal primitieve
manier om een fout ‘op te lossen’. Afhankelijk van de situatie zijn er betere oplossingen denkbaar, zoals bijvoorbeeld een passende mededeling op het scherm zetten
en de gebruiker om nieuwe invoer vragen.
Weer andere excepties doen zich voor als een programma tevergeefs probeert te
schrijven naar een bestand in een directory op een diskette. Er kunnen een heleboel
verschillende redenen zijn waarom dit niet lukt: er zit misschien geen diskette in de
drive, of de drive is niet gesloten, of de diskette is beveiligd tegen schrijven. Ook
kan de diskette stuk of vol zijn, of kan de betreffende directory niet bestaan.
Hoe het programma in zulke uitzonderingsgevallen precies moet handelen is in het
algemeen moeilijk te zeggen. Het programma onderbreken is het meest vergaand.
In veel gevallen is het wenselijk op een of andere manier te proberen de fout te
repareren. In de praktijk blijkt heel vaak dat zo’n reparatie onmogelijk kan gebeuren op de plaats waar de fout is geconstateerd, maar eventueel wel op een ‘hoger
niveau’ in het programma. Hiervoor bestaat in Java een mechanisme dat gebruik
maakt van de woorden try , throw en catch : probeer iets uit, als het mis gaat werp
dan een exceptie en vang deze ergens op.
10.2
Het genereren van een exceptie
In het volgende voorbeeld wordt gevraagd een geheel getal in te voeren in een
tekstvak. Als je dat doet en op Enter drukt wordt de event opgevangen in actionPerformed() . Het ingevoerde getal komt als string binnen en wordt met
Integer.parseInt() omgezet naar een int -waarde, die vermenigvuldigd met 10
in een uitvoervak op het scherm wordt gezet. Zie project InvoerApplet in de map
Voorbeelden\Hoofdstuk 10.
318
OO-PROGRAMMEREN IN JAVA MET BLUEJ
// Genereren van een exceptie
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class InvoerApplet extends JApplet {
public void init() {
setContentPane( new Invoerpaneel() );
}
}
public class Invoerpaneel extends JPanel {
private JTextField invoervak, uitvoervak;
public Invoerpaneel() {
setLayout( new GridLayout( 2, 2 ) );
invoervak = new JTextField( 10 );
invoervak.addActionListener( new VakHandler() );
uitvoervak = new JTextField( 20 );
add(
add(
add(
add(
new JLabel( “Voer een geheel getal in” ) );
invoervak );
new JLabel( “10-voud” ) );
uitvoervak );
}
private class VakHandler implements ActionListener {
public void actionPerformed( ActionEvent e ) {
String invoer = invoervak.getText();
int getal = 10 * Integer.parseInt( invoer );
uitvoervak.setText( “” + getal );
}
}
}
Zie de uitvoer in figuur 10.1.
Figuur 10.1
Het zwakke punt in dit programma is deze regel:
getal = 10 * Integer.parseInt( invoer );
10
Exception handling
319
Zolang de gebruiker een correct geheel getal invoert gaat alles goed. Maar zodra je
iets afwijkends intikt als 5b of hola of in een leeg vak op Enter drukt kan parseInt() zijn werk niet doen: de omzetting naar een int lukt niet. Wat parseInt()
dan doet is een exceptie genereren. Een exceptie is een object dat informatie bevat
over de fout (of uitzondering) die is opgetreden. Een exceptie-object moet je, net als
een event-object, ergens opvangen anders gaat het verloren en, wat erger is, het
programma blijft meestal in een ongewisse toestand. In voorbeeld is er geen voorziening om de exceptie op te vangen.
Als je de applet runt in bijvoorbeeld JCreator zie je de foutmelding over de exceptie
verschijnen in een console-venster. Ook veel browsers hebben een Java consolevenster (dat je aan of uit kunt zetten).
10.3
Het opvangen van een exceptie: try en catch
Het opvangen en afhandelen van een exceptie doe je met behulp van een try catch -blok. Zo’n try -catch -blok ziet er in principe zo uit:
try {
... mogelijk ontstaan van een exceptie
}
catch( Exception e ) {
... afhandeling van de exceptie
}
In het try -blok plaats je de code waar eventueel iets mee mis kan gaan, in dit geval
de omzetting van String naar int door parseInt() . In het catch -blok plaats je
code die zorgt voor de afhandeling van de fout, in dit geval het op het scherm
zetten van een passende mededeling. In de volgende broncode (project TryCatchApplet uit de map Voorbeelden\Hoofdstuk 10) zie je hoe een en ander gaat:
// Opvangen van exceptie bij het
// niet kunnen omzetten van String naar int
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class TryCatchApplet extends JApplet {
public void init() {
setContentPane( new TryCatchpaneel() );
}
}
public class TryCatchpaneel extends JPanel {
private JTextField invoervak, uitvoervak;
public TryCatchpaneel() {
setLayout( new GridLayout( 2, 2 ) );
320
OO-PROGRAMMEREN IN JAVA MET BLUEJ
invoervak = new JTextField( 10 );
invoervak.addActionListener( new VakHandler() );
uitvoervak = new JTextField( 20 );
add(
add(
add(
add(
new JLabel( “Voer een geheel getal in” ) );
invoervak );
new JLabel( “10-voud” ) );
uitvoervak );
}
private class VakHandler implements ActionListener {
public void actionPerformed( ActionEvent e ) {
String invoer = invoervak.getText();
try {
int getal = 10 * Integer.parseInt( invoer );
uitvoervak.setText( “” + getal );
}
catch( NumberFormatException nfe ) {
if( invoer.equals( “” ) )
invoer = “lege invoer”;
uitvoervak.setText( “Fout getal: “ + invoer );
}
}
}
}
De uitvoer van deze applet bij foute invoer is te zien in figuur 10.2
Figuur 10.2
Het verschil tussen het voorbeeld in paragraaf 10.2 en dit voorbeeld zit uitsluitend
in actionPerformed() waar de omzetting van de string invoer naar een gehele
waarde plaatsvindt.
public void actionPerformed( ActionEvent e ) {
String invoer = invoervak.getText();
try {
int getal = 10 * Integer.parseInt( invoer );
uitvoervak.setText( “” + getal );
}
catch( NumberFormatException nfe ) {
if( invoer.equals( “” ) )
invoer = “lege invoer”;
10
Exception handling
321
uitvoervak.setText( “Fout getal: “ + invoer );
}
}
In actionPerformed() staat nu een try -catch -blok, dat is het vetgedrukte gedeelte
vanaf het woord try tot en met de tweede sluitaccolade. Een try -catch -blok omvat
altijd een try -blok en een catch -blok. Het catch -blok heet ook wel een exceptionhandler, of kortweg een handler. Er kan meer dan één handler voorkomen in een
try -catch -blok. Alle handlers bij elkaar vormen dan een handler-list (zie ook
paragraaf 10.5).
Er kunnen zich nu twee situaties voordoen: de invoer is correct of de invoer is
onjuist. In beide gevallen is het programmaverloop anders.
Wat gebeurt er als de invoer onjuist is? Stel dat je als string 325r invoert.
• Het programma haalt eerst de ingevoerde string uit het tekstvak en loopt
vanzelf het try -blok binnen waar de methode parseInt() wordt aangeroepen.
Omdat deze methode de string niet kan converteren naar een geheel getal
‘werpt’ parseInt() een exceptie.
• Bij het werpen van de exceptie wordt de uitvoering van de methode parseInt()
meteen onderbroken en de verdere uitvoering van het try -blok ook. Het laatste
statement in het try -blok wordt in dit geval dus niet uitgevoerd.
• De exceptie die parseInt() genereert is een object van het type NumberFormatException , en het Javasysteem geeft dit object door aan het catch -blok, als er
tenminste een handler is waarvan het argument overeenkomt met het type van
de exceptie. In dit voorbeeld is dat het geval. Het exceptie-object komt het
catch -blok binnen via het argument nfe . Dit argument wordt (in dit voorbeeld)
verder niet gebruikt.
• Het programma gaat dan verder met de uitvoering van de opdrachten in de
handler, hier zijn dat de opdrachten:
if( invoer.equals( “” ) )
invoer = “lege invoer”;
uitvoervak.setText( “Fout getal: “ + invoer );
• Hiermee is de exceptie afgehandeld. Het programma vervolgt zijn weg met de
opdrachten na het catch -blok, in dit geval staan daar geen opdrachten.
Wat gebeurt er als de invoer wel juist is?
• Het programma haalt eerst de ingevoerde string uit het tekstvak en loopt
vanzelf het try -blok binnen waar parseInt() wordt aangeroepen. Als hierin
geen fout optreedt wordt de rest van het try -blok uitgevoerd, het catch -gedeelte wordt in zijn geheel overgeslagen en het programma vervolgt zijn weg met de
opdrachten na het catch -blok, waar in dit geval geen opdrachten staan.
Kort gezegd:
Een statement waarin mogelijk een exceptie kan optreden plaats je in een try -blok.
Bij het daadwerkelijk optreden van een exceptie wordt het try -blok onderbroken
om het catch -blok uit te voeren. Als er geen exceptie optreedt wordt het try -blok
in zijn geheel uitgevoerd en het catch -blok overgeslagen.
322
OO-PROGRAMMEREN IN JAVA MET BLUEJ
10.3.1
Mededeling in een apart venster: JOptionPane
Een fraaie afhandeling van het optreden van een uitzondering in een programma
kan zijn een apart venster te openen en daarin een mededeling aan de gebruiker te
doen. Het volgende voorbeeld is een aangepaste versie van dat uit de vorige
paragraaf. In het volgende voorbeeld komt de mededeling over onjuiste invoer in
een apart dialoogvenster. Dat venster is van het type JOptionPane , een klasse met
een groot aantal voorgedefinieerde vensters. Zie project OptionPaneApplet in de
map Voorbeelden\Hoofdstuk 10.
// Opvangen van een exceptie
// Mededeling in een apart venster
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class OptionPaneApplet extends JApplet {
public void init() {
setContentPane( new Invoerpaneel() );
}
}
public class Invoerpaneel extends JPanel {
private JTextField invoervak, uitvoervak;
public Invoerpaneel() {
setLayout( new GridLayout( 2, 2 ) );
invoervak = new JTextField( 10 );
invoervak.addActionListener( new VakHandler() );
uitvoervak = new JTextField( 20 );
add(
add(
add(
add(
new JLabel( “Voer een geheel getal in” ) );
invoervak );
new JLabel( “10-voud” ) );
uitvoervak );
}
private class VakHandler implements ActionListener {
public void actionPerformed( ActionEvent e ) {
String invoer = invoervak.getText();
try {
int getal = 10 * Integer.parseInt( invoer );
uitvoervak.setText( “” + getal );
}
catch( NumberFormatException nfe ) {
if( invoer.equals( “” ) )
invoer = “lege invoer”;
JOptionPane.showMessageDialog( Invoerpaneel.this,
“Fout getal: “ + invoer,
10
Exception handling
323
“Fout in invoer”,
JOptionPane.ERROR_MESSAGE
);
}
finally {
// zie volgende paragraaf
invoervak.selectAll();
}
}
}
}
In figuur 10.3 zie je het dialoogvenster bij onjuiste invoer.
Figuur 10.3
Het dialoogvenster wordt getoond door middel van de volgende opdracht:
JOptionPane.showMessageDialog(
Invoerpaneel.this,
“Fout getal: “ + invoer,
“Fout in invoer”,
JOptionPane.ERROR_MESSAGE
);
De methode showMessageDialog() is een statische methode, dat wil zeggen dat hij
kan worden aangeroepen met de naam van de klasse in plaats van met een object.
De methode heeft vier argumenten, het eerste argument is een referentie naar het
venster waar het dialoogvenster bij hoort. In dit geval is dat het invoerpaneel.
Vanuit de inwendige klasse VakHandler heet deze referentie Invoerpaneel.this .
Het tweede argument is de mededeling die in het dialoogvenster komt. Het derde
argument is de titel voor het venster en het laatste argument geeft het soort dialoogvenster aan.
Het hier getoonde dialoogvenster is maar één van de talloze mogelijkheden. Zie de
JDK-documentatie voor uitgebreide informatie over JOptionPane .
324
10.3.2
OO-PROGRAMMEREN IN JAVA MET BLUEJ
Finally
Een soms handige voorziening bij een exception handling is een finally -blok. Je
hebt de garantie dat een finally -blok wordt uitgevoerd onafhankelijk van het feit
of er een exceptie is opgetreden of niet. Een finally -is dus ideaal om afsluitende
werkzaamheden te doen, of voorbereidende werkzaamheden voor het verdergaan
van het programma. Schematisch ziet het er zo uit:
try {
// code die exceptie kan opleveren
}
catch( ... ) {
// handel exceptie af
}
catch( ... ) {
// handel andere exceptie af
}
finally {
// afsluitende werkzaamheden
}
// code na finally-blok
Er kan zich een aantal situaties voordoen waarin het programmaverloop verschillend is:
• Er treedt geen exceptie op.
In dit geval voert het programma eerst het try -blok in zijn geheel uit, daarna het
finally -blok en daarna de code na het finally -blok.
• Er treedt in het try -blok een exceptie op die kan worden opgevangen in een
catch -blok.
In dit geval voert het programma eerst het try -blok uit tot de plek waar de
exceptie optreedt, daarna het catch -blok, dan het finally -blok en tot slot de
code na het finally -blok.
• Er treedt in het try -blok een exceptie op die wordt opgevangen in een catch blok en er treedt in deze handler opnieuw een exceptie op.
Dit kan een onverwachte exceptie zijn, het kan ook een rethrow zijn. In elk geval
voert het programma eerst het try -blok uit tot aan de exceptie, dan het catch blok tot de plek waar de exceptie optreedt, vervolgens het finally -blok en geeft
dan de exceptie door naar de aanroepende methode (de eerstvolgende methode
op de call-stack). De code na het finally -blok komt niet aan de beurt.
• Er treedt in het try -blok een exceptie die niet wordt opgevangen door een van
de handlers.
In dit geval voert het programma eerst het try -blok uit tot de plek waar de
exceptie optreedt, dan het finally -blok en geeft vervolgens de exceptie door
naar de aanroepende methode (de eerstvolgende methode op de call-stack). De
code na het finally -blok komt niet aan de beurt.
In alle gevallen komt het finally -blok aan de beurt.
10
Exception handling
325
In het voorbeeld in de vorige paragraaf staat zo’n finally -blok met de opdracht
invoervak.selectAll() :
finally {
invoervak.selectAll();
}
Door de opdracht selectAll() wordt de ingevoerde tekst in het tekstvak geselecteerd, zodat de gebruiker meteen een nieuw getal kan intikken zonder de oude
invoer eerst te hoeven verwijderen.
10.4
Een exceptie opwerpen: throw
In de vorige paragrafen heb je gezien hoe je een exceptie opvangt, maar niet hoe je
zelf een exceptie-object maakt. Ik zal dit laatste toelichten aan de hand van een
ArithmeticException ¸ een exceptie veroorzaakt door een foutieve rekenkundige
bewerking, zoals delen door nul. Merkwaardig is dat deze exceptie automatisch
door het Java-systeem gegenereerd wordt als je een int -waarde door nul deelt,
maar niet als je een double door nul deelt. In dit laatste geval komt er de waarde
Infinity (oneindig) uit. In het volgende voorbeeld (project DeelApplet in de map
Voorbeelden\Hoofdstuk 10) kun je dat zien.
// Delen door 0
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class DeelApplet extends JApplet {
public void init() {
setContentPane( new Invoerpaneel() );
}
}
public class Invoerpaneel extends JPanel {
private JTextField invoervakA, invoervakB, uitvoervak;
public Invoerpaneel() {
setLayout( new GridLayout( 4, 2 ) );
invoervakA = new JTextField( 10 );
invoervakB = new JTextField( 10 );
uitvoervak = new JTextField( 20 );
uitvoervak.setEditable( false );
uitvoervak.setBackground( Color.WHITE );
add( new JLabel( “Getal a” ) );
add( invoervakA );
add( new JLabel( “Getal b” ) );
add( invoervakB );
326
OO-PROGRAMMEREN IN JAVA MET BLUEJ
add( new JLabel( “a gedeeld door b: “ ) );
add( uitvoervak );
JButton deelknop = new JButton( “deel” );
deelknop.addActionListener( new KnopHandler() );
add( new JLabel() ); // leeg label
add( deelknop );
}
private class KnopHandler implements ActionListener {
public void actionPerformed( ActionEvent e ) {
double a = Double.parseDouble( invoervakA.getText() );
double b = Double.parseDouble( invoervakB.getText() );
double resultaat = deel( a, b );
uitvoervak.setText( String.format( “%.4f”, resultaat ) );
}
double deel( double x, double y ) {
return x / y;
}
}
}
De uitvoer staat in figuur 10.4
Figuur 10.4
De feitelijke deling vind plaats in de klasse KnopHandler . Ik heb de uitvoering van
de deling in een aparte methode gezet, om er later nog wat aan toe te kunnen
voegen. De methode deel() is heel simpel: hij deelt zijn argumenten x en y en
levert het resultaat als terugkeerwaarde af. Als y de waarde nul heeft is het resultaat van de deling Infinity . Met Infinity is het lastig verder rekenen, daarom
kun je de methode deel() zo maken dat hij vóór het uitvoeren van de deling
waarschuwt als b de waarde nul heeft. Die waarschuwing komt in de vorm van een
exceptie, en dat gaat zo in zijn werk:
double deel( double x, double y ) {
if( y == 0.0 )
throw new ArithmeticException();
return x / y;
}
10
Exception handling
327
De klasse ArithmeticException is een van de vele voorgedefinieerde exceptieklassen. Met new ArithmeticException() maak je een nieuwe instantie van deze
exceptie en het woord throw betekent letterlijk dat de exceptie ‘gegooid’ wordt naar
de plek vanwaar de methode is aangeroepen. Als de throw -opdracht wordt uitgevoerd (dus als y gelijk aan nul is) wordt de uitvoering van de rest van de methode afgebroken. Dat betekent dat het statement return x/y niet wordt uitgevoerd
en de methode heeft in dat geval geen terugkeerwaarde.
Het is dan ook belangrijk dat programmeurs die de methode aanroepen er van op
de hoogte zijn of er mogelijk een exceptie kan optreden, en zo ja welke exceptie. Als
maker van zo’n methode doe je er dan ook goed aan in de kop van de methode aan
te geven dat deze methode een exceptie kan genereren. Dat doe je met het woord
het woord throws (met een s aan het eind):
double deel( double x, double y ) throws ArithmeticException {
if( y == 0.0 )
throw new ArithmeticException();
return x / y;
}
De uitdrukking throws ArithmeticException betekent dat deze methode mogelijk
een exceptie genereert en de uitdrukking throw new ArithmeticException()
betekent dat er daadwerkelijk een exceptie gemaakt en opgeworpen wordt.
Alle exceptie-klassen hebben twee constructoren: een constructor zonder argumenten (ofwel een default-constructor) en een constructor met een String -argument.
Via dit argument kun je in het exceptie-object bijvoorbeeld een mededeling opbergen over de uitzonderlijke situatie. Deze mededeling kun je weer opvragen met
getMessage() . Het volgende stukje code maakt gebruik van de constructor met
String -argument:
double deel( double a, double b ) throws ArithmeticException {
if( y == 0.0 )
throw new ArithmeticException( “Deling door nul” );
return x / y;
}
Als gebruiker van een methode die een exceptie kan genereren moet je er voor
zorgen de exceptie op te vangen, omdat het programma anders in een ongewisse
toestand verder gaat, de methode deel() levert immers geen terugkeerwaarde als b
gelijk aan nul is. Het opvangen van de exceptie zie je in het volgende voorbeeld
(project DeelApplet2 in de map Voorbeelden\Hoofdstuk 10).
// Opvangen en verwerken van exeptie delen door 0
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class DeelApplet2 extends JApplet {
public void init() {
328
OO-PROGRAMMEREN IN JAVA MET BLUEJ
setContentPane( new Invoerpaneel() );
}
}
public class Invoerpaneel extends JPanel {
private JTextField invoervakA, invoervakB, uitvoervak;
public Invoerpaneel() {
setLayout( new GridLayout( 4, 2 ) );
invoervakA = new JTextField( 10 );
invoervakB = new JTextField( 10 );
uitvoervak = new JTextField( 20 );
uitvoervak.setEditable( false );
uitvoervak.setBackground( Color.YELLOW );
add( new JLabel( “Getal a” ) );
add( invoervakA );
add( new JLabel( “Getal b” ) );
add( invoervakB );
add( new JLabel( “a gedeeld door b: “ ) );
add( uitvoervak );
JButton deelknop = new JButton( “deel” );
deelknop.addActionListener( new KnopHandler() );
add( new JLabel() ); // leeg label
add( deelknop );
}
private class KnopHandler implements ActionListener {
public void actionPerformed( ActionEvent e ) {
double a = Double.parseDouble( invoervakA.getText() );
double b = Double.parseDouble( invoervakB.getText() );
try {
double resultaat = deel( a, b );
uitvoervak.setText(
String.format( Locale.US, “%.4f”, resultaat ) );
}
catch( ArithmeticException ae ) {
JOptionPane.showMessageDialog( Invoerpaneel.this,
ae.getMessage(),
“Fout”,
JOptionPane.ERROR_MESSAGE
);
}
}
double deel( double x, double y ) throws ArithmeticException {
10
Exception handling
329
if( y == 0.0 )
throw new ArithmeticException( “Deling door nul” );
return x / y;
}
}
}
Het resultaat bij deling door 0 is te zien in figuur 10.5.
Figuur 10.5
10.5
Formatteren van uitvoer naar tekstvak
Als je het resultaat van een berekening (een getal) in een tekstvak zet, moet je dat
getal omzetten naar een string. De meest eenvoudige manier om dat te doen is door
middel van de concatenatie met een al of niet lege string, bijvoorbeeld zo:
double resultaat = deel( a, b );
uitvoervak.setText( String.format( “” + resultaat ) );
Nadeel is dat je vaak nogal veel cijfers achter de komma krijgt. Je kunt getallen
formatteren met behulp van de methode format() uit de klasse String . Deze
methode levert een String af. Het is een statische methode, dus je roept hem aan
met de naam van de klasse. De methode format() werkt met een format string op
dezelfde manier als printf() uit hoofdstuk 6.
In de beide applets in paragraaf 10.4 vind je een voorbeeld:
double resultaat = deel( a, b );
uitvoervak.setText( String.format( “%.4f”, resultaat ) );
De uitdrukking String.format( “%.4f”, resultaat ) levert als retourwaarde
een String met een geformatteerde getal met 4 cijfers achter de komma.
330
OO-PROGRAMMEREN IN JAVA MET BLUEJ
Als je liever geen komma maar een decimale punt wilt, kun je dat aangeven met
Locale.US :
String.format( Locale.US, “%.4f”, resultaat )
Zie voor meer mogelijkheden van de format-string de methode printf() in paragraaf 6.6.
10.6
Eén try, twee catches
Een try -blok kun je laten volgen door meer dan één catch -blok. In elk van de
catch-blokken kun je een andere soort exceptie opvangen en afhandelen. In het
programma in de vorige paragraaf kan er niet alleen deling door 0 plaatsvinden,
maar ook kan de gebruiker ongeldige getallen invoeren, of helemaal geen getal. De
methode parseDouble() levert dan, net als parseInt() een NumberFormatException (zie paragraaf 10.3).
Het is mogelijk twee afzonderlijke try -catch -blokken te maken die elk een eigen
exceptie opvangen en afhandelen. Bijvoorbeeld zo:
// Eerste try-catch-blok
try {
double a = Double.parseDouble( invoervakA.getText() );
double b = Double.parseDouble( invoervakB.getText() );
}
catch( NumerFormatException nfe ) {
// Handel foute invoer af
}
// Tweede try-catch-blok
try {
double resultaat = deel( a, b );
uitvoervak.setText( String.format( “%.4f”, resultaat ) );
}
catch( ArithmeticException ae ) {
// Handel delen door 0 af
}
Het is ook mogelijk een try-blok te laten volgen door twee catch-blokken. Elk catchblok vangt dan een aparte exceptie op, zoals in het voorbeeld hieronder.
// Een
import
import
import
try, twee catch-blokken
java.awt.*;
java.awt.event.*;
javax.swing.*;
public class CatchListApplet extends JApplet {
public void init() {
setContentPane( new Invoerpaneel() );
}
}
10
Exception handling
public class Invoerpaneel extends JPanel {
private JTextField invoervakA, invoervakB, uitvoervak;
public Invoerpaneel() {
setLayout( new GridLayout( 4, 2 ) );
invoervakA = new JTextField( 10 );
invoervakB = new JTextField( 10 );
uitvoervak = new JTextField( 20 );
uitvoervak.setEditable( false );
uitvoervak.setBackground( Color.YELLOW );
add( new JLabel( “Getal a” ) );
add( invoervakA );
add( new JLabel( “Getal b” ) );
add( invoervakB );
add( new JLabel( “a gedeeld door b: “ ) );
add( uitvoervak );
JButton deelknop = new JButton( “deel” );
deelknop.addActionListener( new KnopHandler() );
add( new JLabel() ); // leeg label
add( deelknop );
}
private class KnopHandler implements ActionListener {
public void actionPerformed( ActionEvent e ) {
try {
double a = Double.parseDouble( invoervakA.getText() );
double b = Double.parseDouble( invoervakB.getText() );
double resultaat = deel( a, b );
uitvoervak.setText( String.format( “%.4f”, resultaat ) );
}
catch( NumberFormatException nfe ) {
JOptionPane.showMessageDialog( Invoerpaneel.this,
“Fout getal ingevoerd”,
“Fout”,
JOptionPane.ERROR_MESSAGE
);
}
catch( ArithmeticException ae ) {
JOptionPane.showMessageDialog( Invoerpaneel.this,
ae.getMessage(),
“Fout”,
JOptionPane.ERROR_MESSAGE
);
}
}
331
332
OO-PROGRAMMEREN IN JAVA MET BLUEJ
double deel( double x, double y ) throws ArithmeticException {
if( y == 0.0 )
throw new ArithmeticException( “Deling door nul” );
return x / y;
}
}
}
In het try -blok staan drie opdrachten die een exceptie kunnen opleveren:
try {
double a = Double.parseDouble( invoervakA.getText() );
double b = Double.parseDouble( invoervakB.getText() );
double resultaat = deel( a, b );
...
}
De eerste twee statements kunnen een NumberFormatException opleveren die
opgevangen wordt in het eerste catch -blok:
catch( NumberFormatException nfe ) {
...
}
Het derde statement kan een ArithmeticException opwerpen die opgevangen
wordt in het tweede catch -blok:
catch( ArithmeticException ae ) {
...
}
Als je meer dan één catch -blok hebt is soms de volgorde van belang, zie paragraaf
10.7.1.
10.7
De exceptie-klassen
Java kent een uitgebreide hiërarchie van voorgedefinieerde exceptie-klassen. Veel
van deze klassen zijn gedefinieerd in de package java.lang . Dat is een package die
je niet hoeft te importeren omdat dat automatisch wordt gedaan: het is een package
met standaardvoorzieningen van de taal (language) Java.
In figuur 10.6 zie je een stukje van de hiërarchie van deze exceptie-klassen. Het zijn
subklassen van Throwable . Al deze klassen worden gebruikt in de Java-bibliotheek,
maar je kunt ze desgewenst ook in je eigen klassen gebruiken, dat wil zeggen: je
kunt er objecten van maken die je met throw opwerpt. De hiërarchie splitst zich in
twee verschillende takken: Error en Exception . Excepties van het type Error of
van een van zijn subklassen worden gegenereerd door het Java-systeem en komen
zelden voor. Als programmeur hoef je daar geen rekening mee te houden.
10
333
Exception handling
Throwable
checked
unchecked
Exception
Error
IOException
RuntimeException
Figuur 10.6
Van groter belang zijn excepties van het type Exception of van een van zijn subklassen, zie figuur 10.7.
Exception
ClassNotFoundException
CloneNotSupportedException
IllegalAccessException
InstantiationExcepti
IOException
EOFException
FileNotFoundException
IterruptedException
InterruptedIOException
NoSuchFieldException
ObjectStreamException
NoSuchMethodException
InvalidClassException
RuntimeException
ArithmeticException
ArrayStoreException
ClassCastException
IllegalArgumentException
IllegalThreadStateException
NumberFormatException
IllegalMonitorStateException
InvalidObjectException
NotActiveException
NotSerializableException
OptionalDataException
StreamCorruptedException
WriteAbortedException
EOFException
FileNotFoundException
InterruptedIOException
IllegalStateException
IndexOutOfBoundsException
ArrayIndexOutOfBoundsException
StringIndexOutOfBoundsException
NegativeArraySizeException
NullPointerException
SecurityException
UnsupportedOperationException
Figuur 10.7
334
OO-PROGRAMMEREN IN JAVA MET BLUEJ
Zoals je in figuur 10.7 ziet, zijn Exceptions verdeeld in twee soorten: gecontroleeerde
(checked) en ongecontroleerde (unchecked).
Bij een gecontroleerde exceptie heb je als programmeur de plicht om:
• óf de exceptie op te vangen met een try -catch -blok;
• óf de exceptie met throws te melden in de kop van de methode waarin de
exceptie kan optreden.
In het laatste geval geeft de methode de exceptie door aan zijn aanroeper, die op
zijn beurt de exceptie weer moet opvangen of hem melden in de kop van de methode. De compiler controleert of je programma aan deze eisen voldoet.
Voor ongecontroleerde excepties gelden deze eisen niet: je bent niet verplicht ze op
te vangen en je bent niet verplicht ze in de kop van de methode te melden. Wat
zeker niet wil zeggen dat je het daarom altijd moet laten.
Alle excepties van de klasse Exception en de subklassen daarvan zijn gecontroleerde excepties, behalve RuntimeException en zijn subklassen, zie figuur 10.7. Overigens zijn ook excepties van het type Error unchecked.
Ongecontroleerde excepties, dat wil zeggen van het type RuntimeException en
subklassen, duiden soms op programmeerfouten die niet door exception-handling
opgevangen moeten worden, maar door een verbetering aan te brengen in de
broncode van het programma. Een goed voorbeeld daarvan is een IndexOutOfBoundsExceptie die gegenereerd wordt als de index van een array buiten zijn
grenzen treedt, zoals in het volgende fragment:
int[] leeftijdRij = { 18, 21, 34, 17 };
int som = 0;
for( int i = 0; i <= leeftijdRij.length; i++ )
som += leeftijdRij[ i ];
De programmeur heeft hier per ongeluk <= getypt in plaats van < . Het is niet de
bedoeling dat je foute stukjes code omgeeft met een try -catch -blok:
int[] leeftijdRij = { 18, 21, 34, 17 };
int som = 0;
try {
for( int i = 0; i <= leeftijdRij.length; i++ )
som += leeftijRij[ i ];
}
catch( IndexOutOfBoundsException ) {
...Dit is een onzinnig stukje programma
}
Een dergelijke fout moet je verbeteren. Als je zulke fouten maakt is het optreden
van de exceptie een hulpmiddel om de fout op te sporen. Een niet opgevangen
exceptie wordt immers gemeld in het ‘console-venster’ van de ontwikkelomgeving
die je gebruikt.
10
Exception handling
10.7.1
335
Over de volgorde van de catch-blokken
In principe kun je net zoveel catch -blokken achter elkaar zetten als je wilt. Je moet
dan wel rekening houden met de volgorde waarin je de verschillende excepties
opvangt. De reden is de volgende: alle excepties zijn subklassen van Exception , zie
figuur 10.7, en daarom kan een referentie van het statische type Exception wijzen
naar instanties van elk van de subklassen. In principe kun je daarom elke exceptie
opvangen met het volgende stukje code:
try {
// code die excepties kan leveren
}
catch( Exception e ) {
// vang alle mogelijke excepties op
}
Nadeel hiervan is dat je niet weet welke soort exceptie er is opgetreden, zodat het
ook lastig is een passende maatregel te nemen. Daarom is het in het algemeen beter
de verschillende soorten excepties apart op te vangen in hun eigen blok.
Bij het optreden van een exceptie doorzoekt het Java-systeem de diverse catch blokken (als die er zijn) van boven naar beneden en het eerste passende catch -blok
dat gevonden wordt mag de exceptie afhandelen. Als catch(Exception e) als
eerste in de lijst van catch -blokken staat zal deze alle excepties afhandelen, waardoor de andere catch -blokken overbodig zijn. De compiler reageert hier dan ook
op met een foutmelding. Een juiste volgorde is bijvoorbeeld:
try {
// code die diverse soorten excepties kan leveren
}
catch( NumberFormatException nfe ) {
// vang NumberFormatException op
}
catch( ArithmeticException ae ) {
// vang ArithmeticException op
}
catch( Exception ex ) {
// vang alle overige excepties op
}
Het blok catch(Exception ex) staat aan het einde van de lijst, omdat de klasse
Exception hoog staat in de hiërarchie van exceptie-klassen.
10.8
Wat gebeurt er als je een exceptie niet opvangt?
Gecontroleerde excepties moet je opvangen, als je dat vergeet geeft de compiler
daarover een melding. Ongecontroleerde excepties die je niet opvangt leveren een
mededeling in het console-venster, ongeveer zoals in figuur 10.9 te zien is. Ontwikkelomgevingen als JCreator of JBuilder hebben hun eigen console-venster.
336
OO-PROGRAMMEREN IN JAVA MET BLUEJ
Als je buiten een ontwikkelomgeving een Java-applicatie runt, of een Java-applet in
een browser laat uitvoeren, kun je het console-venster openen met de rechter
muisknop te klikken op het icoontje met het koffiekopje in de taakbalk, zie figuur
10.8
Figuur 10.8
Het venster dat je in figuur 10.9 ziet is afkomstig van de Java Runtime Environment
(JRE) dat onder andere gebruikt wordt door een browser die voorzien is van de
Java Plug-in.
Figuur 10.9
10.8.1
De call stack
Wat stellen de regels in figuur 10.9 precies voor? Wat je ziet is de zogenoemde cal
stack, de stapel met informatie over welke methoden zijn aangeroepen voordat de
exceptie is opgetreden.
10
Exception handling
337
Een stack of stapel is een datastructuur waarin je gegevens opbergt door ze als het
ware steeds bovenop een stapel te leggen. In figuur 10.10 zie je drie stadia van een
stapel die wordt gevuld met achtereenvolgens de gegevens a, b en c.
c
a
b
b
a
a
Figuur 10.10
Een stack is zo gemaakt dat je de gegevens alleen van de bovenkant van de stapel
kunt afhalen. Als je in figuur 10.10 de gegevens van de stack haalt krijg je eerst c,
dan b en tot slot a. De gegevens komen dus in de omgekeerde volgorde uit de
stack. Dit principe wordt vaak aangeduid met LIFO: Last In First Out.
Elk programma, niet alleen Java-programma’s, hebben een call-stack met informatie over de aangeroepen, maar nog niet volledig afgehandelde methoden. Als je
vanuit een methode a() methode b() aanroept, en van daaruit c() , moet het
programma na afloop van c() verdergaan met de rest van b() en na het beëindigen
van b() met de rest van a() . In dit voorbeeld gebeurt het aanroepen van de methoden in de volgorde a , b , c en het beëindigen van de methoden gebeurt in de omgekeerde volgorde c , b , a . Dit is een ideale situatie voor het gebruik van een stack.
Een overzicht van de call-stack van een Java-programma kun je op het scherm
krijgen met de methode printStackTrace() uit de klasse Throwable . Deze methode wordt door alle subklassen van Throwable , dus door alle exceptie-klassen
geërfd. In het volgende schematische programma zie je hoe je zo’n stack-trace op
het (console-)venster krijgt. Zie project StacktraceApplet in de map
Voorbeelden\Hoofdstuk 10.
// Laten zien van de call-stack met printStackTrace()
import java.awt.*;
import java.applet.*;
import javax.swing.*;
public class StacktraceApplet extends JApplet {
public void init() {
setContentPane( new Paneel() );
}
}
public class Paneel extends JPanel {
public Paneel() {
try {
test1();
}
catch( RuntimeException re ) {
System.out.println( “Dit is de stack-trace:” );
338
OO-PROGRAMMEREN IN JAVA MET BLUEJ
re.printStackTrace();
}
}
public void paintComponent( Graphics g ) {
super.paintComponent( g );
g.drawString( “Call-stack staat in console-venster.”, 20, 20 );
}
public void test1() {
test2();
}
public void test2() {
throw new RuntimeException();
}
}
Figuur 10.11
De uitvoer van dit voorbeeld komt in het console-venster, zie figuur 10.11. De
stack-trace begint met:
java.lang.RuntimeException
Deze regel vertelt dat er een RuntimeException is opgetreden.
Op de regel daaronder staat waar die exceptie plaatsvond:
at Paneel.test2(StacktraceApplet.java:33)
Hier staat: in de methode test2() van de klasse Paneel , om precies te zijn in regel
33 van het bestand StacktraceApplet.java .
De volgende regel vertelt vanwaar de methode test2() werd aangeroepen:
at Paneel.test1(StacktraceApplet.java:29)
Namelijk in de methode test1() van de klasse Paneel , op regel 29 van het bestand
StacktraceApplet.java .
10
Exception handling
339
Verder zie je dat deze methode op zijn beurt in regel 15 werd aangeroepen in
<init> van Paneel , dat is de constructor. Deze constructor werd aangeroepen in de
methode init() van de klasse StacktraceApplet . De laatste twee regels duiden
op methoden uit het Java-systeem die nodig zijn om de applet op te starten.
Nu je weet wat een call-stack is kan ik de vraag van de titel van paragraaf beantwoorden: wat gebeurt er als je een exceptie niet opvangt? Bekijk het volgende, wat
schematische stukje code waarin de exceptie niet wordt opgevangen (zie project
ExceptieApplet uit de map Voorbeelden\Hoofdstuk 10).
public class ExceptieApplet extends JApplet {
public void init() {
setContentPane( new Paneel() );
System.out.println( “In init()” );
}
}
public class Paneel extends JPanel {
private int getal = 1;
public Paneel() {
test1();
System.out.println( “In constructor van Paneel” );
}
public void test1() {
test2();
System.out.println( “In test1” );
}
public void test2() {
if ( getal == 1 )
throw new RuntimeException();
System.out.println( “In test2” );
}
}
Bij de uitvoering van deze applet gebeurt het volgende:
• Het Java-systeem roept de methode init() van ExceptieApplet aan.
• In init() wordt de constructor van Paneel aangeroepen.
• De constructor van Paneel roept test1() aan.
• test1() roept test2() aan.
• In test2() ontstaat de exceptie.
340
OO-PROGRAMMEREN IN JAVA MET BLUEJ
De call-stack ziet er dus zo uit:
Paneel.test2
Paneel.test1
Paneel.<init>
ExceptieApplet.init
Javasysteem
Bij het optreden van de exceptie in test2() breekt het Java-systeem de uitvoering
van de methode onmiddellijk af. Dat betekent bijvoorbeeld dat de eventuele
opdrachten in test2() die na de exceptie staan, niet worden uitgevoerd. Dus de
mededeling “In test2” komt niet op het scherm.
Nu gebeurt er het volgende: omdat de exceptie niet in een try -blok is ontstaan en
er dus geen passend catch -blok kan zijn, geeft het systeem de exceptie door aan
eerstvolgende methode op de call-stack, dat is de methode test1() . De verdere
uitvoering van deze methode wordt onderbroken, dus de mededeling “In test2”
komt niet op het scherm.
Het systeem geeft de exceptie door aan de volgende methode op de call-stack, de
constructor van Paneel . Ook hier ontbreekt een exception-handler, de uitvoering
van de constructor wordt onderbroken, de mededeling “In constructor van
Paneel” komt niet op het scherm en de exceptie wordt doorgegeven aan init()
van ExceptieApplet . Ook de uitvoering van deze methode wordt onderbroken, de
mededeling “In init()” komt niet op het scherm en de exceptie komt terecht
buiten het programma in het Java-systeem.
Het Java-systeem vangt alle niet eerder opgevangen excepties op, maar weet
uiteraard niet hoe elke exceptie afgehandeld moet worden. Het systeem reageert
daarom alleen met het automatisch aanroepen van printStackTrace() .
Afgezien van het tonen van de call-stack hoef je aan een applet niet veel te merken
als je een exceptie niet opvangt. De vraag is wel in welke toestand de applet zich
bevindt. Uit de zojuist gegeven beschrijving blijkt immers dat een deel van de
methoden die aangeroepen werden voor de exceptie plaatsvond, niet worden
afgemaakt. In ExceptieApplet zijn dat de methoden test2() , test1() , de constructor van Paneel en init() . Voor elke applet is het essentieel dat init() wel op
een normale manier wordt beëindigd. Als dat niet het geval is krijg je de melding:
Applet not initialized , en zie je verder niets op het scherm.
Als je daarentegen de aanroep test1() van init() naar paintComponent() van
Paneel zou verplaatsen, zou de applet op een betrekkelijk normale manier worden
uitgevoerd. Of een applet wel of niet normaal functioneert na een niet opgevangen
exceptie hangt af van de plaats van de exceptie en met name van de opdrachten die
niet aan bod konden in de methoden van de call-stack doordat de exceptie dat
verhinderde. Anders gezegd: als je een exceptie niet opvangt hangt het welslagen
10
Exception handling
341
van je programma af van het toeval. Het is dus raadzaam de oorzaak van elke
exceptie op te sporen en bij het optreden van een RuntimeException (of een van de
subklassen) zo mogelijk de oorzaak uit de weg te ruimen, of anders de exceptie op
te vangen.
Niet alleen voor een applet, maar ook voor het niet opvangen van een exceptie in
een grafische applicatie gelden dezelfde overwegingen. Naast grafische applicaties
kun je in Java ook niet-grafische applicaties (console-applicaties) maken. Het niet
opvangen van een exceptie in een niet-grafische applicatie leidt altijd tot het
stoppen van de applicatie.
10.9
Rethrow
Een rethrow is het opnieuw genereren van dezelfde exceptie die in een handler is
opgevangen. Je kunt vaak een exceptie beter doorgeven aan een ‘hoger’ niveau, dat
wil zeggen aan een methode die eerder op de call-stack is terechtgekomen. Schematisch komt een rethrow op het hierop neer:
try {
// code die exceptie kan opleveren
}
catch( Exception e ) {
// doe wat afsluitende dingen
throw e; // gooi de exceptie opnieuw
}
Eventueel kun je in een finally-blok (zie paragraaf 10.3.2) opdrachten zetten die in
alle gevallen uitgevoerd moeten worden, zowel bij het niet optreden van een
exceptie als bij een rethrow.
10.10
Samenvatting
• Een fout of andere uitzonderlijke situatie die optreedt tijdens het draaien van
een programma heet een exceptie.
• In Java is een exceptie een instantie van een van de exceptie-klassen.
• Het mechanisme van het afhandelen van een exceptie bestaat uit het gooien van
een exceptie-object, het opvangen van dat object en het nemen van passende
maatregelen.
• Er zijn in Java twee soorten excepties: checked (gecontroleerde) en unchecked
(niet gecontroleerde). Checked excepties moet je altijd opvangen, de compiler
controleert of je dat inderdaad doet.
• Exception handling gebeurt in Java met behulp van een try -catch -blok.
• Je kunt verschillende soorten excepties die optreden na een try opvangen in
verschillende catch -blokken.
• De volgorde van de catch-blokken is soms van belang: eerst de meest specialistische excepties, daarna de algemenere.
342
OO-PROGRAMMEREN IN JAVA MET BLUEJ
• Een try-catch -blok kun je afsluiten met een finally -blok waarin code staat die
altijd wordt uitgevoerd.
10.11
Vragen
1. Wat is exception-handling?
2. Wat is een try -blok? Een catch -blok?
3. Als je een catch -lijst hebt met meer dan één catch -blok, in welke volgorde
moeten deze dan staan?
4. Wat is het verschil tussen een unchecked en een checked exceptie?
5. Moet je een exceptie altijd opvangen?
6. Als je een exceptie niet opvangt wat gebeurt er dan?
7. Wat is een rethrow?
8. Waar dient een finally -blok voor?
9. Waarom is het aanroepen van printStackTrace() bij het opvangen van een
exceptie handig?
10.12
Opgaven
1. Schrijf een applicatie die aan de gebruiker een getal vraagt waarvan het programma de wortel bepaalt en op het scherm laat zien. Voor het berekenen van
de wortel kun je de methode Math.sqrt() gebruiken. Ga na welke melding je
krijgt wanneer je een negatief getal intikt.
Pas het programma zodanig aan dat alle situaties netjes afgehandeld worden:
• voor een positief getal (of 0) wordt de vierkantswortel getoond;
• voor een negatief getal krijg je de melding dat een vierkantswortel niet
gedefinieerd is;
• wanneer je geen geldig getal intikt moet een aangepaste melding verschijnen.
2. Schrijf een applicatie die er bij de start uitziet als in figuur 10.12. Maak gebruik
van een array van knoppen, maar gebruik geen lay-outmanager.
Figuur 10.12
Wanneer de gebruiker op een correcte manier zijn keuze bekend maakt door een
getal in te tikken en op Enter te drukken, krijg je een resultaat als in figuur 10.13
10
Exception handling
343
(bedenk dat gebruikers van 1 tot en met 5 tellen, maar de array-indexen van 0
tot en met 4 lopen):
Figuur 10.13
Handel de excepties die optreden als de gebruiker een te groot of te klein getal,
of een letter of string intikt, op een correcte manier af.
Download