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.