You are on page 1of 280

1

Korte inhoudsopgave
1 Programmeren 2 Java 14 25 36 49 5

3 Tekenen en rekenen 4 Nieuwe methoden

5 Objecten en methoden 6 Invloed van buiten 7 Herhaling 8 Keuze 87 77 65

9 Objecten en klassen 10 Overerving 119

101

11 Strings en Arrays

126 142 168

12 Ontwerp van programmas 13 Objectgeori enteerd ontwerp 14 Algoritmen 219

A Gebruik van de compiler met Eclipse B Gebruik van de compiler met JCreator C Programmeerprojecten 253 261

241 247

D Standaardklassen en -methoden E Operatoren 272 273

F Gereserveerde woorden G Syntax 274

Inhoudsopgave
1 Programmeren 5 1.1 Computers en programmas 5 1.2 Orde in de chaos 6 1.3 Programmeerparadigmas 8 1.4 Programmeertalen 8 1.5 Vertalen van programmas 11 1.6 Programmeren 13 2 Java 14 2.1 Omgeving van het programma 14 2.2 Opbouw van een programma 15 2.3 Modules 15 2.4 Methode-denitie 17 2.5 Opdrachten 19 2.6 Methoden en parameters 20 2.7 Naamgeving 20 2.8 Bibliotheek-klassen 21 2.9 Ontwikkelomgevingen 22 3 Tekenen en rekenen 25 3.1 Graphics 25 3.2 Variabelen 26 3.3 Berekeningen 31 3.4 Programma-layout 32 3.5 Declaraties met initialisatie

33

4 Nieuwe methoden 36 4.1 Methode-denitie 36 4.2 Op zoek naar parameters 39 4.3 Methoden met een resultaat 41 5 Objecten en methoden 49 5.1 Variabelen 49 5.2 Typering 54 5.3 Methoden 56 5.4 Constanten 59 5.5 Toepassing: Intro-scherm 61 6 Invloed van buiten 65 6.1 Applets parametriseren 65 6.2 Utility-klassen 67 6.3 Interactie via objecten 68 6.4 Interactie-componenten 70 6.5 Interactie met de gebruiker 73

INHOUDSOPGAVE 7 Herhaling 77 7.1 De while-opdracht 77 7.2 Boolean waarden 78 7.3 De for-opdracht 80 7.4 Bijzondere herhalingen 82 7.5 Toepassing: renteberekening 83 8 Keuze 87 8.1 De if-opdracht 87 8.2 Toepassingen 88 8.3 Graek en nulpunten van een parabool 8.4 Exceptions 98 9 Objecten en klassen 101 9.1 Klasse: beschrijving van een object 101 9.2 Toepassing: Bewegende deeltjes 104 9.3 Animatie 109 9.4 Klasse-ontwerp en -gebruik 116 9.5 Klassen in de Java-libraries 116 10 Overerving 119 10.1 Subklassen 119 10.2 Klasse-hi erarchie en 122 erarchie en in de Java-libraries 10.3 Klasse-hi

92

123

11 Strings en Arrays 126 11.1 Strings en characters 126 11.2 Arrays 130 11.3 Toepassing: CirkelKlikker 131 11.4 Toepassing: Tekst-analyse met letterfrequentie 11.5 Syntax van arrays 138 12 Ontwerp van programmas 142 12.1 Layout van de userinterface 142 12.2 Toepassing: Rekenmachine 144 12.3 Applications 148 12.4 Menus en WindowEvents 150 12.5 Toepassing: een bitmap-editor 151 12.6 Details van de bitmap-editor 154 13 Objectgeori enteerd ontwerp 168 13.1 Abstracte klassen en interfaces 168 13.2 Collections 173 13.3 Uitbreidingen van AWT 181 13.4 Toepassing: een schets-programma 186 13.5 File input/output 205 13.6 Non-window-programmas 214 14 Algoritmen 219 14.1 Toepassing: een zoekend programma 219 14.2 Het zoekalgoritme 223 14.3 Toepassing: automatische taalherkenning 231

134

4 A Gebruik van de compiler met Eclipse 241 A.1 Installatie van de software 241 A.2 Conguratie van de Eclipse IDE 242 A.3 Een programma schrijven en uitvoeren met Eclipse

INHOUDSOPGAVE

244

B Gebruik van de compiler met JCreator 247 B.1 Installatie van de software 247 B.2 Conguratie van de JCreator IDE 248 B.3 Een programma schrijven en uitvoeren met JCreator 250 C Programmeerprojecten C.1 Mandelbrot 253 C.2 Reversi-spel 256 C.3 SchetsPlus 258 253

D Standaardklassen en -methoden D.1 package java.lang 261 D.2 package java.util 262 D.3 package java.awt 264 D.4 package javax.swing 267 D.5 package java.awt.event 268 D.6 package java.net 268 D.7 package java.io 269 D.8 hoofdprogramma 270 D.9 primitieve types 271 E Operatoren 272 273

261

F Gereserveerde woorden G Syntax 274

Hoofdstuk 1

Programmeren
1.1 Computers en programmas
Computer: processor plus geheugen Een computer bestaat uit tientallen verschillende onderdelen, en het is een vak apart om dat allemaal te beschrijven. Maar als je het heel globaal aanpakt, kun je het eigenlijk met twee woorden zeggen: een computer bestaat uit een processor en uit geheugen. Dat geheugen kan allerlei vormen aannemen, voornamelijk verschillend in de snelheid van gegevensoverdracht en de toegangssnelheid. Sommig geheugen kun je lezen en schrijven, sommig geheugen alleen lezen of alleen met wat meer moeite beschrijven, en er is geheugen dat je alleen kunt beschrijven. Invoer- en uitvoer-apparatuur (toetsenbord, muis, monitor, printer enz.) lijken op het eerste gezicht buiten de categorie en processor en geheugen te vallen, maar als je ze maar abstract genoeg beschouwt vallen ze in de categorie geheugen: een toetsenbord is read only geheugen, en een monitor is write only geheugen. Ook het modem en de netwerkkaart, en met een beetje goede wil zelfs de geluidkaart, zijn een vorm van geheugen. De processor, daarentegen, is een wezenlijk ander onderdeel. Taak van de processor is het uitvoeren van opdrachten. Die opdrachten hebben als eect dat het geheugen wordt veranderd. Zeker met onze ruime denitie van geheugen verandert of inspecteert praktisch elke opdracht die de processor uitvoert het geheugen. Opdracht: voorschrift om geheugen te veranderen Een opdracht is dus een voorschrift om het geheugen te veranderen. De opdrachten staan zelf ook in het geheugen (eerst op een disk, en terwijl het wordt uitgevoerd ook in het RAM-geheugen). In principe zou het programma opdrachten kunnen bevatten om een ander deel van het programma te veranderen. Dat idee is een tijdje erg in de mode geweest (en de verwachtingen voor de kunstmatige intelligentie waren hooggespannen), maar dat soort programmas bleken wel erg lastig te schrijvenze veranderen waar je bij staat! We houden het er dus maar op dat het programma in een afzonderlijk deel van het geheugen staat, apart van het deel van het geheugen dat door het programma wordt veranderd. Het programma wordt, alvorens het uit te voeren, natuurlijk wel in het geheugen geplaatst. Dat is de taak van een gespecialiseerd programma, dat we een operating system noemen (of anders een virus). Programma: lange reeks opdrachten Ondertussen zijn we aan een denitie van een programma gekomen: een programma is een (lange) reeks opdrachten, die -als ze door de processor worden uitgevoerd- het doel hebben om het geheugen te veranderen. Programmeren is de activiteit om dat programma op te stellen. Dat vergt het nodige voorstellingsvermogen, want je moet je de hele tijd bewust zijn wat er met het geheugen zal gebeuren, later, als het programma zal worden uitgevoerd. Voorbeelden van programmas in het dagelijks leven zijn talloos, als je bereid bent om het begrip geheugen nog wat ruimer op te vatten: kookrecepten, breipatronen, routebeschrijvingen, ambtelijke procedures, het protocol voor de troonswisseling: het zijn allemaal reeksen opdrachten, die als ze worden uitgevoerd, een bepaald eect hebben. Programmeertaal: notatie voor programmas De opdrachten die samen het programma vormen moeten op een of andere manier geformuleerd. Dat zou met schemas of handbewegingen kunnen, maar in de praktijk gebeurt het vrijwel altijd

Programmeren

door de opdrachten in tekst-vorm te coderen. Er zijn vele verschillende notaties in gebruik om het programma mee te formuleren. Zon verzameling notatie-afspraken heet een programmeertaal. Daar zijn er in de recente geschiedenis nogal veel van bedacht, want telkens als iemand een n og handigere notatie bedenkt om een bepaald soort opdrachten op te schrijven wordt dat al gauw een nieuwe programmeertaal. Hoeveel programmeertalen er bestaan is moeilijk te zeggen, want het ligt er maar aan wat je meetelt: versies, dialecten enz. In Wikipedia (en.wikipedia.org/wiki/List of programming languages) staat een overzicht van bijna 1000 talen, naar keuze alfabetisch, historisch, of naar afkomst gesorteerd. Het heeft weinig zin om die talen allemaal te gaan leren, en dat hoeft ook niet, want er is veel overeenkomst tussen talen. Wel is het zo dat er in de afgelopen 60 jaar een ontwikkeling heeft plaatsgevonden in programmeertalen. Ging het er eerst om om steeds meer nieuwe mogelijkheden van computers te gebruiken, tegenwoordig ligt de nadruk er op om een beetje orde te scheppen in de chaos die het programmeren anders dreigt te veroorzaken.

1.2

Orde in de chaos

Omvang van het geheugen Weinig zaken hebben zon spectaculaire groei doorgemaakt als de omvang van het geheugen van computers. In 1948 werd een voorstel van Alan Turing om een ( e en) computer te bouwen met een geheugencapaciteit van 6 kilobyte nog afgekeurd (te ambitieus, te duur!). Tegenwoordig zit dat geheugen al op de klantenkaart van de kruidenier. Maar ook recent is de groei er nog niet uit: tien jaar geleden had de modale PC een geheugen van 4 megabyte, en niet van 1024 megabyte zoals nu. Voor disks geldt een zelfde ontwikkeling: tien jaar geleden was 300 megabyte best acceptabel, nu is dat eerder 300 gigabyte. En wat zouden we over tien jaar denken van onze huidige 4 gigabyte DVDtjes? Variabele: geheugenplaats met een naam Het geheugen is voor programmas aanspreekbaar in de vorm van variabelen. Een variabele is een plaats in het geheugen met een naam. Een opdracht in het programma kan dan zijn om bepaalde, bij naam genoemde, variabele te veranderen. Voor kleine programmas gaat dat prima: enkele tientallen variabelen zijn nog wel uit elkaar te houden. Maar als we al die nieuw verworven megabytes met aparte variabelen gaan vullen, worden dat er zoveel dat we daar het overzicht totaal over verliezen. In wat oudere programmeertalen is het om die reden dan ook vrijwel niet mogelijk te voldoen aan de eisen die tegenwoordig aan programmatuur wordt gesteld (windowinterface, geheel congureerbaar, what-you-see-is-what-you-get, gebruik van alle denkbare rand- en communicatieapparatuur, onafhankelijk van taal, cultuur en schriftsoort, ge ntegreerde online help en zelfdenkende wizards voor alle klusjes. . . ). Object: groepje variabelen Er is een bekende oplossing die je kunt gebruiken als, door het grote aantal, dingen onoverzichtelijk dreigen te worden: groeperen, en de groepjes een naam geven. Dat werkt voor personen in verenigingen, verenigingen in bonden, en bonden in federaties; het werkt voor gemeenten in provincies, provincies in deelstaten, deelstaten in landen, en landen in unies; het werkt voor werknemers in afdelingen, afdelingen in divisies, divisies in bedrijven, bedrijven in holdings; het werkt voor universiteits-medewerkers in leerstoelgroepen, leerstoelgroepen in instituten, instituten in faculteiten, faculteiten in universiteiten, en universiteiten in regionale clusters. Dat moet voor variabelen ook kunnen werken. Een groepje variabelen die bij elkaar horen en als geheel met een naam kan worden aangeduid, staat bekend als een object. In de zogenaamde objectgeori enteerde programmeertalen kunnen objecten ook weer in een variabele worden opgeslagen, en als zodanig deel uitmaken van grotere objecten. Zo kun je in programmas steeds grotere gehelen manipuleren, zonder dat je steeds met een overweldigende hoeveelheid details wordt geconfronteerd. Omvang van programmas Programmas staan ook in het geheugen, en omdat daar zo veel van beschikbaar is, worden programmas steeds groter. Twintig jaar geleden pasten operating system, programmeertaal en tekstverwerker samen in een ROM van 16 kilobyte; de nieuwste tekstverwerkers worden geleverd op meerdere CDs ` a 640 megabyte.

1.2 Orde in de chaos

library

package

package

package

package

package

klasse

klasse

klasse

klasse

klasse

methode

methode

methode

methode

methode opdracht opdracht opdracht

Figuur 1: Terminologie voor hi erarchische structurering van programmas

In een programma staan een enorme hoeveelheid opdrachten, en het is voor e en persoon totaal niet meer te bevatten wat die opdrachten precies doen. Erger is, dat ook met een team er moeilijk uit te komen is: steeds moet zon team weer vergaderen over de precieze taakverdeling. Methode: groepje opdrachten met een naam Het recept is bekend: we moeten wat orde in de chaos scheppen door de opdrachten te groeperen, en van een naam te voorzien. We kunnen dan door het noemen van de naam nonchalant grote hoeveelheden opdrachten aanduiden, zonder ons steeds in alle details te verdiepen. Dat is de enige manier om de complexiteit van grote programmas nog te kunnen overzien. Dit principe is al vrij oud, al wordt zon groepje opdrachten door de geschiedenis heen steeds anders genoemd (de naam van elk apart groepje wordt uiteraard door de programmeur bepaald, maar het gaat hier om de naam van de naamgevings-activiteit. . . ). In de vijftiger jaren van de vorige eeuw heette een van naam voorzien groepje opdrachten een subroutine. In de zestiger jaren ging men spreken van een procedure. In de tachtiger jaren was de functie in de mode, en in de jaren negentig moest je van een methode spreken om er nog bij te horen. We houden het nog steeds maar op methode, maar hoe je het ook noemt: het gaat er om dat de complexiteit van lange reeksen opdrachten nog een beetje te beheersen blijft door ze in groepjes in te delen, en het groepje van een naam te voorzien. Klasse: groepje methoden met een naam Decennia lang kon men heel redelijk uit de voeten met hun procedures. Maar met de steeds maar groeiende programmas onstond er een nieuw probleem: het grote aantal procedures werd te onoverzichtelijk om nog goed hanteerbaar te zijn. Het recept is bekend: zet de procedures in samenhangende groepjes bij elkaar en behandel ze waar mogelijk als e en geheel. Zon groepje heet een klasse. En als om de overgang naar deze nieuwe zienswijze te benadrukken ging men, sinds de groepjes opdrachten in klassen gebundeld werden, de groepjes opdrachten in de bundel methoden noemen. Packages: groepje klassen met een naam Niet iedereen hoeft opnieuw het wiel uit te vinden. Door de jaren heen zijn er vele klassen geschreven, die in andere situaties opnieuw bruikbaar zijn. Vroeger heette dat de standard library, maar naarmate het er meer werden, en er ook alternatieve libraries ontstonden, werd het handig om ook klassen weer in groepjes te bundelen. Zon groepje klassen (bijvoorbeeld: alles wat met le-input/output te maken heeft, of alles wat met interactieve interfaces te maken heeft) wordt tegenwoordig vaak een package genoemd.

Programmeren

1.3

Programmeerparadigmas

Imperatief programmeren: gebaseerd op opdrachten Ook in de wereld van de programmeertalen kunnen we wel wat orde in de chaos gebruiken. Programmeertalen die bepaalde eigenschappen gemeen hebben behoren tot hetzelfde programmeerparadigma. (Het woord paradigma is gestolen van de wetenschapslosoe, waar het een gemeenschappelijk kader van theorievorming in een bepaalde periode aanduidt; heel toepasselijk dus.) Een grote groep programmeertalen behoort tot het imperatieve paradigma; dit zijn dus imperatieve programmeertalen. In het woord imperatief herken je de gebiedende wijs; imperatieve programmeertalen zijn dan ook talen die gebaseerd zijn op opdrachten om het geheugen te veranderen. Imperatieve talen sluiten dus direct aan op het besproken computermodel met processor en geheugen. In deze cursus staat een imperatieve programmeertaal centraal, en dat verklaart dan ook de naam van de cursus. Declaratief programmeren: gebaseerd op functies Het feit dat we de moeite nemen om de imperatieve talen als zodanig te benoemen doet vermoeden dat er nog andere paradigmas zijn, waarin geen opdrachten gebruikt worden. Kan dat dan? Wat doet de processor, als hij geen opdrachten uitvoert? Het antwoord is, dat de processor weliswaar altijd opdrachten uitvoert, maar dat je dat in de programmeertaal niet noodzakelijk hoeft terug te zien. Denk bijvoorbeeld aan het maken van een ingewikkeld spreadsheet, waarbij je allerlei verbanden legt tussen de cellen op het werkblad. Dit is een activiteit die je programmeren kunt noemen, en het nog-niet-ingevulde spreadsheet is het programma, klaar om actuele gegevens te verwerken. Het programma is niet op het geven van opdrachten gebaseerd, maar veeleer op het leggen functionele verbanden tussen de diverse cellen. Naast dit soort functionele programmeertalen zijn er nog talen die op de propositielogica zijn gebaseerd: de logische programmeertalen. Samen staan deze bekend als het declaratieve paradigma. Maar daar gaat deze cursus dus niet over. Procedureel programmeren: imperatief + methoden Programmeertalen waarin procedures (of methoden, zoals we tegenwoordig zouden zeggen) een prominente rol spelen, behoren tot het procedurele paradigma. Alle procedurele talen zijn bovendien imperatief: in die procedures staan immers opdrachten, en de aanwezigheid daarvan maakt een taal imperatief. Object-geori enteerd programmeren: procedureel + objecten Weer een uitbreiding van procedurele talen vormen de object-geori enteerde talen. Hierin kunnen niet alleen opdrachten gebundeld worden in procedures (of liever: methoden), maar kunnen bovendien variabelen gebundeld worden in objecten. Je ziet het procedurele en het objectieve paradigma wel eens als contrast gepresenteerd: programmeer jij procedureel of object-geori enteerd?. Zon vraag berust op een misverstand; het moet zijn: programmeer jij procedureel, of ook object-geori enteerd?.

1.4

Programmeertalen

Imperatieve talen: Assembler, Fortran, Basic De allereerste computers werden geprogrammeerd door de instructies voor de processor direct, in getalvorm, in het geheugen neer te zetten. Al snel kreeg men door dat het handig was om voor die instructies gemakkelijk te onthouden afkortingen te gebruiken, in plaats van getallen. Daarmee was rond 1950 de eerste echte programmeertaal ontstaan, die Assembler werd genoemd, omdat je er gemakkelijk programmas mee kon bouwen (to assemble). Elke processor heeft echter zijn eigen instructies, dus een programma in Assembler is speciek voor een bepaalde processor. Je kunt dus eigenlijk niet spreken van de taal Assembler, maar moet liever spreken van Assembler-talen. Dat was natuurlijk niet handig, want als er een nieuwe type processor wordt ontwikkeld zijn al je oude programmas waardeloos geworden. Een nieuwe doorbraak was rond 1955 de taal Fortran (een afkorting van formula translator). De opdrachten in deze taal waren niet speciek ge ent op een bepaalde processor, maar konden (met een speciaal programma) worden vertaald naar diverse processoren. De taal werd veel gebruikt voor technisch-wetenschappelijke toepassingen. Nog steeds trouwens; niet dat modernere talen daar niet geschikt voor zouden zijn, maar omdat er in de loop

1.4 Programmeertalen

Imperatief Fortran Procedureel Pascal PHP

Assembler

Declaratief Functioneel ML Excel Lisp Scheme Haskell

Basic Algol C Python

Object-georinteerd Simula C# C++ Java

Logisch Prolog

Figuur 2: Programmeerparadigmas

der jaren nu eenmaal veel programmatuur is ontwikkeld, en ook omdat mensen niet zo gemakkelijk van een eenmaal aangeleerde taal afstappen. Voor beginners was Fortran een niet zo toegankelijke taal. Dat was aanvankelijk niet zo erg, want zon dure computer gaf je natuurlijk niet in handen van beginners. Maar na verloop van tijd (omstreeks 1965) kwam er toch de behoefte aan een taal die wat gemakkelijker in gebruik was, en zo ontstond Basic (Beginners All-purpose Symbolic Instruction Code). De taal is later vooral populair geworden doordat het de standaard-taal werd van personal computers: de Apple II in 1978, de IBM-PC in 1979, en al hun opvolgers. Helaas was de taal niet gestandaardiseerd, zodat op elk merk computer een apart dialect werd gebruikt, dat niet uitwisselbaar was met de andere. Procedurele talen: Algol, Pascal, C, PHP, Python Ondertussen was het inzicht doorgebroken dat voor wat grotere programmas het gebruik van procedures onontbeerlijk was. De eerste echte procedurele taal was Algol (een wat merkwaardige afkorting van Algorithmic Language). De taal werd in 1960 gelanceerd, met als bijzonderheid dat de taal een oci ele denitie had, wat voor de uitwisselbaarheid van programmas erg belangrijk was. Er werd voor de gelegenheid zelfs een speciale notatie (BNF) gebruikt om de opbouw van programmas te beschrijven, die (anders dan Algol zelf) nog steeds gebruikt wordt. In het vooruitgangsgeloof van de zestiger jaren was in 1968 de tijd rijp voor een nieuwe versie: Algol68. Een grote commissie ging er eens goed voor zitten en voorzag de taal van allerlei nieuwe idee en. Zo veel idee en dat het erg lastig was om vertalers te maken voor Algol68-programmas. Die kwamen er dan ook nauwelijks, en dat maakt dat Algol68 de dinosauri ers achterna is gegaan: uitgestorven vanwege zijn complexiteit. Het was wel een leerzame ervaring voor taal-ontwerpers: je moest niet willen streven naar een taal met eindeloos veel toeters en bellen, maar juist naar een compact en simpel taaltje. De eerste simpele, maar wel procedurele, taal werd als e enmansactie bedacht in 1971: Pascal (geen afkorting, maar een vernoeming naar de losoof Blaise Pascal). Voornaamste doel van ontwerper Wirth was het onderwijs aan de universiteit van Z urich te voorzien van een gemakkelijk te leren, maar toch verantwoorde (procedurele) taal. Al gauw werd de taal ook voor serieuze toepassingen gebruikt; allicht, want mensen stappen niet zo gauw af van een eenmaal aangeleerde taal. Voor echt grote projecten was Pascal echter toch te beperkt. Zon groot project was de ontwikkeling van het operating system Unix eind jaren zeventig bij Bell Labs. Het was sowieso nieuw om een operating system in een procedurele taal te schrijven (tot die tijd gebeurde dat in Assembler-talen), en voor deze gelegenheid werd een nieuwe taal ontworpen: C (geen afkorting, maar de opvolger van eerdere prototypes genaamd A en B). Het paste in de losoe van Unix dat iedereen zijn eigen uitbreidingen kon schrijven (nieuwe editors en dergelijke). Het lag voor de hand dat die programmas ook in C werden geschreven, en zo werd C de belangrijkste imperatieve taal van de jaren tachtig, ook buiten de Unix-wereld.

10

Programmeren

Ook recente talen om snel en makkelijk een web-pagina te genereren (PHP) of data te manipuleren (Perl, Python) zijn procedureel. Object-geori enteerde talen: Simula, Smalltalk, C++, C#, Java In 1975 was in Bergen (Noorwegen) een zekere Ole-Johan Dahl ge nteresseerd in programmas die simulaties uit konden voeren (van het gedrag van rijen in een postkantoor, de doorstroming van verkeer, enz.). Het was in die tijd al niet zo raar meer om je eigen taal te ontwerpen, en zo ontstond de taal Simula als een uitbreiding van Algol60. Een van die uitbreidingen was het object als zelfstandige eenheid. Dat kwam handig uit, want een persoon in het postkantoor of een auto in het verkeer kon dan mooi als object worden beschreven. Simula was daarmee de eerste object-geori enteerde taal. Simula zelf leidde een marginaal bestaan, maar het object-idee werd opgepikt door onderzoekers van Xerox in Palo Alto, die (eerder dan Apple en Microsoft) experimenteerden met window-systemen en een heuse muis. Hun taaltje (genaamd Smalltalk) gebruikte objecten voor het modelleren van windows, buttons, scrollbars en dergelijke: allemaal min of meer zelfstandige objecten. Maar Smalltalk was wel erg apart: werkelijk alles moest een object worden, tot aan getallen toe. Dat werd niet geaccepteerd door de massa. Toch was duidelijk dat objecten op zich wel handig waren. Er zou dus een C-achtige taal moeten komen, waarin objecten gebruikt konden worden. Die taal werd C++ (de twee plustekens betekenen in C de opvolger van, en elke C-programmeur begreep dus dat C++ bedoeld was als opvolger van de taal C). De eerste versie is van 1978, en de ele standaard verscheen in 1981. oci De taal is erg geschikt voor het schrijven van window-gebaseerde programmas, en dat begon in die tijd net populair te worden. Maar het succes van C++ is ook toe te schrijven aan het feit dat het echt een uitbreiding is van C: de oude constructies uit C bleven bruikbaar. Dat kwam goed uit, want mensen stappen nu eenmaal niet zo gemakkelijk af van een eenmaal aangeleerde taal. De taal C++ is weliswaar standaard, maar de methode-bibliotheken die nodig zijn om windowsystemen te maken zijn dat niet. Het programmeren van een window op een Apple-computer, een Windows-computer of een Unix-computer met X-windows moet dan ook totaal verschillend worden aangepakt, en dat maakt de interessantere C++-programmas niet uitwisselbaar met andere operating systems. Oorspronkelijk vond men dat nog niet eens zo heel erg, maar dat werd anders toen midden jaren negentig het Internet populair werd: het was toch jammer dat de programmas die je via het Internet verspreidde slechts door een deel van het publiek gebruikt kon worden (mensen met hetzelfde operating system als jij). Tijd dus voor een nieuwe programmeertaal, ditmaal eentje die gestandaardiseerd is voor gebruik onder diverse operating systems. De taal zou moeten lijken op C++, want mensen stappen nu eenmaal niet zo gemakkelijk af van een eenmaal aangeleerde taal, maar het zou een mooie gelegenheid zijn om de nog uit C afkomstige en minder handige idee en overboord te zetten. De taal Java vervult deze rol (geen afkorting, geen losoof, maar de naam van het favoriete kofemerk van de ontwerpers; de naam is blijkbaar bedacht tijdens een rondje brainstormen in de coeeshop). Internet-gebruikers zijn gewend dat alles gratis is, en wilde de taal een kans maken dat zou hij ook gratis gebruikt moeten kunnen worden. Welk bedrijf zou daarin willen investeren? Hardwarefabrikant Sun is zo aardig geweest, natuurlijk niet zonder eigenbelang: een taal die onafhankelijk is van het operating system zou het dreigende monopolie van concurrent Microsoft kunnen voorkomen. Het is in dat verband wel leuk om de kleine lettertjes van de Java-licentie te lezen. Anders dan bij de meeste andere software is zowat alles toegestaan: gebruiken, kopi eren, verspreiden; en dat allemaal gratis. Slechts e en ding is hevig verboden: het toevoegen van extratjes aan de taal die speciek zijn voor een bepaald operating system. Dat laatste vindt Microsoft natuurlijk weer niet leuk, en die heeft daarom een eigen object-geori enteerde opvolger-van-C++ ontworpen: de taal C# (uit te spreken als C-sharp). Versies van Java De enige die wel nieuwe versies van Java mag maken is Sun zelf, en dat gebeurt dan ook regelmatig. Enerzijds is dat jn, want het wordt steeds mooier, maar anderzijds onstaat er zo wel een wildgroei van niet-compatibele versies van programmas. De versies van Java zijn samengevat in guur 3. De eerste versie werden uitgebracht in de vorm van een Java Development Kit, en de afkorting JDK werd eigenlijk gangbaarder dan de naam Java. Bij versie 1.2 heeft Sun geprobeerd om de taal Java2 te noemen (want versie 2 klinkt wat belangrijker dan versie 1.2). Bovendien kwamer

1.5 Vertalen van programmas

11

JDK JDK J2SE J2SE J2SE J2SE Java SE Java SE

1.0 1.1 1.2 1.3 1.4 5.0 6 7

jan feb dec mei feb sept dec feb

1996 1997 1998 2000 2002 2004 2006 2010

Oak Playground Kestrel Merlin Tiger Mustang Dolphin

Figuur 3: Versiegeschiedenis van Java

er drie edities: een Standard Edition (SE), een Enterprise Edition (EE) voor bedrijven die willen betalen voor extra libraries, en een Mobile Edition (ME) voor programmas die moeten draaien op simpele hardware. Maar al gauw werd dat toch weer afgekort tot J2SE, en de versienummering telde gewoon weer door met 1.3 en 1.4. Bij de volgende versie proberen ze het weer: in plaats van 1.5 heet deze 5.0, to better reect the level of maturity, stability, scalability and security of Java2, want wie wil er nou 8 jaar bij versie 1 blijven hangen? Ondertussen is de 2 in de naam een raar fossiel geworden. De volgende versies heten daarom Java SE 6 en 7. Waar dit alles toe moet leiden is lastig te voorspellen. Java en C# leven al tien jaar naast elkaar en er is nog geen winnaar aan te wijzen. Ook C++ is nog volop in gebruik, maar hoe lang nog? nterpreteerde scripttalen zoals PHP en Python de markt Gaan in het volgende decennium hippe ge overnemen van de klassieke gecompilerde object-geori enteerde talen? En wat moeten we denken van de beleidswijziging van Sun om het onderhoud van de gratis versie van Java SE nog maar voor 3 jaar te garanderen, en de introductie van Java SE for business met een langere garantie? In ieder geval is Java eenvoudiger te leren dan C++ (dat door de compatibiliteit met C een nogal complexe taal is), en je kunt er dus sneller interessante programmas mee schrijven. Objectgeori enteerde idee en zijn in Java prominent aanwezig, en het kan zeker geen kwaad om die te leren. Andere object-geori enteerde talen (C++, C#, of nog weer andere) zijn, met Java als basiskennis, relatief gemakkelijk bij te leren. En dat kan nooit kwaad, want er is geen enkele reden nooit meer af te stappen van een eenmaal geleerde taal. . .

1.5

Vertalen van programmas

Assembler Een computerprogramma wordt door een speciaal programma vertaald voor gebruik op een bepaalde computer. Afhankelijk van de omstandigheden heet zon vertaalprogramma een assembler, een compiler, of een interpreter. Een assembler wordt gebruikt voor het vertalen van Assembler-programmas naar machinecode. Omdat een Assembler-programma speciek is voor een bepaalde processor, heb je voor verschillende computers verschillende programmas nodig, die elk door een overeenkomstige assembler worden vertaald. Compiler Het voordeel van alle talen behalve Assembler is dat ze, in principe althans, geschreven kunnen worden onafhankelijk van de computer. Er is dus maar e en programma nodig, dat op een computer naar keuze kan worden vertaald naar de betreende machinecode. Zon vertaalprogramma heet een compiler. De compiler zelf is wel machine-speciek; die moet immers de machinecode van de betreende computer kennen. Het door de programmeur geschreven programma (de source code, of kortweg source, of in het Nederlands: broncode) is echter machine-onafhankelijk. Vertalen met behulp van een compiler is gebruikelijk voor de meeste procedurele talen, zoals Pascal, C en C++. Interpreter Een directere manier om programmas te vertalen is met behulp van een interpreter. Dat is een programma dat de broncode leest, en de opdrachten daarin direct uitvoert, dus zonder deze eerst

12

Programmeren

met een assembler:

.asm sourcecode voor processor 1 .asm sourcecode voor processor 2

Assembler voor processor 1 Assembler voor processor 2

.exe machinecode voor processor 1 .a machinecode voor processor 2

met een compiler:

.cpp sourcecode

Compiler voor processor 1 Compiler voor processor 2

.exe machinecode voor processor 1 .a machinecode voor processor 2

met een interpreter:

.php sourcecode

Interpreter voor processor 1 Interpreter voor processor 2

met een compiler en een interpreter:

.java sourcecode

Compiler

.class bytecode

Interpreter voor processor 1 Interpreter voor processor 2

Figuur 4: Vier manieren om een programma te vertalen

te vertalen naar machinecode. De interpreter is speciek voor de machine, maar de broncode is machine-onafhankelijk. Het woord interpreter betekent letterlijk tolk, dit naar analogie van het vertalen van mensentaal: een compiler kan worden vergeleken met schriftelijk vertalen van een tekst, een interpreter vertaalt de uitgesproken zinnen direct mondeling. Het voordeel van een interpreter boven een compiler is dat er geen aparte vertaalslag nodig is. Het nadeel is echter dat het uitvoeren van het programma langzamer gaat, en dat eventuele fouten in het programma niet in een vroeg stadium door de compiler gemeld kunnen worden. Vertalen met behulp van een interpreter is gebruikelijk voor de wat eenvoudigere talen, zoals Basic, maar ook bijvoorbeeld voor html en de daarin ingebedde taal Javascript (niet te verwarren met Java). Compiler+interpreter Bij Java is voor een gemengde aanpak gekozen. Java-programmas zijn bedoeld om via het Internet te verspreiden. Het verspreiden van de gecompileerde versie van het programma is echter niet handig: de machinecode is immers machine-speciek, en dan zou je voor elke denkbare computer aparte versies moeten verspreiden. Maar het verspreiden van broncode is ook niet altijd wenselijk; dan ligt de tekst van het programma immers voor het oprapen, en dat is om redenen van auteursrecht niet altijd de bedoeling. Het komt veel voor dat gebruikers het programma wel mogen gebruiken, maar niet mogen inzien of wijzigen; machinecode is voor dat doel heel geschikt. De aanpak die daarom voor Java wordt gehanteerd is een compiler die de broncode vertaalt: maar niet naar machinecode, maar naar een nog machine-onafhankelijke tussenliggende taal, die bytecode wordt genoemd. Die bytecode kan via het Internet worden verspreid, en wordt op de computer van de gebruiker vervolgens met behulp van een interpreter uitgevoerd. De bytecode is dusdanig eenvoudig, dat de interpreter erg simpel kan zijn; interpreters kunnen dan eenvoudig worden ingebouwd in Internet-browsers. Omdat het meeste vertaalwerk al door de compiler is gedaan, kan het interpreteren van de bytecode relatief snel gebeuren, al zal een naar echte machinecode gecompileerd programma altijd sneller kunnen worden uitgevoerd.

1.6 Programmeren

13

1.6

Programmeren

In het klein: Edit-Compile-Run Omdat een programma een tekst is, begint het implementeren over het algemeen met het tikken van de programmatekst met behulp van een editor. Is het programma compleet, dan wordt het bestand met de broncode aangeboden aan de compiler. Als het goed is, maakt de compiler de bijbehorende bytecode, die we vervolgens met een interpreter kunnen uitvoeren. Zo ideaal verloopt het meestal echter niet. Het bestand dat je aan de compiler aanbiedt, moet wel geldige Java-code bevatten: je kunt moeilijk verwachten dat de compiler van willekeurige onzin naar zinvolle bytecode kan vertalen. De compiler controleert dan ook of de broncode aan de vereisten voldoet; zo niet, dan volgt er een foutmelding, en weigert de compiler om bytecode te maken. Nu doe je over het algemeen wel je best om een echt Java-programma te compileren, maar een tikfout is snel gemaakt, en de vorm-vereisten voor programmas zijn nogal streng. Reken er dus maar op dat je een paar keer door de compiler wordt terugverwezen naar de editor. Vroeg of laat zal de compiler echter wel tevreden zijn, en bytecode produceren. Dan kun je de volgende fase in: het uitvoeren van het programma, in het Engels run of execute genoemd, en in het Nederlands dus ook wel runnen of executeren. In veel gevallen merk je dan, dat het programma toch net niet (of helemaal niet) doet wat je bedoeld had. Natuurlijk heb je je best gedaan om de bedoeling goed te formuleren, maar een denkfout is snel gemaakt. Er zit dan niets anders op om weer terug te keren naar de editor, en het programma te veranderen. Dan weer compileren (en hopen dat je geen nieuwe tikfouten gemaakt hebt), en dan weer runnen. Om tot de conclusie te komen dat er nu wel iets anders gebeurt, maar toch n et niet wat je bedoelde. Terug naar de editor. . . In het groot: Modelleer-Speciceer-Implementeer Zodra de doelstelling van een programma iets ambitieuzer wordt, kun je niet direct achter de editor plaatsnemen en het programma beginnen te tikken. Aan het implementeren (het daadwerkelijk schrijven en testen van het programma) gaan nog twee fasen vooraf. Als eerste zul je een praktijkprobleem dat je met behulp van een computer wilt oplossen moeten formuleren in termen van een programma dat invoer krijgt van een gebruiker en bepaalde resultaten te zien zal geven. Deze fase, het modelleren van het probleem, is misschien nog wel het moeilijkste. Is het eenmaal duidelijk wat de taken zijn die het programma moet uitvoeren, dan is de volgende stap om een overzicht te maken van de klassen die er nodig zijn, en de methoden die daarin ondergebracht gaan worden. In deze fase hoeft van de methoden alleen maar beschreven te worden wat ze moeten doen, nog niet hoe dat precies gebeurt. Bij dit speciceren zul je wel in de gaten moeten houden dat je niet het onmogelijke van de methoden verwacht: ze zullen later immers ge mplementeerd moeten worden. Als de specicatie van de methoden duidelijk is, kun je beginnen met het implementeren. Daarbij zal de edit-compile-run cyclus waarschijnlijk meermalen doorlopen worden. Is dat allemaal af, dan kun je het programma overdragen aan de opdrachtgever. In veel gevallen zal die dan opmerken dat het weliswaar een interessant programma is, maar dat er toch eigenlijk een net iets ander probleem opgelost moest worden. Dan begint het weer van voren af aan met het herzien van de modellering, gevolgd door aanpassing van de specicatie en een nieuwe implementatie, en dan. . .

Opgaven
1.1 Assembler en compiler a. Wat is het verschil tussen een assembler en een compiler? b. Wat is het voordeel van een compiler boven een assembler? c. Waarom wordt voor sommige dingen toch, ook nu nog, een assembler gebruikt? d. Voor wat voor soort dingen is dat dan? 1.2 Programmeerparadigmas Waar of niet waar (en waarom?) a. Alle imperatieve talen zijn object-geori enteerd. b. Er zijn object-geori enteerde talen die niet procedureel zijn. c. Procedurele talen moeten worden gecompileerd. d. Declaratieve talen kunnen niet op dezelfde processor runnen als imperatieve, omdat die processor een toekenningsopdracht kan uitvoeren, die in declaratieve talen niet bestaat.

14

Hoofdstuk 2

Java
2.1 Omgeving van het programma
Java-applets Een van de bestaansredenen van Java is het verspreiden van programmas via het Internet. De meest directe manier om dat te doen is om programmas direct vanuit de Internet-browser te starten. Het programma krijgt dan een deel van het browser-window toegewezen, en kan dat gebruiken om met de gebruiker te communiceren. Zon Java-programma heet een applet; dit is een voor de gelegenheid bedachte samenstelling van application (toepassing) en het achtervoegsel -plet (-tje). Een applet is dus een toepassingetje. In de opmaaktaal voor web-paginas html is het mogelijk om aan te geven dat er een applet in een pagina moet worden opgenomen. Op dezelfde manier waarop je met de <img>-tag een plaatje kunt neerzetten op een pagina, kun je met de <applet>-tag een applet neerzetten. De eigenlijke bytecode van het programma maakt geen deel uit van de html-tekst, maar wordt in een apart bestand geplaatst, op dezelfde manier als een plaatje apart in een gif- of jpeg-bestand staat opgeslagen. Java-applications Omdat Java een complete, general purpose programmeertaal is, is het ook mogelijk om vrijstaande programmas te maken, dus programmas die los van een Internet-browser kunnen worden gebruikt. Zon programma wordt, in contrast met een applet, een application genoemd. De opzet van een programma dat als application moet kunnen werken is een beetje anders dan dat van een applet. Zo moet een application zijn eigen window maken, terwijl een applet het browserwindow kan gebruiken. Een application kan, als dat nodig is, meer dan e en window cre eren, en dat is in een applet juist weer niet mogelijk. Verder kan een application de les op de disk van de computer gebruiken, iets wat in een applet om veiligheidsredenen niet is toegestaan (wel kunnen zowel applets als applications les van het internet lezen). Voor het grootste deel van het programma maakt het echter niet uit of het als applet of als application gebruikt zal worden. Een applet is heel eenvoudig om te bouwen tot application (door een paar opdrachten toe te voegen die een window maken, en daarbinnen vervolgens de applet tonen). Omgekeerd kan het wat lastiger zijn, omdat de application zich mogelijk niet houdt aan de beperkingen die voor applets gelden. Javascript is geen Java Het is een beetje verwarrend dat er nog een andere programmeertaal is die bedoeld is voor gebruik in combinatie met het Internet. Deze taal heet Javascript, en hoewel de naam er op lijkt, is dat echt heel wat anders dan Java. Behalve de naam zijn de overeenkomsten tussen Javascript en Java dat beiden imperatieve machineonafhankelijke talen zijn, gebruikt kunnen worden via een Internet-browser, en dat op een veilige manier, zonder dat de gebruiker bang hoeft te zijn dat er met de les op de disk gerommeld gaat worden. Er zijn echter behoorlijk wat verschillen: Java is object-geori enteerd, Javascript is slechts procedureel; Java wordt gecompileerd (en daarna de bytecode ge nterpreteerd), Javascript wordt direct ge nterpreteerd; Java bytecode staat in een apart bestand, Javascript-broncode staat direct in het htmlbestand;

2.2 Opbouw van een programma

15

Java-applets werken geheel binnen het hun toegewezen deel van het browser-window, Javascriptprogrammas kunnen zich met de rest van de browser bemoeien (kleur veranderen, naar een andere pagina surfen, enz.); Java-programmas hebben de beschikking over een uitgebreide bibliotheek met klassen en methoden, Javascript-programmas alleen over een beperkt aantal ingebouwde methoden; In de praktijk komt het er op neer dat Java vooral wordt gebruikt door programmeurs, en Javascript vooral door web-ontwerpers. In deze cursus hebben we het verder alleen over Java, omdat dat op de lange termijn meer inzicht geeft in object-geori enteerde programmeertalen. En het is tenslotte niet de bedoeling dat je nooit meer afstapt van een eenmaal geleerde taal. (Dit neemt niet weg dat we wel eens jaloers zullen worden op de itsende grasche eecten die met Javascript relatief eenvoudig te maken zijn).

2.2

Opbouw van een programma

Programma: opsomming van klassen (en andere modules) Omdat Java een imperatieve taal is, bestaat een programma vooral uit opdrachten die uitgevoerd moeten worden. Omdat Java bovendien een procedurele taal is, zijn die opdrachten gebundeld in methoden. En omdat Java bovendien een object-geori enteerde taal is, zijn die methoden op hun beurt gebundeld in klassen. Een programma bestaat dus uit een package van alle daarin benodigde klassen (met daarin de methoden (met daarin de opdrachten)). In listing 1 staat een van de kortst mogelijke Java-applet. Het is een applet die de tekst Hallo! op het scherm schrijft. Het bestaat uit slechts e en opdracht. Die ene opdracht moet desondanks in een methode worden verpakt, en die ene methode vervolgens weer in een klasse. Elke klasse heeft een naam, die door de programmeur wordt bedacht. In het voorbeeld heet de klasse Hallo. Klasse: opsomming van methoden (en andere members) Een klasse bestaat grotendeels uit een opsomming van de methoden die er deel van uitmaken. In het voorbeeld is dat er maar e en. Net als de klasse krijgen ook de methoden een door de programmeur bedachte naam. In het voorbeeld is dat paint. Behalve methoden mogen er in een klasse ook variabelen worden ge ntroduceerd, dat wil zeggen namen (van plaatsen in het geheugen) met een waarde. In het voorbeeld gebeurt dat met de variable serialVersionUID, die de waarde 1 krijgt. Klassen kunnen uit vele methoden en variabele-introducties bestaan. De namen van methoden en variabelen mag de programmeur kiezen. In elk applet moet er echter e en methode zijn, met de naam paint zijn, en e en variabele, met de naam serialVersionUID. In dit korte programma hebben we dus nog weinig keus in de naamgeving. Methode: opsomming van opdrachten In de methoden zijn de eigenlijke opdrachten ondergebracht. In het voorbeeld-programma is dat er maar e en, namelijk een de opdracht om op een bepaalde plaats op het scherm een bepaalde tekst te tekenen. We zullen het voorbeeld nu wat gedetailleerder bekijken.

blz. 18

2.3

Modules

Module: meestal een klasse-denitie Een programma bestaat uit een package met e en of meerdere klasse-denities. In hoofdstuk 6 zullen we zien dat er behalve klasse-denities ook zogeheten interface-denities in een programma mogen staan. Er zijn dus twee soorten modules die in een package kunnen worden ondergebracht. Maar voorlopig zijn de enige modules die we bekijken klasse-denities. Klasse-header en -body Elke denitie van een klasse bestaat uit een header (een kopregel) en een body. De header van de klasse Hallo in het voorbeeldprogramma is
public class Hallo extends Applet

De body die daaronder volgt moet in zijn geheel tussen accolades { en } staan: daarmee wordt aangegeven welke methoden er allemaal bij deze klasse horen.

16

Java

Het is gebruikelijk om deze accolades recht onder elkaar te zetten, op een aparte regel. Alle tekst die daartussen staat wordt een stukje ingesprongen. Zo kun je gemakkelijk zien waar de klasse eindigt. public: bruikbaar voor de omgeving In de header van de klasse-denitie staat in ieder geval het woord class, om aan te geven dat het een klasse-denitie betreft. Direct daarachter staat de door de programmeur gekozen naam, in dit geval Hallo. Nog v o or het woord class kan het woord public staan. Dat geeft aan dat de klasse van buitenaf gebruikt mag worden. Omdat het voorbeeldprogramma een applet is, moet deze klasse inderdaad public gemaakt worden, omdat de klasse door de browser gebruikt wordt. Eventuele andere klassen, die alleen binnen het programma gebruikt worden, hoeven niet public te zijn. extends: voortbouwen op eerder werk Achter de naam van de klasse mag in de header het woord extends staan, gevolgd door de naam van een andere, al bestaande klasse. Daarmee kunnen we aangeven dat deze klasse niet van de grond af aan wordt opgebouwd, maar een uitbreiding is van de bestaande klasse. Omdat het hallo-programma uit het voorbeeld een applet moet worden, zorgen we er voor dat de klasse een uitbreiding is van de bestaande klasse Applet. Daardoor hoeven we alleen nog maar aan te geven in hoeverre ons programma afwijkt van een blanco applet. Syntax-diagrammen Het is lastig om in woorden te beschrijven hoe een Java-programma is opgebouwd: dat blijkt in de vorige paragraaf al. Daarom gaan we de grammatica van Java (de zogeheten syntax) beschrijven met diagrammen: syntax-diagrammen. Volg de route van links naar rechts door het rangeerterrein, en je ziet precies wat er allemaal nodig is. Woorden in een gele/lichtgekleurde rechthoek moet je letterlijk opschrijven; cursieve woorden in een groene/donkergekleurde ovaal verwijzen naar een ander syntax-diagram voor de details van een bepaalde deel-constructie. De opbouw van een klasse-denitie worde beschreven in het syntax-diagram voor module. Je ziet dat het mag beginnen met het woord public, maar dat dit ook mag worden overgeslagen. Daarna volgt de klasse-header, die zoals blijkt uit het tweede diagram, altijd begint met het woord class. In het diagram van klasse-header kun je ook zien dat het gedeelte met extends in sommige gevallen mag worden overgeslagen, maar dat als het woord extends er staat, er ook altijd een naam achter moet staan.

module
klasse

header
public
klasse

member

header
klasse

class

naam
extends

naam

De opbouw van de klasse-body ( e en of meer members, met een extra stel accolades om het geheel) hadden we in een apart diagram kunnen uitwerken. Maar om het aantal diagrammen niet te groot te maken is de opbouw van de klasse-body direct in het diagram van module uitgewerkt. De complete syntax-diagrammen van (vrijwel) de hele taal Java staan in bijlage G. De syntaxdiagrammen in de tekst zijn vaak wat vereenvoudigd. Zo kun je in de bijlage zien dat na het woord public ook nog optioneel het woord abstract en/of het woord final mag staan, en dat er behalve klassen ook nog een ander soort mudulen bestaan: interfaces. Maar omdat interfaces pas in hoofdstuk 6, en abstracte klassen pas in hoofdstuk 13 aan de orde komen, zijn deze details uit het diagram hierboven weggelaten.

2.4 Methode-denitie

17

2.4

Methode-denitie

Member: methode-denitie of variabele-denitie De body van een klasse bestaat uit zijn members. Een member kan de denitie van een methode of van een variabele zijn. In het (vereenvoudigde) syntax-diagram voor member wordt de opbouw van een member beschreven.

member
private
methode

header
public static final

opdracht naam
=

type

getal

Net als bij een module kan een member beginnen met het woord public. Ditmaal mag er in plaats van public ook private staan, maar je mag het ook helemaal weglaten. Daarna volgt de splitsing tussen een methode-denitie of een variabele-denitie. Syntax van een variabele-denitie De variabele-denitie in het voorbeeld-programma is
private static final long serialVersionUID = 1;

Deze voldoet aan de syntax van variabele-denitie (het onderste spoor in het syntaxdiagram hierboven). Als type is voor long gekozen, als naam voor serialVersionUID, en als getal voor 1. Een variabele-denitie met dit type en deze naam is verplicht in elk Java-applet. Hij wordt gebruikt om latere versies van het programma (waarin we dan getal 2 kunnen gebruiken) te onderscheiden van deze versie. Versie-problematiek is nog niet zo aan de orde in dit miniatuur-programma, dus voorlopig is deze variabele-denitie vooral hinderlijk, maar ja: verplicht is verplicht. . . Syntax van een methode-denitie Het bovenste spoor in het syntaxdiagram is belangrijker: het beschrijft de opbouw van een methode-denitie. Een methode-denities lijkt qua opbouw wel wat op een klasse-denitie: een header en een body tussen accolades. Maar de header is anders opgebouwd, en de body bestaat ditmaal niet uit members maar uit opdrachten. In het voorbeeldprogramma is er maar e en methode-denitie, namelijk van een methode genaamd paint. De header van de methode in het voorbeeld is:
public void paint(Graphics g)

De syntax van zon methode-header is als volgt:


methode

header
methode

type
void

naam

type
,

naam

De body van de methode moet in zijn geheel tussen accolades staan. Daarmee wordt aangegeven welke opdrachten allemaal bij deze methode horen. In dit geval is dat er overigens maar e en. Voor de duidelijkheid zetten we de accolades weer op aparte regels, recht onder elkaar, en springen we de opdracht in de body weer een stukje verder in. public: bruikbaar voor de omgeving Voor de methode-header staat, net als de klasse-header, een optionele aanduiding van de toegansrechten: het woord public of private. De methode in het voorbeeld moet door de browser gebruikt mogen worden, en is dus public. De variabele serialVersionUID is echter voor intern gebruik in de klasse, en is dus private. Als je geen toegangsrechten vermeldt, krijg je een tussenvorm tussen public en private: zon member mag wel buiten de klasse gebruikt worden, mits het binnen het package gebeurt. Hij kan dus niet vanuit een ander package gebruikt worden.

18 import java.awt.Graphics; import java.applet.Applet; public class Hallo extends Applet { public void paint(Graphics g) { g.drawString("Hallo!", 30, 20); } private static final long serialVersionUID = 1; } Listing 1: Hallo/src/Hallo.java

Java

10

<html> Hier is een <b>simpel</b> applet: <br> <applet codebase="bin" code="Hallo.class" width="100" height="50"> </applet> </html> Listing 2: Hallo/Hallo.html

void: doet dingen, maar berekent niets De eigenlijke header van de methode begint met het type van de methode. Sommige methoden kunnen een waarde berekenen, en dan geeft het type aan wat voor soort waarde dat is. Maar er zijn ook methoden die er alleen maar voor dienen om een aantal opdrachten uit te voeren; de methode in het voorbeeld is er zo een. In dat geval moet als type het woord void worden gebruikt. Letterlijk betekent dit leeg, en dat klopt, want de resultaatwaarde van deze methode is leeg. Naam van de methode Het derde woord in de methode-header is de naam van de methode. De naam van de methode mag in principe door de programmeur bedacht worden. Het is echter zo, dat de browser op het moment dat een applet in beeld gebracht wordt, de opdrachten begint uit te voeren die in de methode paint staan. Willen we dus dat onze methode daadwerkelijk door de browser uitgevoerd zal worden, dan is er eigenlijk geen keuze en moeten we de methode paint noemen. Parameters van de methode Achter de naam van de methode volgt een opsomming van de zogenaamde parameters van de methode. Die staan tussen haakjes; gewone, ronde haakjes en dus geen accolades. De programmeur mag kiezen hoeveel parameters er zijn: nul, e en, twee of meer. Maar zelfs als er nul parameters zijn moeten de haakjes er staan, met niets er tussen. In het geval van de methode paint is die keuzevrijheid er echter niet, omdat er over deze methode al afspraken bestaan met de browser. De methode paint moet daarom precies e en parameter krijgen. De parameters geven aan wat het type is van de variabelen die bij de start van de methoden beschikbaar zijn voor gebruik in die methode. In het geval van paint is er geen keuze mogelijk: die ene parameter moet per se een Graphics-object zijn. Wel mogen we zelf een naam voor deze parameter verzinnen. In het voorbeeld is voor de parameter de naam g gekozen. Waar die parameters voor dienen zal spoedig duidelijk worden, maar eerst gaan we de body van de methode bekijken.

2.5 Opdrachten

19

2.5

Opdrachten

Syntax van opdrachten Er zijn in Java een tiental verschillende soorten opdrachten mogelijk. De complete syntax ervan is te vinden in bijlage G. We beginnen met een van de belangrijkste opdracht-vormen: de aanroep van een methode. De syntax van zon opdracht is als volgt:

opdracht
methode

object

naam

expressie
,

expressie getal

symbool

Het diagram verwijst naar een ander diagram, namelijk dat van een expressie. Ook dat is een constructie met een tiental verschillende vormen, waarvan er voorlopig nog maar twee van belang zijn: een getal en een letterlijke tekst tussen aanhalingstekens. Syntax en semantiek Weten hoe een opdracht (althans e en van de tien mogelijke vormen) is opgebouwd is e en ding, maar het is natuurlijk ook van belang om te weten wat er gebeurt als zon opdracht wordt uitgevoerd. Dat heet de betekenis of semantiek van de opdracht. Semantiek van een methode-aanroep Als een methode-aanroep door de processor wordt uitgevoerd, dan zal de processor op dat moment de opdrachten gaan uitvoeren die in de body van die methode staan. Pas als die allemaal zijn uitgevoerd, gaat de processor verder met de opdracht die volgt op de methode-aanroep. Het aardige is dat de opdrachten in de body van de aangeroepen methode ook weer methodeaanroepen mogen zijn, van weer andere methoden. Beschouw het maar als een soort delegeren van werk aan anderen: als een methode geen zin heeft om het werk zelf uit te voeren, wordt een andere methode aangeroepen om het vuile werk op te knappen. In het voorbeeldprogramma is de enige opdracht in de body van de methode paint zon methodeaanroep:
g.drawString("Hallo!", 30, 20);

We zullen deze opdracht nu in detail ontleden. Object waarop de methode werkt Elke methode die wordt aangeroepen neemt een bepaald object onder handen. In de methodeaanroep wordt dat object als eerste genoemd het paradigma heet niet voor niets objectgeori enteerd programmeren! In het voorbeeld gaat het object g, dat we als parameter hebben gekregen, dienen als object waar de methode-aanroep op werkt. Naam van de methode Achter de naam van het object volgt een punt, en daarna de naam van de methode die we willen aanroepen. In het voorbeeld willen we dat het object g iets voor ons gaat tekenen, en daarom roepen we de methode drawString aan. Parameters van de methode Bij de aanroep van de methode drawString moeten we nog wat details doorgeven. Deze methode verwacht namelijk een drietal parameters: welke tekst er getekend moet worden, en op welke plaats

20

Java

dat moet gebeuren. De tekst in kwestie is "Hallo!", de positie op het scherm 30 beeldpunten van de linkerrand en 20 beeldpunten van de bovenrand van het window verwijderd. De parameters moeten tussen haakjes (gewone ronde) worden geschreven. Tenslotte sluit een puntkomma de methode-aanroep af. De tekst staat tussen aanhalingstekens, om aan te geven dat deze tekst echt letterlijk op het scherm gezet moet worden. Zonder de aanhalingstekens zou de compiler gaan klagen dat er geen tekst genaamd Hallo bestaat (wel een klasse, maar die kan weer niet getekend worden), en dat zon los uitroepteken ook geen geldige Java is. Tussen de aanhalingstekens accepteert de compiler echter alles. De aanhalingstekens bakenen de grenzen van de tekst af, en de tussenliggende symbolen worden letterlijk overgenomen.

2.6

Methoden en parameters

Parameters in een methode-denitie Parameters zijn de manier waarop methoden met elkaar communiceren: in een methode-header kun je aangeven dat een methode parameters verwacht, bij een methode-aanroep moet je aangeven wat de waarden van de parameters dan wel zijn. De methode-header van paint luidde:
public void paint(Graphics g)

Hiermee geven we aan dat we van degene die de methode aanroept eisen dat hij bij aanroep van de methode een object van type Graphics verschaft. Parameters in een methode-aanroep Bij de aanroep van een methode moeten de eisen worden ingewilligd. Nu is het zo dat in de header van de methode drawString is aangegeven dat deze methode een tekst en twee getallen verwacht. Bij aanroep van de methode zijn we dus verplicht om inderdaad met een tekst en twee getallen op de proppen te komen. De parameters staan, zowel in de header van de methode als bij aanroep, tussen haakjes. Maar wat er tussen de haakjes staat, verschilt: in de header staat er het type van de parameter en een zelfbedachte naam, bij de aanroep moet er alleen een waarde voor de betreende parameter worden vermeld. Objecten van type Graphics Wat moeten we ons nu voorstellen bij dat Graphics-object, dat de methode paint als parameter ontvangt? Een Graphics-object is het beste te beschouwen als een teken-apparaat. Het is een object dat op verzoek teksten (en andere dingen) kan tekenen, en wel door de methode drawString aan te roepen met het object v o or de punt. De web-browser is magischerwijze in het gelukkige bezit van zon teken-apparaat, en bij aanroep van paint is hij zo aardig om dat object als parameter mee te geven. In de methode-header van paint nemen we het object in ontvangst, en geven het de naam g. In de body van paint kunnen we van het verworven object g gebruik maken, door het object in een methode-aanroep te gebruiken v o or het punt-teken.

2.7

Naamgeving

Eisen voor de naamgeving Veel zaken in Java worden aangeduid door een naam: klassen, methoden, object-typen, parameters. Soms is dat een naam die door de programmeur zelf wordt bedacht, soms is het de naam van een al bestaande klasse of methode. De naam moet voldoen aan de volgende eisen: de naam bestaat uit e en of meer letters, cijfertekens, en/of onderstrepings- en dollar-tekens; het eerste teken is in ieder geval een letter; een vijftigtal woorden is taboe, omdat die een speciale betekenis hebben in Java (voorbeelden hiervan zijn public, class, extends, en void). Hoofdletters en kleine letters worden als verschillende symbolen beschouwd; de methode in het voorbeeld moet dus echt paint heten, en niet Paint of PAINT. Deze regels zijn bindend; als je ertegen zondigt zal de compiler bezwaar maken.

2.8 Bibliotheek-klassen

21

Aanbevelingen voor de naamgeving Daarnaast doen de ontwerpers van Java een paar aanbevelingen voor de naamgeving. Deze regels zijn niet bindend in de zin dat ze door de compiler worden afgedwongen, maar het is een goede gewoonte om je er aan te houden. Als iedereen dat doet komen programmas die door een team worden geschreven er mooi uniform uit te zien. Deze stijlregels luiden: namen van klassen beginnen met een hoofdletter (bijvoorbeeld onze eigen klasse Hallo, en de al bestaande klasse Applet), en namen van object-typen ook (bijvoorbeeld Graphics); namen van methoden beginnen met een kleine letter (bijvoorbeeld paint en drawString) en namen van parameters ook (bijvoorbeeld g); als een naam uit meerdere woorden bestaat, beginnen het tweede en de verdere woorden met een hoofdletter (zoals in drawString); rare afkortingen moeten worden vermeden: gebruik liever een naam met meerdere woorden (dus niet emn maar eenMooieNaam).

2.8

Bibliotheek-klassen

Importeren van klassen In het voorbeeldprogramma worden twee klassen gebruikt uit een bibliotheek van Javastandaardklassen: Applet, omdat onze klasse Hallo daar een uitbreiding van is Graphics, omdat dat het objecttype is van de parameter van paint Om deze klassen te mogen gebruiken, moeten we aan het begin van het programma aangeven dat deze klassen worden ge mporteerd. Dit gebeurt met een speciale import-aanwijzing aan het begin van het programma:
import java.awt.Graphics; import java.applet.Applet;

Dit zijn, in tegenstelling tot de methode-aanroep verderop in het programma, geen opdrachten; ze hoeven immers niet door de processor worden uitgevoerd tijdens het runnen van het programma. Het zijn veeleer aanwijzingen voor de compiler dat de bewuste klassen in het programma gebruikt gaan worden. Dat is de reden dat we ze niet import-opdrachten noemen. Dit diagram beschrijft de syntax van een complete le met source code. Hieruit blijkt dat import een speciale constructie is, die aan de eigenlijke modules (klasse-denities) van het programma vooraf gaat:

source file
package klasse

import

naam

naam

module

Package: bundeling van klassen De klassen in de Java-bibliotheek zijn georganiseerd in zogenaamde packages. (Informatici hebben iets met hi erarchische ordeningen). In de import-aanwijzingen moet de naam van de package vermeld worden. De klasse Applet zit in package java.applet (waarin naast Applet zelf nog een paar klassen zitten die iets met applets te maken hebben). De klasse Graphics zit in package java.awt. Dit is een package waarin alle klassen zitten die behoren tot de abstract window toolkit. Deze toolkit heet abstract omdat de klassen niet speciek bedoeld zijn voor een bepaald operating system. Je kunt er dus window-gebaseerde programmas mee maken die op meerdere computersystemen werken. In het complete syntax-diagram in bijlage G kun je zien dat er ook een aanwijzing is om je eigen programma deel uit te laten maken van een package. Voor de kleine, losstaande programmas die we voorlopig zullen maken is dat niet echt van belang.

22

Java

Hallo.java
import java.awt.Graphics; import java.applet.Applet; public class Hallo extends Applet { public void paint (Graphics g) { g.drawString("Hallo!", 30, 20); } }

Hallo.class compiler
LineNumberTable LocalVariableTable this LHallo; paint (Ljava/awt/Graphics;)V Hallo! java/awt/Graphics drawString (Ljava/lang/String;II)V g Ljava/awt/Graphics; SourceFile Hallo.java !

Hallo.html
<html> Hier is een <b>simpel</b> applet: <br> <applet code = "Hallo.class" width = "100" height= "50" > </applet> </html>

webbrowser

appletviewer

Figuur 5: Source code, byte code en html code

2.9

Ontwikkelomgevingen

blz. 18

Java-applets op een web-pagina Java-applets maken onderdeel uit van een web-pagina. Om een applet te kunnen uittesten, moet je dus eerst een html-bestand maken met een verwijzing naar de applet. In listing 2 staat een voorbeeld van een html-bestand waarin de Hallo-applet is opgenomen. Dit gebeurt met een <applet>-tag, waarbij de naam van de le met de bytecode, en de breedte en hoogte die voor het applet moet worden gereserveerd, is aangegeven. Er is ook een bijbehorende eind-tag </applet>. Tekst die tussen de begin- en eindtag staat wordt alleen maar weergegeven als de browser onverhoopt niet in staat is om applets te runnen. Source code, bytecode en html-code De naam van het bestand waarin de source code staat moet dezelfde naam hebben als de klasse die daarin wordt beschreven, met de extensie .java. Het programma met de klasse Hallo staat dus in het bestand Hallo.java. Niet alle compilers zijn even strikt in het afdwingen van deze regel, maar het is sowieso wel een handig systeem om je klassen later zelf te kunnen terugvinden. Als je de source code compileert, maakt de compiler een bestand aan met de extensie .class, in dit geval dus Hallo.class. Dat is een beetje verwarrend, want de tekst van de class staat dus in de java-le, terwijl de class-le bytecode bevat. . . Het bytecode-bestand is niet leesbaar met een editor. Dat hoeft ook niet, want het bestand is bestemd voor de interpreter die in de web-browser is ingebouwd. Het is overigens wel aardig om de class-le eens met een hexadecimale editor te bekijken. De .class-le en de .html-le moeten op de web-server worden geplaatst. De .java-le hoef je niet op de server te zetten; die kun je dus desgewenst geheim houden. Als je vervolgens met de browser naar de .html-le surft, begint de applet te werken (de browser roept namelijk de methode paint aan). Het resultaat is te zien in guur 5. De Java Software Development Kit (JDK) In de door Sun uitgebrachte Java Software Development Kit zitten onder andere de volgende programmas: javac, een compiler die Java source code vertaalt naar bytecode java, een interpreter voor stand alone Java applications appletviewer, een interpreter voor applets, waarmee je ook zonder browser je applets kunt testen. In de JDK zit geen aparte editor. Je kunt de source code en de html-code echter gewoon intikken met je favoriete editor. Een simpele editor zoals notepad of pfe volstaat.

2.9 Ontwikkelomgevingen De compiler moet je starten vanaf een commandoregel, met het commando
javac Hallo.java

23

Eventuele foutmeldingen verschijnen ook in het window waar de compiler werd gestart. De compiler geeft het regelnummer van de fout aan; die regel kun je in de editor opzoeken, om de fout in het programma te verbeteren. Als de compiler niets meer te klagen heeft, is er als het goed is een .class-le ontstaan. Je kunt de applet dan testen met het commando
appletviewer Hallo.html

waarbij Hallo.html de html-le is waarin de <applet>-tag met de verwijzing naar Hallo.class is opgenomen. Als het programma geen applet is maar een application, dan kun je de bytecode interpreteren met het commando
java Startklasse

Waarbij Startklasse de naam van de klasse is (dus niet de naam van een le!) waarin het hoofdwindow wordt opgebouwd. Voorlopig zullen we dat echter in deze cursus niet gebruiken, omdat de programmas in de komende hoofdstukken allemaal applets zijn. Ge ntegreerde ontwikkelomgevingen Hoewel de JDK in principe genoeg is om Java-programmas te ontwikkelen, is het gebruiksgemak nogal beperkt. Meer service wordt geboden door zogenaamde ge ntegreerde ontwikkelomgevingen, waarin de compiler en interpreter, maar ook de editor, zijn samengevoegd tot e en geheel. Zon ontwikkelomgeving biedt meestal de volgende faciliteiten: je kunt de foutmeldingen van de compiler aanklikken, waarna de editor direct naar de bewuste regel springt; de editor kent Java, en geeft bijvoorbeeld bijzondere woorden zoals class en void weer in een aparte kleur; de editor ondersteunt typische Java-constructies, zoals paren accolades met een ingesprongen body daartussen; de editor geeft een overzicht van klassen en methoden in het programma; met e en druk op de knop kun je het programma compileren en/of runnen; als de cursor op een klasse- of methode-naam uit de bibliotheek staat, kun je met een druk op de help-toets direct de documentatie daarvan raadplegen. ntegreerde ontwikkelomgevingen (Integrated Development Environments of IDE) Er zijn diverse ge voor Java op de markt, waaronder Webgain Visual Cafe, Inprise JBuilder, Microsoft J++, IBM Visual Age, Oracle Jdeveloper, Metroworks CodeWarrior, en Tek-tools Kawa (als al die bedrijven tenminste nog zo heten). De meeste IDEs hebben geen ingebouwde compiler, maar gebruiken achter de schermen de compiler van de JDK. De IDE vormt dus alleen een schil rond de JDK. Dit heeft als voordeel dat je dezelfde IDE kunt blijven gebruiken als er een nieuwe versie van de JDK uitkomt. In bijlage A beschrijven we het gebruik van twee verschillende IDEs: JCreator en Eclipse. Deze hebben vergelijkbare mogelijkheden als de hierboven genoemde commerci ele producten, en hebben als bijkomend voordeel dat ze gratis zijn te gebruiken.

Opgaven
2.1 Namen veranderen a. De klasse in het voorbeeldprogramma in hoofdstuk 2 heet Hallo. Wat moet er allemaal veranderen als we dat willen veranderen in Hoi? Wat hoeft er strikt genomen niet te veranderen, maar zou logisch zijn om ook te veranderen? b. De parameter van de methode paint in dit programma heet g. Wat moet er allemaal veranderen als we dat willen veranderen in gr ? 2.2 Naamgeving In de wiskunde en de natuurkunde worden vaak vaste variabele- en constantennamen gebruikt in een vaste betekenis. Welke bijvoorbeeld? Is dat handig? 2.3 Edit-compile-run

24 a. b. c. d. e. Tik het programma Hallo in (of een variatie daarop). Compileer en run het programma in de JCreator en/of Eclipse omgeving. Run het programma vanuit de www-browser Zet de applet op je homepagina op de www-server, en surf erheen. Verander iets aan het programma en run het opnieuw.

Java

2.4 Multiple window a. Maak het window voor de foutmeldingen van de compiler groter. b. Hoe kun je de inhoud van twee les (bijvoorbeeld Java- en html-les) bekijken? 2.5 Compiler foutmeldingen Introduceer steeds e en van de hieronderstaande fouten (als je dat al niet per ongeluk had gedaan. . . ) en verklaar de reactie van de compiler: a. vergeet een import-aanwijzing b. vergeet het woord public in de klasse- en/of methode-header c. vergeet de hoofdletter in de klassenaam d. vergeet het aanhalingsteken sluiten e. vergeet de puntkomma achter de opdracht f. vergeet de accolade sluiten achter de methode en/of klasse 2.6 Help-paginas a. Zoek de help-pagina van de methode drawString. b. Welke methoden zitten er o.a. nog meer in deze klasse? c. Probeer een van die methoden te gebruiken in de applet.

25

Hoofdstuk 3

Tekenen en rekenen
3.1 Graphics
Grasche uitvoer Een programma dat alleen maar "Hallo!" op het scherm schrijft is niet geweldig interessant (het had zelfs zonder applet gekund, door de tekst "Hallo!" direct in de html-le te zetten. . . ). Gelukkig kennen objecten van type Graphics nog andere methoden dan drawString. Door in de body van de paint-methode van een applet meerdere methoden aan te roepen, kunnen we een gecompliceerde tekening opbouwen. Het programma in listing 3 bijvoorbeeld, maakt op deze manier een schilderij in de Stijl van Mondriaans compositie met rood en blauw. In guur 7 is deze applet in werking te zien. Methoden in de klasse Graphics Objecten van het type Graphics, zoals we aangeleverd krijgen als parameter van de methode paint, kunnen worden gebruikt met onder andere de volgende methoden: drawString(t, x, y) tekent tekst t op positie (x,y) drawLine(x1, y1, x2, y2) tekent een lijn van positie (x1,y1) naar (x2,y2) drawRect(x, y, b, h) tekent een rechthoek met breedte b en hoogte h drawOval(x, y, b, h) tekent een ovaal binnen de genoemde rechthoek fillRect(x, y, b, h) als drawRect, maar nu helemaal ingekleurd setColor(c) gebruikt kleur c in volgende teken-acties Alle afmetingen en posities worden geteld in beelscherm-punten, en worden gerekend vanaf de ordinaat loopt dus gewoon van links naar rechts, maar de y -co ordinaat linkerbovenhoek. De x-co loopt van boven naar beneden (net andersom als in wiskunde-graeken); zie guur 6. In listing 3 gebruiken we de methode fillRect om een aantal rechthoeken in te kleuren. Voor de gekleurde rechthoeken roepen we eerst setColor aan. Klassen beschrijven de mogelijkheden van objecten Alle methoden uit de klasse Graphics kun je aanroepen, als je tenminste de beschikking hebt over een object met object-type Graphics. Dat is in de body van de methode paint geen probleem, want die methode heeft een Graphics-object als parameter, die in de body gebruikt mag worden. Dit illustreert de rol van klasse-denities. Het is niet zomaar een opsomming van methoden: de methoden kunnen gebruikt worden om een object uit die klasse te bewerken. In zekere zin beschrijft de lijst van methoden de mogelijkheden van een object: een Graphics-object kan teksten, lijnen, rechthoeken en ovalen tekenen. Je kunt zien dat objecten geheugen hebben. Na aanroep van de methode setColor onthoudt het object immers welke kleur gebruikt moet worden als er later nog aanroepen van een van de draw-methoden zouden volgen. Dat klopt ook wel met de manier waarop in sectie 1.2 over objecten werd gesproken: een object is een groepje variabelen. Inmiddels hebben we gezien dat een klasse (sectie 1.2: groepje methoden met een naam) beschrijft wat je met zon object kunt doen. Het gedrag dat het object door aanroep van de methoden kan vertonen is veel interessanter dan een beschrijving van welke variabelen nou precies deel uitmaken van een object. Je ziet dit duidelijk aan de manier waarop we het Graphics-object gebruiken: uit welke variabelen het object precies is opgebouwd hoeven we helemaal niet te weten, als we maar weten welke methoden aangeroepen kunnen worden. Het gebruik van bibliotheek-klassen gebeurt onder het motto: vraag niet hoe het kan, maar proteer ervan!.

blz. 27

blz. 27

26
x (x1,y1)
drawString

Tekenen en rekenen

(0,0) y

Hallo
(x,y) (x,y) b (x2,y2)

drawLine

drawRect

drawOval

fillRect

fillOval

Figuur 6: Enkele methoden uit de klasse Graphics

De klasse Color De methode setColor in de klasse Graphics behoeft nog wat nadere aandacht. Bij aanroep van deze methode worden we geacht een kleur als parameter mee te geven. Zon kleur is in Java een object op zichzelf, en de vraag is: hoe komen we aan kleur-objecten? Er is een bibliotheek-klasse Color, waarin een aantal methoden staan die gebruikt kunnen worden met een Color-object onder handen. Maar zon object hebben we nou juist nog niet. . . Nu is het zo, dat in klassen nog meer te halen valt dan alleen methoden. In sommige klassen zitten ook constanten die je in je programmas kunt gebruiken. Zo ook in de klasse Color: hierin zitten een aantal kant-en-klare Color-objecten. Deze constanten kun je pakken door de naam van de klasse en de naam van de constante te noemen, gescheiden door een punt, bijvoorbeeld:
Color.BLUE

Ditmaal staat er dus voor de punt niet de naam van een object (zoals bij methode-aanroepen), maar de naam van de klasse. Het aldus verkregen Color-object kunnen we gebruiken als parameter bij de aanroep van de methode setColor:
g.setColor( Color.BLUE );

waarbij g het Graphics-object is dat voortaan in blauw moet gaan tekenen. In de klasse Color zijn de volgende Color-constanten beschikbaar:
BLACK WHITE DARK_GRAY LIGHT_GRAY RED CYAN GREEN MAGENTA BLUE YELLOW GRAY ORANGE PINK

Cyan is de complementaire kleur van rood: de blauw/groene kleur van cyanidezouten. Magenta is de complementaire kleur van groen: lichtroze. Als de klasse Color in een programma wordt gebruikt, moet deze aan het begin van het programma worden ge mporteerd. Net als de klasse Graphics zit de klasse Color in het package java.awt, dus de import-aanwijzing luidt:
import java.awt.Color;

3.2

Variabelen

Opslag in het geheugen In het voorbeeldprogramma worden drie vertikale zwarte balken getekend. Dat had gekund met de volgende opdrachten:
g.fillRect(10, 0, 10, 100); g.fillRect(50, 0, 10, 100); g.fillRect(90, 0, 10, 100);

3.2 Variabelen

27

import java.awt.Graphics; import java.awt.Color; import java.applet.Applet;


5

/* Deze applet tekent een Mondriaan-achtige "compositie met rood en blauw" */ public class Mondriaan extends Applet { public void paint(Graphics g) { int breedte, hoogte, balk, x1, x2, x3, y1, y2; // posities van de lijnen breedte = 200; x1 = 10; x2 = 50; x3 = 90; hoogte = 100; y1 = 40; y2 = 70; balk = 10; // achtergrond g.setColor(Color.WHITE); g.fillRect(0, 0, breedte, hoogte); // zwarte balken g.setColor(Color.BLACK); g.fillRect(x1, 0, balk, hoogte); g.fillRect(x2, 0, balk, hoogte); g.fillRect(x3, 0, balk, hoogte); g.fillRect(0, y1, breedte, balk); g.fillRect(0, y2, breedte, balk); // gekleurde vlakken g.setColor(Color.BLUE); g.fillRect(0, y1+balk, x1, y2-(y1+balk) ); g.setColor(Color.RED); g.fillRect(x3+balk, 0, breedte-(x3+balk), y1); } private static final long serialVersionUID = 1;

10

15

20

25

30

35

40

} Listing 3: Mondriaan/src/Mondriaan.java

28

Tekenen en rekenen

Figuur 7: De applet Mondriaan in werking

De eerste twee parameters geven de plaats aan van de balken: 10, 50 en 90 beeldpunten vanaf de linkerrand, tegen de bovenrand aan. De derde en vierde parameter stellen de breedte (10) en hoogte (100) voor van de balken. Nu zou het kunnen zijn dat we er na enig experimenteren achter komen dat het mooier is als de breedte van de balken niet 10, maar 12 is. Bij dat experimenteren moeten we dan in alle aanroepen de 10 vervangen door de 12. Het vervelende is, dat dat niet met zoek- en vervang-opdracht van de editor kan, want dan wordt ook de x-co ordinaat van de eerste balk veranderd, en verandert de 100 in 120. (Wie denkt dat de drie getalletjes ook wel met de hand veranderd kunnen worden, stelle zich een experiment met de Victory Boogie-Woogie voor). Een oplossing is het gebruik van variabelen. In plaats van de constanten 10 en 100 gebruiken we twee variabelen, laten we zeggen balk en hoogte:
g.fillRect(10, 0, balk, hoogte); g.fillRect(50, 0, balk, hoogte); g.fillRect(90, 0, balk, hoogte);

We gaan er voor zorgen dat die variabelen de waarden 10 en 100 hebben, maar mochten we daar later niet tevreden mee zijn, zijn ze eenvoudig te wijzigen. Syntax van de toekenningsopdracht Je kunt variabelen in Java een waarde geven met een zogenaamde toekennings-opdracht (engels: assignment statement). Dat gebeurt als volgt:
balk = 10; hoogte = 100;

Een toekenningsopdracht bestaat dus uit: de naam van de variabele die een waarde moet krijgen; het teken = , uit te spreken als wordt; de nieuwe waarde van de variabele; een puntkomma. Na de methode-aanroep in sectie 2.5 is de toekenningsopdracht de tweede vorm van Javaopdrachten die we zijn tegengekomen. Het syntax-diagram van opdracht is daarmee tot nu toe:

opdracht
methode

object variabele

. =

naam

( ;

expressie
,

expressie

3.2 Variabelen

29

Semantiek van de toekenningsopdracht Op het moment dat de processor een toekenningsopdracht uitvoert, wordt de waarde van de variabele veranderd in de waarde die de expressie rechts van het wordt-teken op dat moment heeft. Als de variabele al eerder (met een andere toekenningsopdracht) een waarde had gekregen, dan gaat de oude waarde verloren. En omgekeerd: de nieuwe waarde blijft behouden tot de volgende toekenningsopdracht aan dezelfde variabele. Dat is ook de reden dat je het =-teken maar beter niet kunt uitspreken als is, maar liever als wordt: de waarde van de variabele balk is nog niet 10, althans niet noodzakelijkerwijs, maar hij wordt het door uitvoeren van de toekenningsopdracht. Declaratie van variabelen Je mag niet in het wilde weg variabelen gebruiken in een programma. De variabelen die je nodig hebt, moeten van tevoren worden aangekondigd. Dat gebeurt met de declaratie van variabelen; de variabelen moeten worden gedeclareerd. De declaratie van de variabelen balk en hoogte ziet er als volgt uit:
int balk, hoogte;

Een declaratie bestaat dus uit: het type dat de variabelen moeten krijgen; de naam of namen van de variabele(n), gescheiden door kommas; een puntkomma. Of in een syntax-diagram:

type declaratie type naam


, ;
klasse

naam
int double

Een declaratie is geen opdracht: er valt immers, tijdens het runnen, niets uit te voeren aan een declaratie. Een declaratie is veeleer een aanwijzing voor de compiler dat bepaalde variabelen gebruikt gaan worden en van een bepaald type zijn. De plaats van declaraties Declaraties van variabelen staan in de body van de methode waarin de variabelen nodig zijn, dus na de accolade-openen van de methode. Het syntax-diagram voor methode-denitie is in werkelijkheid wat uitgebreider dan we eerder besproken hebben: de body van een methode kan behalve uit opdrachten ook uit declaraties bestaan:

member
private
methode

declaratie
{ }

header
public static final

opdracht type naam


=

getal

Uit het diagram blijkt dat declaraties in principe overal in de body mogen staan, dus afgewisseld met de opdrachten. In de praktijk staan ze vaak aan het begin van de methode, op dezelfde manier waarop een recept begint met een opsomming van ingredi enten. Na de declaratie volgen toekenningsopdrachten die de variabelen van een waarde voorzien, en daarna kunnen de variabelen worden gebruikt:

30

Tekenen en rekenen

public void paint(Graphics g) { int balk, hoogte; balk = 10; hoogte = 100; g.fillRect(10, 0, balk, hoogte); g.fillRect(50, 0, balk, hoogte); g.fillRect(90, 0, balk, hoogte); }

Declaraties van variabelen lijken veel op de parameters, die in de methode-header zijn opgesomd. In feite zijn dat o ok declaraties. Maar er zijn een paar belangrijke verschillen: variabelen worden gedeclareerd in de body van de methode, parameters worden gedeclareerd tussen de haakjes in de methode-header; variabelen krijgen een waarde door een toekennings-opdracht, parameters krijgen automatisch een waarde bij de aanroep van de methode; in een variabele-declaratie kun je meerdere variabelen tegelijk declareren en het type maar e en keer opschrijven, in parameter-declaraties moet bij elke parameter opnieuw het type worden opgeschreven (zelfs als dat hetzelfde is); variabele-declaraties eindigen met een puntkomma, parameter-declaraties niet. Het type int Door middel van declaraties (zowel variabele-declaraties in de body van een methode, als parameter-declaraties in de header van een methode) hecht je een type aan de variabele-naam, respectievelijk parameter-naam. In veel gevallen is dat een object-type: de parameter van de methode paint die we in de parameterdeclaratie g genoemd hebben is bijvoorbeeld een Graphics-object. De variabelen balk en hoogte in de variabele-declaratie hebben het type int. Dit type is geen object-type, en balk en hoogte zijn dan ook geen objecten; het zijn getallen, en dat zijn in Java primitieve waarden. Dat wil zeggen dat dit soort waarden in Java ingebouwd zijn, zonder dat er een klasse is waarin hun eigenschappen liggen vastgelegd. Primitieve waarden zijn geen objecten, en je kunt er dan ook geen methoden van aanroepen. Wel kun je ze gebruiken als parameter van methoden, zoals in het voorbeeld gebeurt bij de aanroep van fillRect. De betreende parameter-positie moet, bij de denitie van de methode, dan wel als int zijn gedeclareerd. Primitieve waarden zijn geen objecten, en de primitieve types zijn dan ook geen klassen. Daarom wordt het type int, in tegenstelling tot object-typen, niet met een hoofdletter geschreven. Het type int is ingebouwd in de taal, en hoeft dus ook niet ge mporteerd te worden. Het woord int is niet eens een naam, zoals de klassenamen die object-typen aanduiden dat wel zijn. Het woord int, en de andere primitieve typen, hebben de status van gereserveerd woord, net zoals woorden als public en extends. Variabelen (en parameters) met het type int zijn getallen. Hun waarde moet geheel zijn; er kunnen in int-waarden dus geen cijfers achter de komma staan. De waarde kan positief of negatief zijn. De grootst mogelijk int-waarde is 2147483647, en de kleinst mogelijke waarde is 2147483648; het bereik ligt dus ruwweg tussen min en plus twee miljard. Er zijn maar een handjevol primitieve typen. Andere primitieve typen die we nog zullen tegenkomen zijn double (getallen die wel cijfers achter de komma kunnen hebben), char (lettertekens) en boolean (waarheidswaarden). Alle andere typen zijn object-typen; hun mogelijkheden worden w` el beschreven in een klasse. Nut van declaraties Declaraties zijn nuttig om meerdere redenen: de compiler weet door de declaraties van elke variabele wat het type is; daardoor kan de compiler controleren of methode-aanroepen wel zinvol zijn (aanroep van fillRect is zinvol met Graphics-objecten, maar onmogelijk met waarden van andere object-typen of met intwaarden); de compiler kan bij aanroep van methoden controleren of de parameters wel van het juiste type zijn; zou je bijvoorbeeld bij aanroep van drawString de tekst en de positie omwisselen, dan kan de compiler daarvoor waarschuwen;

3.3 Berekeningen

31

als je een tikfout maakt in de naam van een variabele (bijvoorbeeld hootge in plaats van hoogte), dan komt dat aan het licht doordat de compiler klaagt dat deze variabele niet is gedeclareerd

3.3

Berekeningen

Expressies met een int-waarde Op verschillende plaatsen in het programma kan het nodig zijn om een int-waarde op te schrijven, bijvoorbeeld: als parameter in een methode-aanroep van een methode met int-parameters aan de rechterkant van een toekenningsopdracht aan een int-variabele Op deze plaatsen kun je een constante getalwaarde schrijven, zoals 37, of de naam van een intvariabele, zoals hoogte. Maar het is ook mogelijk om op deze plaats een formule te schrijven waarin bijvoorbeeld optelling en vermenigvuldiging een rol spelen, bijvoorbeeld hoogte+5. In dat geval wordt, op het moment dat de opdracht waarin de formule staat wordt uitgevoerd, de waarde uitgerekend (gebruikmakend van de op dat moment geldende waarden van variabelen). De uitkomst wordt gebruikt in de opdracht. Zon formule wordt een expressie genoemd: het is een uitdrukking waarvan de waarde kan worden bepaald. Gebruik van variabelen en expressies In het voorbeeldprogramma in listing 3 komen variabelen en expressies goed van pas. Om het programma gemakkelijk aanpasbaar te maken, zijn er niet alleen variabelen gebruikt voor de breedte en hoogte van het schilderij, en voor de breedte van de zwarte balken daarin, maar ook voor de positie van de zwarte balken. De x-positie van de drie verticale balken wordt opgeslagen in de drie variabelen x1, x2 en x3, en de y -positie van de twee horizontale balken in de twee variabelen y1 en y2 (er mogen cijfers voorkomen in variabele-namen, als die maar met een letter begint). Met toekenningsopdrachten krijgen deze variabelen een waarde toegekend:
breedte = 200; x1 = 10; x2 = 50; x3 = 90;
blz. 27

enzovoorts. Bij het tekenen van de balken komt er, behalve het getal 0, geen enkele constante meer aan te pas:
g.fillRect(x1, 0, balk, hoogte); g.fillRect(0, y1, breedte, balk);

Met behulp van expressies kunnen we ook de positie van de gekleurde vlakken in termen van deze variabelen aanduiden. Het blauwe vlak aan de linkerkant ligt direct onder de eerste zwarte balk; dit vlak heeft dus een y -coordinaat die e en balkbreedte groter is dan de y -coordinaat van de eerste balk:
g.setColor(Color.BLUE); g.fillRect(0, y1+balk, iets , iets );

Omdat het vlak tegen de linkerkant aanligt, is de breedte van het vlak gelijk aan de x-positie van de eerste verticale balk. Zelfs de hoogte van het vlak kunnen we met een expressie aangeven: het is het verschil van de y -posities van de twee horizontale balken, minus e en extra balkbreedte. Het vlak kan dus getekend worden met de methode-aanroep:
g.fillRect(0, y1+balk, x1, y2-(y1+balk) );

Ook het rode vlak tegen de bovenrand kan op zon manier beschreven worden. Operatoren In int-expressies kun je de volgende rekenkundige operatoren gebruiken: + optellen - aftrekken * vermenigvuldigen / delen % bepalen van de rest bij delingindexrest bij deling

32

Tekenen en rekenen

Voor vermenigvuldigen wordt een sterretje gebruikt, omdat de in de wiskunde gebruikelijke tekens ( of ) nou eenmaal niet op het toetsenbord zitten. Helemaal weglaten van de operator, zoals in de wiskunde ook wel wordt gedaan is niet toegestaan, omdat dat verwarring zou geven met meer-letterige variabelen. Bij gebruik van de delings-operator / wordt het resultaat afgerond, omdat het resultaat van een bewerking van twee int-waarden in Java weer een int-waarde oplevert. De afronding gebeurt door de cijfers achter de komma weg te laten; positieve waarden worden dus nooit naar boven afgerond (en negatieve waarden nooit naar beneden). De uitkomst van de expressie 14/3 is dus 4. De bijzondere operator % geeft de rest die overblijft bij de deling. De uitkomst van 14%3 is bijvoorbeeld 2, en de uitkomst van 456%10 is 6. De uitkomst zal altijd liggen tussen 0 en de waarde rechts van de operator. De uitkomst is 0 als de deling precies op gaat. Prioriteit van operatoren Als er in e en expressie meerdere operatoren voorkomen, dan geldt de gebruikelijke prioriteit van de operatoren: vermenigvuldigen gaat voor optellen. De uitkomst van 1+2*3 is dus 7, en niet 9. Optellen en aftrekken hebben onderling dezelfde prioriteit, en vermenigvuldigen en de twee delings-operatoren ook. Komen in een expressie operatoren van dezelfde prioriteit naaast elkaar voor, dan wordt de expressie van links naar rechts uitgerekend. De uitkomst van 10-5-2 is dus 3, en niet 7. Als je wilt afwijken van deze twee prioriteitsregels, dan kun je haakjes gebruiken in een expressie, zoals in (1+2)*3 en 3+(6-5). In de praktijk komen in dit soort expressies natuurlijk variabelen voor, anders had je de waarde (9 en 4) meteen zelf wel kunnen uitrekenen. Een overbodig extra paar haakjes is niet verboden: 1+(2*3), en wat de compiler betreft mag je naar hartelust overdrijven: ((1)+(((2)*3))). Dat laatste maakt het programma er voor de menselijke lezer echter niet duidelijker op. De syntax van expressies tot nu toe wordt samengevat in het volgende syntax-diagram. De laatste regel, die aangeeft dat een methode-aanroep, ontdaan van zijn puntkomma, op de plaats van een expressie gebruikt kan worden, wordt besproken in sectie 4.3.

expressie getal variabele


symbool

expressie
(

operator
)
methode

expressie

expressie
.

object

naam

expressie
,

3.4

Programma-layout

Commentaar Voor de menselijke lezer van een programma (een collega-programmeur, of jijzelf over een paar maanden, als je de details van de werking van het programma vergeten bent) is het heel nuttig als er wat toelichting bij het programma staat geschreven. Dit zogenaamde commentaar wordt door de compiler geheel genegeerd, maar zorgt ervoor dat het programma beter te begrijpen is. Er zijn in Java twee manieren om commentaar te markeren: alles tussen de tekencombinatie /* en de eerstvolgende teken-combinatie */ (mogelijk pas een paar regels verderop) alles tussen de tekencombinatie // en het einde van de regel Dingen waarbij het zinvol is om commentaar te zetten zijn: groepjes opdrachten die bij elkaar horen, methoden en de betekenis van de parameters daarvan, en complete klassen.

3.5 Declaraties met initialisatie

33

Het is de kunst om in het commentaar niet de opdracht nog eens in woorden weer te geven; je mag er van uitgaan dat de lezer Java kent. In het voorbeeld-programma staat daarom bijvoorbeeld het commentaar
// posities van de lijnen x1 = 10; x2 = 50;

en niet
// maak de variabele x1 gelijk aan 10, en x2 aan 50 x1 = 10; x2 = 50;

Tijdens het testen van het programma kunnen de commentaar-tekens ook gebruikt worden om een of meerdere opdrachten tijdelijk uit te schakelen. Het staat echter niet zo verzorgd om dat soort uitgecommentarieerde opdrachten in het denitieve programma te laten staan. Regel-indeling Er zijn geen voorschriften voor de verdeling van de tekst van een Java-programma over de regels van de le. Hoewel het gebruikelijk is om elke opdracht op een aparte regel te schrijven, worden hier door de compiler geen eisen aan gesteld. Als dat de overzichtelijkheid van het programma ten goede komt, kan een programmeur dus meerdere opdrachten op e en regel schrijven (in het voorbeeldprogramma is dat gedaan met de relatief korte toekenningsopdrachten). Bij hele lange opdrachten (bijvoorbeeld methode-aanroepen met veel of ingewikkelde parameters) is het een goed idee om de tekst over meerdere regels te verspreiden. Verder is het een goed idee om af en toe een regel over te slaan: tussen verschillende methoden, en tussen groepjes opdrachten (en het bijbehorende commentaar) die bij elkaar horen. Witruimte Ook voor de plaatsing van spaties zijn er nauwelijks voorschriften. De enige plaats waar spaties vanzelfsprekend werkelijk van belang zijn, is tussen afzonderlijke woorden: public void paint mag niet worden geschreven als publicvoidpaint. Omgekeerd, midden in een woord mag geen extra spatie worden toegevoegd. In een tekst die letterlijk genomen wordt omdat er aanhalingstekens omheen staan, worden ook de spaties letterlijk genomen. Er is dus een verschil tussen
g.drawString("hallo", 10, 10);

en
g.drawString("h a l l o ", 10, 10 );

Maar voor het overige zijn extra spaties overal toegestaan. Goede plaatsen om extra spaties te schrijven zijn: achter elke komma en puntkomma (maar niet ervoor) links en rechts van het = teken in een toekenningsopdracht aan het begin van regels, zodat de body van methoden en klassen wordt ingesprongen (4 posities is gebruikelijk) ten opzichte van de accolades die de body begrenzen.

3.5

Declaraties met initialisatie

Combineren van declaratie en toekenning Aan alle variabelen zul je ooit een waarde toekennen. De variabele moet een waarde hebben gekregen voordat je hem in een berekening gebruikt. Als je dat vergeet, geeft de compiler een foutmelding: variable may not have been initialized. Variabelen die je niet in een berekening gebruikt, hoef je geen waarde te geven. Maar als je een variabele niet gebruikt, is de hele declaratie zinloos geworden. Dat is niet fout, maar wel verdacht, en daarom geeft de compiler in dat soort situaties een waarschuwing: variable is never used. Omdat een toekenning aan een variabele dus vrijwel onvermijdelijk is, is er een notatie om de declaratie van een variabele met de eerste toekenning aan die variabele te combineren. In plaats van
int breedte; breedte = 200;

mogen we ook schrijven:

34

Tekenen en rekenen

int breedte = 200;

Dit hoeft/mag alleen bij de eerste toekenning aan de variabele. Het is dus niet de bedoeling dat je bij elke toekenning opnieuw het type erbij gaat schrijven. Je zou de variabele dan steeds opnieuw declareren, en de compiler zal reageren met een foutmelding: duplicate variable. Syntax van declaraties De eerste toekenning aan een variabele heet een initialisatie. Dit is de uitgebreide syntax van declaraties waarin zon initialisatie is opgenomen:

declaratie type naam


= ,
Met deze uitbreiding voldoet ook de denitie van de member serialVersionUID aan de syntax van een declaratie. Dat maakt dat we het syntax-diagram van member iets kunnen versimpelen. We doen het meteen maar goed, met een extra diagram blok die de body van een methode beschrijft:

expressie

member
private static final
methode

header
public

blok

declaratie
final

blok declaratie
{ }

opdracht

Te zien is dat de aanduidingen static en final behalve op declaraties ook bij methode-denities gebruikt mogen worden, en dat ze beide optioneel zijn. De declaraties die in de body van een methode (in een blok) staan, mogen ook final zijn. Final declaraties Variabelen kunnen veranderen het woord zegt het al. De waarde verandert bij elke toekenningsopdracht aan die variabele. Soms is het handig om een bepaalde waarde een naam te geven, als die waarde veel in een programma voorkomt. In een programma met veel wiskundige berekeningen is het bijvoorbeeld handig om eenmalig te schrijven:
double pi = 3.1415926535897;

Daarna kun je waar nodig de variabele pi gebruiken, in plaats van elke keer dat hele getal uit te schrijven. Het is in dit geval niet de bedoeling dat de variabele later in het programma nog wijzigt echt variabel is deze variabele dus niet. Om er voor te zorgen dat dat niet per ongeluk zal gebeuren (bijvoorbeeld door een tikfout bij het intikken van het programma), kun je bij de declaratie met het keywoord final aangeven dat de variabele meteen zijn denitieve waarde krijgt. Deze initialisatie moet dan meteen bij de declaratie plaatsvinden. Een aldus gedeclareerde variabele heet een nal variabele; raar woord eigenlijk, want deze variabele kan dus niet vari eren. Constante zou een beter woord zijn, maar dat woord geeft weer verwarring met getallen zoals 100 en teksten zoals "Hallo" die immers ook constant zijn. De invloed van final op een methode-denitie wordt besproken in sectie 10.1. De betekenis van static wordt besproken in sectie 5.3.

3.5 Declaraties met initialisatie

35

Opgaven
3.1 Declaratie, opdracht, expressie Wat is het verschil tussen een declaratie, een opdracht en een expressie? 3.2 Kleuren mengen Welke kleuren hebben de beeldpunten van een kleuren-TV? Welke kleuren hebben de esjes inkt van een kleuren-printer? Kun je het verschil verklaren? 3.3 Engels jargon Het Engelse woord voor opdracht in een programmeertaal is statement. Dat woord is eigenlijk erg ongelukkig gekozen. Waarom? 3.4 Smiley Schrijf een applet dat een smiley op het scherm tekent: een grote cirkel, met twee kleine cirkels als ogen, en een cirkelboog als mond. Zoek in de help-paginas van de klasse Graphics op hoe je een cirkelboog kunt tekenen. Gebruik variabelen in het programma, zodat je met het wijzigen van e en toekenningsopdracht de afmeting van de smiley kunt aanpassen.

36

Hoofdstuk 4

Nieuwe methoden
4.1 Methode-denitie
Orde in de chaos Als je een vierkant tekent met twee schuine lijntjes erbovenop heb je een simpel huisje getekend. Het voorbeeld-applet in dit hoofdstuk tekent drie huisjes. Het complete programma staat in listing 4; in guur 8 is het resultaat te zien. Deze drie huisjes zouden getekend kunnen worden met de volgende paint-methode:
public void paint(Graphics g) { // kleine huisje links g.drawRect(20,60,40,40); g.drawLine(20,60,40,40); g.drawLine(40,40,60,60); // kleine huisje midden g.drawRect(70,60,40,40); g.drawLine(70,60,90,40); g.drawLine(90,40,110,60); // grote huis rechts g.drawRect(120,40,60,60); g.drawLine(120,40,150,10); g.drawLine(150,10,180,40); }

blz. 37

Ondanks het commentaar begint dit nogal onoverzichtelijk te worden. Wat zou je bijvoorbeeld in dit programma moeten veranderen als bij nader inzien niet het rechter, maar juist het linker huis groot getekend moet worden? Om het programma op die manier aan te passen zou je alle parameters van alle opdrachten weer moeten napuzzelen, en als je dat niet nauwkeurig doet loop je een goede kans dat in de nieuwe versie van het programma een van de daken in de lucht getekend wordt. En dan is dit nog maar een programma dat drie huisjes tekent; dit programma uitbreiden zodat het niet drie maar tien huisjes tekent is ronduit vervelend. We gaan wat orde scheppen in deze chaos met behulp van methoden. Nieuwe methoden Methoden zijn bedoeld om groepjes opdrachten die bij elkaar horen als e en geheel te kunnen behandelen. Op het moment dat het groepje opdrachten moet worden uitgevoerd, kun je de dat laten gebeuren door de methode aan te roepen. In het voorbeeld horen duidelijk steeds drie opdrachten bij elkaar die samen e en huisje tekenen (de aanroep van drawRect en de twee aanroepen van drawLine). Die drie opdrachten zijn dus een goede kandidaat om in een methode te zetten; in de methode paint komen dan alleen nog maar drie aanroepen van deze nieuwe methode te staan. De opzet van het programma wordt dus als volgt:

4.1 Methode-denitie

37

import java.awt.Graphics; import java.applet.Applet; // Deze applet tekent drie huizen van divers formaat
5

10

15

public class Huizen extends Applet { // Deze methode tekent een huis // met linkeronderhoek (x,y) en breedte br private void tekenHuis(Graphics gr, int x, int y, int br) { int topx, topy; topx = x + br/2; topy = y - 3*br / 2; gr.drawRect(x, y-br, br, br); gr.drawLine(x, y-br, topx, topy); gr.drawLine(topx, topy, x+br, y-br); } public void paint(Graphics g) { this.tekenHuis(g, 20, 100, 40); this.tekenHuis(g, 70, 100, 40); this.tekenHuis(g, 120, 100, 60); } private static final long serialVersionUID = 1; } Listing 4: Huizen/src/Huizen.java

20

25

Figuur 8: De applet Huizen in werking

38

Nieuwe methoden

public class Huizen extends Applet { private void tekenHuis( iets ) { iets .drawRect( iets ); iets .drawLine( iets ); iets .drawLine( iets ); } public void paint(Graphics g) { iets .tekenHuis( iets ); iets .tekenHuis( iets ); iets .tekenHuis( iets ); } }

Er zijn dus twee methoden: naast de gebruikelijke methode paint is er een tweede methode die e en huis tekent, en die we daarom tekenHuis noemen. De naam mag vrij worden gekozen, en het is een goed idee om die naam de taak van de methode een beetje te laten beschrijven. Als de browser het applet wil tekenen, roept hij de methode paint aan. Het maakt niet uit dat deze methode niet aan het begin van het programma staat: wat er verder ook allemaal in de klasse zit, de methode paint zal als eerste worden aangeroepen. Pas als de methode paint een aanroep doet van de methode tekenHuis, worden de opdrachten in de body van de methode tekenHuis uitgevoerd. Als dat klaar is, gaat paint weer verder met de volgende opdracht. In dit geval is dat toevallig weer een aanroep van tekenHuis, dus wordt er een tweede huis getekend. Ook bij de derde aanroep in paint wordt er een huisje getekend, en als daarna gaat het weer verder op de plaats van waaruit paint zelf werd aangeroepen. Methoden nemen een object onder handen Het raamwerk van het programma is nu klaar, maar er zijn nog de nodige details die ingevuld moeten worden (in het raamwerk aangegeven met iets). Als eerste bekijken we de vraag: wat komt er v o or de punt te staan bij de aanroep van de methode drawRect in de body van tekenHuis? Elke methode die je aanroept, krijgt een object onder handen; dit is het object dat je voor de punt in de methode-aanroep aangeeft. De methode drawRect bijvoorbeeld, krijgt een Graphics-object onder handen. Tot nu toe hebben we daar het Graphics-object voor gebruikt, dat als parameter beschikbaar was in de methode paint (en dat we steeds g noemden). De parameter van de methode paint is echter niet zomaar beschikbaar in de body van de methode tekenHuis. Parameters van methoden We moeten er dus voor zorgen dat ook in de body van tekenHuis een Graphics-object beschikbaar is, en dat kunnen we doen door ook tekenHuis een Graphics-object als parameter te geven. In de body van tekenHuis kunnen we die parameter dan mooi gebruiken voor de punt in de aanroep van drawRect en drawLine:
private void tekenHuis(Graphics gr, iets ) { gr.drawRect( iets ); gr.drawLine( iets ); gr.drawLine( iets ); }

Voor de afwisseling hebben we de parameter nu eens gr genoemd in plaats van g. Je mag als programmeur de naam van de parameter immers vrij kiezen. In de body van de methode moet je, als je de parameter wilt gebruiken, wel diezelfde naam gebruiken, dus ook daar gebruiken we nu het Graphics-object gr. De naam van het type van de parameter mag je niet zomaar kiezen: het object-type Graphics is een bestaande bibliotheek-klasse, en die mogen we niet ineens Grafiek of iets dergelijks gaan noemen. Nu we in de header van de methode tekenHuis gespeciceerd hebben dat de eerste parameter een Graphics-object is, moeten we er voor zorgen dat bij aanroep van tekenHuis ook inderdaad een Graphics-object wordt meegegeven. De aanroep van tekenHuis vindt plaats vanuit de methode paint, en daar hebben we gelukkig een Graphics-object beschikbaar: het object dat door de browser als parameter aan paint is meegegeven. De aanroepen van tekenHuis komen er dus als

4.2 Op zoek naar parameters volgt uit te zien:


public void paint(Graphics g) { iets.tekenHuis(g, iets ); iets.tekenHuis(g, iets ); iets.tekenHuis(g, iets ); }

39

De methode tekenHuis wordt alleen maar door paint aangeroepen, en niet door de browser (althans niet direct). De methode tekenHuis is daarom als een private methode gedeclareerd: hij is alleen voor intern gebruik. Let er op dat er e en Graphics-object is, dat door de verschillende methoden verschillend wordt genoemd: in paint heet het object g, en in tekenHuis heet het gr. En waarschijnlijk noemde de browser dit object, alvorens het aan paint mee te geven, weer anders. Of misschien ook wel niet het punt is dat dat er niet toe doet. Hoe methoden hun eigen parameters noemen is hun zaak, en degene die de methode aanroept hoeft niet te weten wat die naam is. De aanroeper heeft er alleen voor te zorgen dat op de betreende parameterplaats een waarde van het juiste type wordt meegegeven. Het object this Een volgend detail dat we nog moeten invullen in het programma is het object v o or de punt bij de aanroep van tekenHuis. Welk object krijgt tekenHuis eigenlijk onder handen? En welk object heeft paint zelf eigenlijk onder handen? Het is in ieder geval niet dat Graphics-object dat in de vorige paragraaf centraal stond: Graphicsobjecten kennen immers alleen maar draw. . . - en fill. . . -methoden, en geen methode paint en al zeker niet een methode tekenHuis. Het object dat door methoden onder handen wordt genomen, is van het object-type zoals dat in de klasse-header staat waarin de methode staat. De methode drawRect heeft een Graphics-object onder handen, omdat drawRect in de klasse Graphics staat. Welnu, de methoden paint en tekenHuis staan in de klasse Huizen, en hebben dus blijkbaar een eerd, waarna de methode Huizen-object onder handen. Zon Huizen-object is door de browser gecre paint werd aangeroepen met dat object onder handen. In de body van paint zouden we datzelfde object wel willen gebruiken om door tekenHuis onder handen genomen te laten worden. Maar hoe moeten we het object dat we onder handen hebben, aanduiden? Dit object is immers geen parameter, dus we hebben het in de methode-header geen naam kunnen geven. De oplossing van dit probleem is dat in Java het object dat een methode onder handen heeft gekregen, kan worden aangeduid met het woord this. Dit woord kan dus worden geschreven op elke plaats waar het object nodig is. Nu komt het dus goed van pas om in de body van de methode paint aan te geven dat bij de aanroep van tekenHuis hetzelfde object onder handen genomen moet worden als dat paint zelf al onder handen heeft:
public void paint(Graphics g) { this.tekenHuis(g, iets ); this.tekenHuis(g, iets ); this.tekenHuis(g, iets ); }

Het woord this is in Java een voor dit speciale doel gereserveerd woord. Je mag het dus (evenmin als class, extends, void, public, private en int) gebruiken als naam van een variabele of iets dergelijks. In elke methode duidt this een object aan. Dit object heeft als object-type dat wat in de header van de klasse staat waarin de methode is gedenieerd.

4.2

Op zoek naar parameters

Parameters maken methoden exibeler Het administratieve werk zorgen dat alle methoden over de benodigde Graphics- en Huizenobject kunnen beschikken is nu gedaan, en het leuke werk kan beginnen: de jacht op de overige parameters. Tot nu toe hebben we voor het gemak gezegd dat de huis-tekenende opdrachten (drawRect en tweemaal drawLine) in alle drie gevallen hetzelfde is, en dat ze daarom met drie aanroepen van

40

Nieuwe methoden

tekenHuis kunnen worden uitgevoerd. Maar de opdrachten die de drie huizen tekenen zijn niet precies hetzelfde: per huisje verschillen de getallen die als parameter worden meegegeven aan drawRect en drawLine. We kijken eerst maar eens naar de aanroepen van drawRect in de oorspronkelijke (chaotische) versie van het programma:
g.drawRect( 20, 60, 40, 40); g.drawRect( 70, 60, 40, 40); g.drawRect(120, 40, 60, 60);

De eerste twee getallen zijn de co ordinaten van de linkerbovenhoek van de rechthoek, de laatste twee getallen zijn de breedte en hoogte van de rechthoek. Omdat we vierkanten tekenen zijn de breedte en hoogte gelijk: 40 voor de kleine huisjes, en 60 voor het grote. De breedte (tevens hoogte) is niet in alle gevallen dezelfde. Als we de gewenste breedte echter door een parameter aangeven, dan kunnen we bij elke aanroep een andere breedte speciceren. Wat betreft de co ordinaten geldt hetzelfde: aangezien deze verschillend zijn bij alle drie de aanroepen, laten we de aanroeper van tekenHuis ook deze waarden speciceren. Voor de aanroeper is het waarschijnlijk gemakkelijker om de co ordinaten van de linker-onderhoek te speciceren: de co ordinaten van de bovenhoek zijn verschillend voor huizen van verschillende grootte, terwijl de y -co ordinaat van de onderhoek voor huizen op e en rij hetzelfde zijn. Ook dit kan geregeld worden: we spreken af dat de y -co ordinaat-parameter van de methode tekenHuis de basislijn van de huizen voorstelt, en de y -co ordinaat van de bovenhoek berekenen we met een expressie:
private void tekenHuis(Graphics gr, int x, int y, int br) { gr.drawRect(x, y-br, br, br); gr.drawLine( iets ); gr.drawLine( iets ); } public void paint(Graphics g) { this.tekenHuis(g, 20, 100, 40); this.tekenHuis(g, 70, 100, 40); this.tekenHuis(g, 120, 100, 60); }

De parameters van de twee aanroepen van drawLine (de co ordinaten van begin- en eindpunt van de lijnen die het dak van het huis vormen) zijn ook in alle gevallen verschillend. Het is echter niet nodig om die apart als parameter aan tekenHuis mee te geven; deze co ordinaten kunnen namelijk worden berekend uit de positie en de breedte van het vierkant, en die hebben we al als parameter. De co ordinaten van de top van het dak zijn twee maal nodig: als het eindpunt van de eerste lijn, en als beginpunt van de tweede. Om de berekening van dit punt niet twee maal te hoeven doen, gebruiken we twee variabelen om deze co ordinaten tijdelijk op te slaan. Deze variabelen zijn nodig in de methode tekenHuis, en worden dan ook in die methode gedeclareerd:
private void tekenHuis(Graphics gr, int x, int y, int br) { int topx, topy; topx = x + br/2; topy = y - 3*br / 2; gr.drawRect(x, y-br, br, br); gr.drawLine(x, y-br, topx, topy); gr.drawLine(topx, topy, x+br, y-br); }

Grenzen aan de exibiliteit Nu we besloten hebben om de linkeronderhoek van het huisje te speciceren (en niet de linkerbovenhoek van de gevel), blijkt de y -co ordinaat in alle drie de aanroepen van tekenHuis hetzelfde te zijn (namelijk 100). Achteraf gezien was deze parameter dus niet nodig geweest: we hadden de waarde 100 in de body van tekenHuis kunnen schrijven op alle plaatsen waar nu een y staat. Kwaad kan het echter ook niet om te veel parameters te gebruiken. Wie weet willen we later nog wel eens huisjes tekenen op een andere hoogte dan 100, en dan is onze methode er alvast maar op voorbereid. De vraag is wel hoe ver je moet gaan in het exibeler maken van methoden, door het toevoegen van extra parameters. De methode tekenHuis zoals we die nu hebben geschreven kan alleen maar

4.3 Methoden met een resultaat

41

huisjes met een vierkante gevel tekenen. Het is ook denkbaar om de breedte en de hoogte apart als parameter mee te geven, want wie weet willen we later nog wel eens een niet-vierkant huisje tekenen, en dan is de methode er alvast maar op voorbereid. En je zou de hoogte van het dak apart als parameter mee kunnen geven, want wie weet willen we later nog wel eens een huisje met een extra schuin of extra plat dak tekenen. En je zou nog een kleur-object apart als parameter kunnen meegeven, want wie weet willen we later nog wel eens een gekleurd huisje tekenen. Of twee kleur-objecten, zodat het dak een andere kleur kan krijgen dan de gevel. . . Al die extra parameters hebben wel een prijs, want bij de aanroep moeten ze steeds maar meegegeven worden. En als de aanroeper helemaal niet van plan is om al die variatie te gaan gebruiken, zijn die overbodige parameters maar tot last. De kunst is om een afweging te maken tussen de moeite die het kost om extra parameters te gebruiken (zowel voor de programmeur van de methode als voor de programmeur die de aanroepen schrijft) en de kans dat de extra exibiliteit in de toekomst ooit nodig zal zijn. Flexibiliteit in het groot Hetzelfde dilemma doet zich voor bij programmas als geheel. Gebruikers willen graag exibele software, die ze naar hun eigen wensen kunnen congureren. Maar ze zijn weer ontevreden als ze eindeloze lijsten met opties moeten instellen voordat ze aan het werk kunnen, en onnodige opties maken een programma maar complex en (daardoor) duur. Achteraf heb je makkelijk praten, maar had men in het verleden kunnen voorzien dat er ooit behoefte zou ontstaan aan een 4-cijferig jaartal in plaats van een 2-cijferig? (Ja.) Maar moeten we er nu al rekening mee houden dat in de toekomst de jaarkalender misschien een dertiende maand krijgt, en alle maanden 28 dagen? (Nou, nee). Moet de gebruiker van nanci ele software zelf kunnen instellen wat het geldende BTW-tarief is? Of moet de gebruiker, als het tarief ooit zal veranderen, maar een nieuwe versie van het programma kopen? En moet de software er nu al in voorzien dat er behalve een laag en een hoog BTW-tarief ook een midden-tarief komt? En dat de munteenheid verandert? En het symbool daarvoor? Moet de gebruiker van een programma waarin tijden een rol spelen zelf kunnen instellen op welke datum de zomertijd eindigt? Of is het beter als de regel daarvoor (laatste zondag van oktober) in het programma is ingebouwd? En als de regel dan veranderd wordt? Of moet de gebruiker zelf de regel kunnen speciceren? En mag hij dan eerst kiezen in welke taal hij oktober mag spellen?

4.3

Methoden met een resultaat

Numerieke typen Een variabele met het type int kan alleen maar gehele getallen bevatten. Om getallen met cijfers achter de komma te kunnen opslaan in een variabele, moet je een ander type gebruiken in de declaratie. Meestal wordt daarvoor het type double gebruikt (een afkorting van double precision oating point number). Na de declaratie
double d;

Kun je de variabele een waarde geven met een toekenningsopdracht


d = 3.141592653;

Overeenkomstig de angelsaksische gewoonte wordt in dit soort getallen een decimale punt gebruikt, en dus niet zoals in Nederland een decimale komma. Hoewel je in een double variabele dus ook gehele getallen kunt opslaan, is het toch zinvol om het onderscheid met int te maken, en daar waar je zeker weet dat er alleen maar gehele getallen nodig zijn een int te declareren. De voordelen daarvan zijn: een int kost minder opslagruimte dan een double berekeningen met een int gaan (iets) sneller dan met een double waarden worden altijd exact opgeslagen door het gebruik van een passend type documenteer je impliciet de bedoeling die je als programmeur met een bepaalde variabele hebt. Berekenen van functies Door middel van parameters kan de methode-aanroeper waarden (int-waarden, double-waarden, en zelfs waarden met een object-type zoals Graphics) doorspelen aan een methode, zodat die

42

Nieuwe methoden

waarden in de body van de methode gebruikt kunnen worden. Dit is echter e enrichtingverkeer: de methode kan niets terugzeggen tegen de aanroeper. Toch is dat vaak wel gewenst. Vergelijk het maar met het berekenen van functies in de wiskunde. Je kunt een functie, zoals de functie kwadraat, als het ware aanroepen door hem van een parameter te voorzien: kwadraat(5). Zon functie berekent een resultaat; in dit geval is dat 25. Dat antwoord is beschikbaar voor degene die de functie aanroept. In Java kun je methoden, net als wiskundige functies, ook een resultaat laten berekenen, dat door de aanroeper kan worden gebruikt. En net als in de wiskundige functies kan een methode meerdere parameters krijgen, maar heeft hij maar e en resultaat. Zon resultaatwaarde kan een int-waarde zijn, maar je kunt methoden ook een object-waarde laten teruggeven aan de aanroeper. Het resultaattype van een methode Net als de parameters, heeft de resultaat-waarde van een methode een type. Dat type kan int zijn als je een geheel getal wilt teruggeven, maar ook bijvoorbeeld het object-type Color, als je de methode een kleur wilt laten teruggeven. Het resultaattype van de methode moet in de header van de methode worden gespeciceerd, direct v o or de naam van de methode. De header van de kwadraat-methode kan er dus zo uitzien:
private double kwadraat(double x)

Als de methode geen resultaat heeft, staat op de plaats van het resultaattype het woord void. De return-opdracht In de body van een methode met een resultaat moet op de een of andere manier worden aangegeven wat het resultaat dan wel is. Voor dit doel is er in Java een speciale opdracht beschikbaar: de return-opdracht. Dit is de derde opdrcht-vorm die we tegenkomen, naast de methode-aanroep en toekenningsopdracht:

opdracht
methode

object variabele
return

. =

naam

( ; ;

expressie
,

expressie expressie

In de body van de kwadraat-methode staat als enige opdracht zon return-opdracht:


private double kwadraat(double x) { return x*x; }

Een return-opdracht bestaat uit het, speciaal voor dit doel gereserveerde, woord return, gevolgd door een expressie. Net als de toekennings-opdracht en de methode-aanroep wordt de returnopdracht afgesloten met een puntkomma. Op het moment dat de return-opdracht wordt uitgevoerd, wordt de expressie uitgerekend, gebruik makend van de op dat moment geldende waarden van variabelen en parameters. Die waarde wordt dan als resultaatwaarde opgeleverd aan de aanroeper van de methode. Aanroep van methoden Methoden met een resultaatwaarde moeten op een andere manier worden aangeroepen dan methoden met void als resultaat-type; de resultaatwaarde die door de return-opdracht in de methode wordt teruggegeven moet immers op de een of andere manier worden geaccepteerd. Aanroepen van void-methoden hebben de status van een zelfstandige opdracht. Bijvoorbeeld:
g.drawString("hallo", 10, 10);

Aanroep van een niet-void-methode heeft echter de status van een expressie. Die expressie staat niet op zichzelf, maar kan gebruikt worden in een opdracht naar keuze, bijvoorbeeld als rechterhelft van een toekenningsopdracht:

4.3 Methoden met een resultaat

43

double k; k = this.kwadraat(5);

Maar de aanroep van de methode kwadraat kan ook op andere plaatsen worden gebruikt waar double-waarden nodig zijn, bijvoorbeeld als onderdeel van een grotere expressie, of als parameter van een andere aanroep:
double oppervlakte, vierdemacht; oppervlakte = 3.1415926 * this.kwadraat(5); vierdemacht = this.kwadraat( this.kwadraat(5) );

Uiteindelijk zal je die grotere expressie dan toch weer moeten gebruiken in een opdracht (hetzij een toekenningsopdracht, hetzij een aanroep van een void-methode). Methodes die elkaar aanroepen Methodes kunnen elkaar aanroepen. De methode kwadraat bijvoorbeeld, komt goed van pas in de methode die de oppervlakte van een cirkel kan uitrekenen:
private double opper(double x) { return 3.1415926535 * this.kwadraat(x); }

Omdat de methoden opper en kwadraat zich in dezelfde klasse bevinden, kunnen ze elkaar aanroepen met this als betrokken object. Geen van beide methoden zet een resultaat op het scherm. Dat kunnen ze niet eens, want ze hebben niet de beschikking over een Graphics-object. Het opleveren van een resultaatwaarde betekent nog niet automatisch dat die waarde op het scherm verschijnt! Dat moet expliciet gebeuren, bijvoorbeeld door in de methode paint het resultaat van de methode op het scherm te tekenen:
public void paint(Graphics g) { g.drawString("oppervlakte: " + this.opper(5) ); }

In listing 5 en guur 9 staat het programma Opper, dat een tabelletje tekent met de straal en de bijbehorende oppervlaktes van een aantal cirkels. De berekening wordt gedaan met behulp van de extra methoden kwadraat en opper. De plaats van de return-opdracht Voorafgaand aan de return-opdracht kunnen nog andere opdrachten staan, die wat voorbereidende berekeningen uitvoeren. Onderstaande methode berekent bij een parameter x de waarde van (x + 1)3 :
private int derdeMachtVanOpvolger(int x) { int v; v = x+1; return v*v*v; }

blz. 44

Een return-opdracht zou in theorie ook halverwege een methode kunnen staan, en je zou zelfs twee return-opdrachten in e en methode kunnen zetten:
private int raar(int x) { return x*x; return x*x*x; }

Maar wat levert deze methode nou op: het kwadraat of de derdemacht van de parameter? Bij de eerste return-opdracht wordt de waarde uitgerekend en teruggegeven aan de aanroeper, en daarmee is de methode-aanroep ook meteen be eindigd. Aan het uitrekenen van de derdemacht komt deze methode raar helemaal nooit toe. Hoewel het dus in principe is toegestaan om een return-opdracht op een andere plaats dan als laatste in een methode te zetten, is dat in de praktijk zinloos: opdrachten na de return-opdracht kunnen nooit worden uitgevoerd. De compiler zal in zon geval dan ook waarschuwen dat de methode unreachable code bevat. Het woord return kun je op twee manieren vertalen:

44

Nieuwe methoden

import java.awt.Graphics; import java.applet.Applet; public class Opper extends Applet { private double kwadraat(double x) { return x*x; } private double opper(double r) { return 3.1415926535 * this.kwadraat(r); }
15

10

20

public void paint(Graphics g) { g.drawString("straal", 2, 10 ); g.drawString("oppervlakte", 60, 10 ); g.drawString("25 g.drawString("37 g.drawString("50 g.drawString("99 " " " " + + + + this.opper(25), this.opper(37), this.opper(50), this.opper(99), 10,35); 10,55); 10,75); 10,95);

25

g.drawLine(0, 15, 170, 15); g.drawLine(40, 0, 40, 100);


30

} private static final long serialVersionUID = 1; } Listing 5: Opper/src/Opper.java

Figuur 9: De applet Opper in werking

4.3 Methoden met een resultaat

45

teruggeven: de waarde van de expressie in de return-opdracht wordt teruggegeven aan de aanroeper; terugkeren: na het uitvoeren van de return-opdracht keert de processor terug naar de aanroeper, ongeacht of er nog meer opdrachten staan na de return-opdracht. Beide vertalingen zijn een adequate beschrijving van de return-opdracht.

Opgaven
4.1 Vermenigvuldigen en delen Is er verschil tussen de opdrachten (uit het programma Huizen):
topy = y - 3*br / 2; topy = y - 3/2 * br; topy = y - br/2 * 3;

4.2 Uren, minuten, seconden Stel dat de variabele tijd een (mogelijk groot) aantal seconden bevat. Schrijf een aantal opdrachten, waardoor de variabelen uren, minuten en seconden een daarmee overeenkomende waarde krijgen, waarbij de waarden van minuten en seconden kleiner dan 60 moeten zijn. Schrijf bovendien een methode die, het omgekeerde probleem, uit drie parameters uren, minuten en seconden de totale hoeveelheid seconden berekent. 4.3 Methoden en parameters Schrijf, gebruikmakend van de methode tekenHuis, een methode tekenStraat, die vier huizen naast elkaar tekent. Wat zijn geschikte parameters voor tekenStraat? 4.4 Expressie versus opdracht Wat is het verschil tussen een expressie en een opdracht? De aanroep van een methode vormt soms een expressie, en soms een opdracht; waardoor wordt dit onderscheid gemaakt? 4.5 Schubert In guur 10 is het beginstuk weergegeven van de melodie van een muziekstuk (het Impromptu opus 90 nummer 4 van Schubert). Schrik niet, je hoeft geen noten te kunnen lezen om deze opgave te maken; we leggen het hier even uit (als informaticus moet je je tenslotte snel kunnen inwerken in de eigenaardigheden en bijzondere notaties van een klant): Elke noot heeft een toonhoogte; hoe hoger hij in de balk staat, hoe hoger de noot klinkt. Voor het gemak van de niet-musici staat er een getal bij de noten geschreven: hoe groter, hoe hoger. Niet alle noten duren even lang. Noten met een dubbele dwarsbalk (de groepjes van 4) duren ieder 1 tijdseenheid; noten met een enkele vlag er aan (bijvoorbeeld nummer 70 op de eerste regel) duren twee tijdseenheden, en noten met alleen een verticale streep eraan (bijvoorbeeld de zes halverwege de tweede regel) duren 4 tijdseenheden. Het speciale symbool 0 op de eerste regel is een stilte van twee tijdseenheden. Voor de leesbaarheid staat er na elke 12 tijdseenheden (een maat) een verticale streep in de notenbalk. Veronderstel dat we in een klasse zitten, waar een methode play bestaat met twee parameters: een getal dat de toonhoogte voorstelt, en een getal dat de lengte voorstelt. We zouden nu een nieuwe methode schubert kunnen schrijven, die het hele muziekstuk afspeelt:
public void schubert( ) { this.play(83,1); this.play(87,1); this.play(83,1); this.play(80,1); this.play(80,1); this.play(83,1);

en zo verder alle andere 300 noten in dit fragment. Maar dat wordt wel een erg saai programma. Het kan handiger, door gebruik te maken van extra methoden. Stukken die twee keer voorkomen kun je in een methode onderbrengen, en twee keer

46

Nieuwe methoden

Figuur 10: Schubert, Impromptu op.90 nr.4

4.3 Methoden met een resultaat

47

aanroepen. Maar door handig gebruik te maken van parameters, kun je ook stukken die niet exact hetzelfde zijn, in e en methode onderbrengen, en aanroepen met verschillende parameters. De opgave luidt nu: schrijf een programma dat alle 306 noten afspeelt, waarbij je zo min mogelijk aparte aanroepen van play verwerkt. Buit dus alle aanwezige structuur uit in extra methoden: de regelmaat van groepjes van 4, maar ook de grotere structuren van 2 of 6 maten. In de practicum-directory met voorbeeldprogrammas staat een programma MidiMaker, dat inderdaad zon methode play bevat. Er zit ook een methode score in, die de aanroepen naar play bevat (vergelijk paint in een gewone applet). Je kunt deze methode score vervangen door je eigen methode, om deze opgave te testen. Als je het programma runt, wordt de muziek afgespeeld. 4.6 Fractale geometrie Tekenen met behulp van Graphics is wel handig, maar soms is het een nadeel dat je alle coordinaten van de lijnen die je wilt tekenen als precieze waarde moet opgeven. Soms is het handiger om de tekening relatief te speciceren, dus niet trek een lijn van A naar B , maar trek een lijn door vanaf het huidige punt S stappen naar voren te gaan. Om ook andere dan rechte lijnen te kunnen trekken heb je dan ook nodig: draai je neus G graden rond. Om ook niet-aaneengesloten guren te kunnen tekenen heb je verder nog nodig: zet de pen neer en trek de pen op. Tekenen op deze manier staat bekend als turtle graphics, omdat je als het ware een schildpad opdrachten kunt geven om te lopen, te draaien en een aan zijn schild bevestigde pen neer te zetten. In de practicum-directory staat een klasse TurtleApplet die het makkelijk maakt om zon turtletekening te maken. Om hem te gebruiken hoef je (nog) niet precies te begrijpen wat er in deze klasse gebeurt. Hoofdzaak is dat er een methode makeDrawing is, die door jou moet worden ingevuld. Het is de bedoeling dat er in die methode aanroepen komen te staan naar de andere methoden die er in de klasse aanwezig zijn: move, turn, en penUp en penDown. Je zou bijvoorbeeld een vierkant kunnen tekenen door eerst this.move(100); aan te roepen, dan this.turn(90);, en dat vier keer te herhalen. a. Maak een nieuw project aan, en kopieer de tekst van TurtleApplet in plaats van de Hallo-tekst die de JCreator-wizard voor je aan heeft gemaakt. Vul de methode makeDrawing in zodat er een vierkant wordt getekend. Compileer en run het programma. Nog chiquer is het om een tweede Java-le te maken, die op zijn beurt een extensie is van de klasse TurtleApplet. Daarin komt dan alleen de methode makeDrawing te staan, plus eventuele extra methoden die je nog gaat schrijven:
public class MijnTurtle extends TurtleApplet { public void makeDrawing() { // to be defined } }

b. Maak een nieuwe methode vierkant die een vierkant tekent. Roep de methode aan vanuit makeDrawing. Maak een tweede methode driehoek, en roep m aan. Experimenteer ook wat met andere aantallen lijnen en hoeken bij de aanroep van turn. c. Schrijf een methode koch met als parameter een lengte (ter ere van de wiskundige Helge von Koch, die in 1904 deze constructie bedacht). Laat m een lijn tekenen van die lengte, maar dan zodanig dat het middelste een-derde deel van die lijn ontbreekt, en wordt vervangen door twee lijnstukjes die een omtrekkende beweging maken, zoals in de guur hier linksonder:

d. Schrijf nu een methode koch2, met weer dezelfde parameter. Deze methode doet vrijwel hetzelfde als koch, echter waar koch de methode move aanroept, roept koch2 de methode koch aan; het resultaat is de guur hier rechtsboven e. Schrijf ook een methode koch3, die viermaal koch2 aanroept, een methode koch4 die viermaal koch3 aanroept, en koch5 die viermaal koch4 aanroept. Test de methode, door ze vanuit

48

Nieuwe methoden

makeDrawing alledrie aan te roepen met gebruikmaking van verschillende kleuren. f. Schrijf een methode vierkant5, die voor zijn vier zijden in plaats van move de methode koch5 aanroept. g. Maak een nieuwe versie van de koch-familie, met een extra parameter hoek. Deze parameter geeft aan over welke hoek er gedraaid wordt tussen de lijnsegmenten. Ook de waarde waardoor de lengte van het totaal gedeeld moet worden om de lengte van een van de lijnsegmenten te krijgen (in de vorige versie was dat dus 3) kan gevarieerd worden. De beste resultaten krijg je echter door te delen door 2 (1 + cos(hoek )). Let op dat de hoek hier niet in graden maar in radialen moet worden opgegeven. h. Maak een kerstkaart, door de methode vierkant5 aan te roepen, in verschillende kleuren, met verschillende waarden van hoek.

49

Hoofdstuk 5

Objecten en methoden
5.1 Variabelen
Programmeren is groeperen Om in grote en ingewikkelde programmas het overzicht te kunnen blijven behouden is het belangrijk om dingen die bij elkaar horen te groeperen en als e en geheel te beschouwen. We hebben daar al een aantal voorbeelden van gezien: Een groepje opdrachten die bij elkaar horen vormen een methode. Een aantal methoden die bij elkaar horen vormen een klasse. Een aantal klassen die bij elkaar horen zijn in de bibliotheek van standaardklassen ondergebracht in een package. Ook bij het werken met de JCreator programmeeromgeving wordt er op allerlei nivos gegroepeerd: De les met broncode van zelf-geschreven klassen, en de bijbehorende html-les die voor een bepaald programma nodig zijn vormen een project. Meerdere projecten waar je tegelijk aan bezig bent zijn ondergebracht in een workspace. Java is een object-geori enteerde programmeertaal. Natuurlijk spelen in zon taal objecten een belangrijke rol. Ook objecten zijn het resultaat van groepering van samenhangende zaken: een object is een groepje variabelen dat bij elkaar hoort. Object: groepje variabelen dat bij elkaar hoort In sectie 3.2 zijn variabelen ge ntroduceerd: een variabele is een geheugenplaats met een naam, die je kunt veranderen met een toekenningsopdracht. Een variabele x kan bijvoorbeeld op een bepaald moment de waarde 7 bevatten, en een tijdje later de waarde 12. In veel situaties is het handig om meerdere variabelen te groeperen en als e en geheel te behandelen. Bijvoorbeeld, met twee variabelen, laten we zeggen x en y, kun je de positie van een punt in het platte vlak beschrijven. Die twee variabelen zou je dan samen als e en positie-object kunnen beschouwen. Met drie getallen kun je een kleur beschrijven: de hoeveelheid rood, groen en blauw licht die in de kleur gemengd zijn. Drie variabelen die ieder een getal bevatten zijn dus samen als e en kleur-object te beschouwen. Voor het beschrijven van complexere zaken zijn veel meer variabelen nodig. Voor het beheer van een window op het scherm zijn variabelen nodig om de positie van het window te bepalen, de afmetingen, misschien de achtergrondkleur en de naam dit in de titelbalk staat, het aanwezig zijn van scrollers en als die er zijn de positie daarvan, om nog maar niet te spreken van de inhoud van het window. Het is duidelijk dat het erg gemakkelijk is om in een programma een window in zn geheel te kunnen manipuleren, in plaats van steeds opnieuw met al die losse variabelen te worden geconfronteerd. Het is lang niet altijd nodig om precies te weten uit welke variabelen een bepaald object is opgebouwd. Het kan handig zijn om je er ongeveer een voorstelling van te maken, maar strikt noodzakelijk is dat niet. Om je een voorstelling te maken van een kleur-object kun je aan een groepje van drie variabelen denken, maar ook zonder die kennis zou je een kleur-object kunnen manipuleren. We hebben dat in hoofdstuk 3 gedaan: door het meegeven van een Color-object aan de methode setColor werd de kleur van de vlakken in het Mondriaan-schilderij bepaald. Dat kan, zonder dat we hoefden te weten dat een kleur-object in feite een groepje van drie variabelen is. Het is eerder regel dan uitzondering dat je niet precies weet hoe een object is opgebouwd. In programmas worden windows, buttons, les en allerlei andere objecten gebruikt, zonder dat de

blz. 25

50

Objecten en methoden

blz. 101

programmeur de opbouw van die objecten in detail kent. Die details worden (gelukkig) afgeschermd in de bibliotheek-klasssen. Van de meeste standaard-objecten (windows, buttons enz.) is het zelfs zo dat je de opbouw niet te weten kan komen, zelfs als je dat uit nieuwsgierigheid zou willen. Dat is geen pesterij: de opbouw van objecten wordt geheim gehouden om de auteur van de standaard-bibliotheek de vrijheid te geven om in de toekomst een andere opbouw te kiezen (bijvoorbeeld omdat die eci enter is), zonder dat bestaande programmas daaronder te leiden hebben. Als je zelf nieuwe object-typen samenstelt (we gaan dat doen in hoofdstuk 9) dan moet je natuurlijk wel weten hoe zon object is opgebouwd. Maar zelfs dan kan het een goed idee zijn om dat zo snel mogelijk weer te vergeten, en je eigen objecten waar mogelijk als ondeelbaar geheel te behandelen. Voorlopig gebruiken we alleen standaardobjecten, zoals kleur-objecten en tekst-objecten. Om die te kunnen manipuleren is het nodig om eerst iets meer te weten over declaraties. Declaratie: aangifte van het type van een variabele In sectie 3.2 hebben we gezien dat je de variabelen die je in je programma wilt gebruiken moet declareren. Dat gebeurt door middel van een zogeheten declaratie, waarin je de namen van de variabelen opsomt en hun type aangeeft. Een voorbeeld van een declaratie is
int x, y;

Je maakt daarmee ruimte in het geheugen voor twee variabelen, genaamd x en y, en geeft aan dat het type daarvan int is. Het type int staat voor integer number, oftewel geheel getal. Je kunt je de situatie in het geheugen als volgt voorstellen:

De geheugenplaatsen zijn beschikbaar (in de tekening gesymboliseerd door het hok), maar ze hebben nog geen waarde. Een variabele krijgt een waarde door middel van een toekennigsopdracht, zoals
x = 20;

De situatie in het geheugen wordt daarmee:

20

Met een tweede toekenningsopdracht kan ook aan de variabele y een waarde worden toegekend. In de expressie aan de rechterkant van het =-teken kan de variabele x worden gebruikt, omdat die inmiddels een waarde heeft. Bijvoorbeeld:
y = x+5;

Na het uitvoeren van deze opdracht is de situatie als volgt:

20

25

Het kan gebeuren dat later een andere waarde aan een variabele wordt toegekend, bijvoorbeeld met
x = y*2;

De variabele x krijgt daarmee een nieuwe waarde, en de oude waarde gaat voor altijd verloren. De situatie die daardoor ontstaat is als volgt:

50

25

Merk op dat met toekenningsopdrachten de waarde van een variabele kan veranderen. De naam wordt echter met de declaratie denitief vastgelegd. Om te zien wat er in ingewikkelde situaties gebeurt, kun je de situatie op papier naspelen. Teken daartoe voor elke declaratie met pen een hok met bijbehorende naam. De toekenningsopdrachten voer je uit door het hok van de variabelen met potlood in te vullen, waarbij je een eventuele oude inhoud van het hok eerst uitgumt.

5.1 Variabelen

51

blz. 36

Numerieke typen In hoofdstuk 4 zagen we het type double. Variabelen van dat type kunnen getallen met cijfers achter de decimale punt bevatten. Na de declaratie
double d;

Kun je de variabele een waarde geven met een toekenningsopdracht


d = 3.141592653;

3.14159265

Overeenkomstig de angelsaksische gewoonte wordt in dit soort getallen een decimale punt gebruikt, en dus niet zoals in Nederland een decimale komma. Variabelen van het type double kunnen ook gehele getallen bevatten; er komt dan automatisch 0 achter de decimale punt te staan:
d = 10;

10.0

Anders dan bij het type int, treden er bij deling van double variabelen slechts kleine afrondingsfouten op:
d = d / 3;

3.33333333

Naast int en double zijn er in Java nog vier andere types voor numerieke variabelen. Vier van de zes numerieke types kunnen worden gebruikt voor gehele getallen. Het verschil is het bereik van de waarden die kunnen worden gerepresenteerd: type byte short int long kleinst mogelijke waarde 128 32768 2147483648 9223372036854775808 grootst mogelijke waarde 127 32767 2147483647 9223372036854775807

Het type long is alleen maar nodig als je van plan bent om extreem grote of kleine waarden te gebruiken. De types byte en short worden gebruikt als het bereik van de waarden beperkt blijft. De besparing in geheugengebruik die dit oplevert is eigenlijk alleen de moeite waard als er erg veel (duizenden of zelfs miljoenen) van dit soort variabelen nodig zijn. Ook voor getallen met cijfers achter de komma zijn er twee verschillende types beschikbaar. Ze verschillen behalve in de maximale waarde die kan worden opgeslagen ook in het aantal signicante cijfers dat beschikbaar is. type float double signicante cijfers circa 8 circa 15 grootst mogelijke waarde 3.4028235 1038 1.7976931348623157 10308

Hier is het type float het zuinigst met geheugenruimte; desondanks wordt het over het algemeen het type double gebruikt, ook in de standaardmethoden.

52

Objecten en methoden

Objectverwijzingen In sectie 1.2 hebben we een object gedenieerd als een groepje variabelen dat bij elkaar hoort. Een object kan als e en geheel behandeld worden, bijvoorbeeld door het als parameter aan een methode mee te geven. Een voorbeeld van een object is een kleur. (Bijna) elke kleur kan namelijk door middel van drie getallen worden weergegeven: de hoeveelheid rood, de hoeveelheid groen en de hoeveelheid blauw licht die moeten worden gemengd om de kleur te verkrijgen. In Java (en de meeste andere object-geori enteerde talen) heeft elk object een type. Een kleur is iets anders dan (bijvoorbeeld) een lettertype, en daarom bestaan er Color-objecten en Font-objecten. In de standaardbibliotheken is een groot aantal object-typen beschikbaar. Je kunt ook zelf nieuwe object-typen deni eren. Net als voor waarden van numerieke typen, kun je variabelen declareren om waarden van objecttypen op te slaan. De declaratie bestaat, net als bij declaraties van numerieke variabelen, uit het type en een of meer zelfbedachte namen:
Color geel;

In zon variabele met een object-type kan een verwijzing naar een object worden opgeslagen, dat op zijn beurt weer uit variabelen bestaat.
geel

Met puur de declaratie wijst de variabele echter nog niet naar een bepaald object. Net als bij numerieke variabelen is er een toekenningsopdracht nodig om de variabele een waarde te geven. In dit geval is de waarde een verwijzing, die je je kunt voorstellen als een pijl naar het eigenlijke object:
geel = Color.YELLOW;

geel

r g b

255 255 0

Een Color-object is een groepje van drie variabelen, die als e en geheel behandeld worden. De pijl wijst dus naar het hele groepje. Het is niet het object zelf dat geel heet, maar de verwijzingsvariabele. Dat dat onderscheid belangrijk is, blijkt als je een tweede variabele declareert, en die met een toekenningsopdracht gelijk maakt aan de eerste:
Color gelb; gelb = geel;

geel gelb

r g b

255 255 0

De waarde van de variabele gelb is gelijk gemaakt aan die van de variabele geel. Dat wil zeggen dat gelb nu dezelfde verwijzing bevat als geel, en dus naar hetzelfde object verwijst. Het is de pijl die is gekopieerd, niet het object zelf. Dat maakt dat een toekenning aan variabelen met een klasse als type altijd snel kan gebeuren, ook als het om hele grote objecten gaat. De geheugenruimte die nodig is om verwijzingen op te slaan is gelijk aan die van een int: 4 bytes. Voorbeelden van objectverwijzingstypen In de standaardbibliotheken zijn een groot aantal typen objecten gedenieerd. De verzameling wordt met elke versie van Java verder uitgebreid (en soms komen er ook te vervallen als er een beter alternatief beschikbaar komt). Om een idee te geven van welke object-typen er bestaan volgt hier een kleine selectie:

5.1 Variabelen

53

opdrachten
zijn gegroepeerd in

veranderen

variabelen
zijn gegroepeerd in

methoden
zijn gegroepeerd in

bewerken

objecten
hebben als type

klasse

klasse

klasse

Figuur 11: De twee rollen van het begrip klasse

typen van objecten waarin heel duidelijk een klein groepje variabelen is te herkennen: Dimension (twee gegroepeerde variabelen: een lengte en een breedte), Color (drie variabelen: rood, groen en blauw), GregorianCalendar (een groepje variabelen die een datum en een tijd kunnen bevatten). typen van objecten met een wat complexere structuur, die een zeer natuurlijke eenheid vormen: String (een tekst bestaande uit nul of meer lettertekens), Font (een lettertype), BufferedImage (een afbeelding) typen van objecten die nodig zijn om een interactieve interface te maken: Button (een drukknop op het scherm), Scrollbar (een schuifregelaar), TextField (een invulveld voor de gebruiker), maar ook samengestelde componenten zoals Frame en zelfs Applet typen van objecten waarin de details van een bepaalde gebeurtenis kunnen worden weergegeven, zoals MouseMotionEvent en KeyEvent typen van objecten die een bepaald kunstje kunnen uitvoeren: Graphics (om een tekening te maken), Scanner (om een tekst in stukjes te splitsen) typen van objecten waarmee les en internet-verbindingen gemanipuleerd kunnen worden: File, URL, FileReader, FileWriter en vele anderen. Van al deze objecttypen kunnen variabelen worden gedeclareerd. De variabelen kunnen een verwijzing bevatten naar een object van het betreende type. Klasse: groepje methoden ` en type van object In sectie 1.2 hebben we een klasse gedenieerd als een groepje methoden met een naam. Als je zelf methoden schrijft moet je die onderbrengen in een klasse: zo was de methode tekenHuis in hoofdstuk 3 een methode in de klasse Huizen. Ook de standaardmethoden zijn ondergebracht in een klasse: zo is de methode drawRect bijvoorbeeld beschikbaar in de klasse Graphics. Maar klassen spelen nog een andere rol: ze zijn het type van objecten. De naam van de klasse kan op de plaats staan van het type bij de declaratie van een variabele, net zoals numerieke basistypen dat kunnen. Vergelijk:
int x; Color geel;

blz. 25

De twee rollen die een klasse kan spelen zijn sterk met elkaar verbonden. Methoden hebben immers een object onder handen (het object dat voor de punt staat in de methode-aanroep). Dat object bestaat uit variabelen, die kunnen worden veranderd door de opdrachten in de methode. Objecten die een bepaalde klasse als type hebben, kunnen onder handen worden genomen door de methoden uit die klasse. Of anders gezegd: de methoden van een klasse kunnen objecten onder handen nemen, die die klasse als type hebben. In guur 11 staat de samenhang tussen de begrippen opdracht, variabele, methode, object en klasse, waarbij de dubbele rol van klassen duidelijk naar voren komt.

54

Objecten en methoden

5.2

Typering

Typering voorkomt fouten Elke variabele heeft een type, die door de declaratie van de variabele is vastgelegd. Het type kan een van de zes numerieke basistypen zijn (de variabele kan dan een getal van dat type bevatten) of een klasse (de variabele kan dan een verwijzing naar een object van die klasse bevatten). Declaraties worden verwerkt door de compiler. Dat is wat ze onderscheidt van opdrachten, die tijdens het runnen van het programma worden uitgevoerd. Door de declaraties kent de compiler de typen van alle variabelen. De compiler is daardoor in staat om te controleren of aanroepen van methoden wel zinvol zijn. Methoden uit een bepaalde klasse kunnen immers alleen worden aangeroepen met een object onder handen dat die klasse als type heeft. Klopt de typering niet, dan geeft de compiler een foutmelding. De compiler maakt dan geen bytecode aan, en het programma kan dus niet worden uitgevoerd. Hoewel het in de praktijk een heel gedoe kan zijn om de compiler helemaal tevreden te stellen wat betreft de typering van het programma, is dat verre te prefereren boven de situatie waar vergissingen met de typering pas aan het licht zouden komen bij het uitvoeren van het programma. In programmeertalen waarin geen of een minder strenge vorm van typering wordt gebruikt kunnen er verborgen fouten in een programma zitten. Als de bewuste opdrachten bij het testen toevallig niet aan bod zijn gekomen, blijft de fout in de code sluimeren totdat een ongelukkige gebruiker in een onwaarschijnlijke samenloop van omstandigheden de foute opdracht eens tegenkomt. Voor de programmeur is het een onrustbarende gedachte dat dat zou kunnen gebeuren daarom is het goed dat voor Java de compiler de typering zo streng controleert. Als de compiler geen foutmeldingen meer geeft, betekent dat niet dat het programma ook gegarandeerd foutvrij is. Een compiler kan natuurlijk niet de bedoeling van de programmeur raden, en waarschuwen voor het feit dat er een rode cirkel wordt getekend in plaats van een groene. Wel kan de compiler weten dat groen als diameter van een cirkel nooit kan kloppen, omdat groen een kleur is en de diameter een getal moet zijn. De compiler controleert de typen van objecten die door een methode onder handen worden genomen, en ook van alle parameters van een methode. Ook bij het gebruik van rekenkundige operatoren worden de types van de twee argumenten gecontroleerd, zodat bijvoorbeeld niet twee kleuren vermenigvuldigd kunnen worden, maar alleen getallen van een van de zes numerieke typen. Conversie van numerieke typen Waarden van numerieke typen zijn in sommige situaties uitwisselbaar. Zo is de waarde 12 in principe van het type int, maar het is ook acceptabel als rechterkant van een toekenningsopdracht aan een variabele van type double. Bijvoorbeeld, na de declaraties
int i; double d;

Zijn de volgende toekenningen acceptabel:


i = 12; d = 12; d = i; // waarde wordt automatisch geconverteerd // waarde wordt automatisch geconverteerd

Bij de toekenningen van een int-waarde aan de double variabele, of dat nu een constante is of de waarde van een int-variabele, wordt de waarde automatisch geconverteerd. Omgekeerd is toekenning van een double-waarde aan een int-variabele niet mogelijk, omdat er in een int-variabele geen ruimte is voor cijfers achter de decimale punt. De controle wordt uitgevoerd door de compiler op grond van de typen. Een double-expressie is nooit acceptabel als waarde voor een int-variabele, zelfs niet als de waarde toevallig een nul achter de decimale punt heeft. De compiler kan dat namelijk niet weten, omdat de uitkomst van berekeningen kan afhangen van de situatie tijdens het runnen. De controle gebeurt puur op grond van het type, en daarom zijn zelfs toekenningen van constanten met 0 achter de decimale punt aan een int-variabele verboden.
d i i i i i = = = = = = 2.5; 2.5; d; 2*d; 5.0; 5; // // // // // // dit is goed FOUT: double-waarde past niet in een int FOUT: double-waarde past niet in een int FOUT: typecontrole doet geen berekeningen FOUT: 5.0 blijft een double dit mag natuurlijk wel

5.2 Typering

55

Het kan natuurlijk gebeuren dat je als programmeur zeker weet dat de conversie van double naar int in een bepaalde situatie w` el verantwoord is. Je kunt dat aan de compiler aangeven door v o or de expressie tussen haakjes het gewenste type te zetten, dus bijvoorbeeld:
i = (int) d; i = (int) (2*d); // cast converteert double naar int // cast van een double-expressie

De compiler accepteert de toekenningen, en converteert de double-waarden naar int-waarden door het gedeelte achter de decimale punt weg te gooien. Als er 0 achter de decimale punt staat is dat natuurlijk geen probleem; anders gaat er enige informatie verloren. Als programmeur geef je door het expliciet vermelden van (int) aan dat je dat geen probleem vindt. De conversie is een ruwe manier van afronden: 2.3 wordt geconverteerd naar 2, maar ook 2.9 wordt 2. De cijfers achter de decimale punt worden zonder meer weggegooid, er wordt niet afgerond naar de dichtstbijzijnde integer. Deze notatie, waarmee expressies van een bepaald type kunnen worden geconverteerd naar een ander type, staat bekend als een cast. Letterlijk is de betekenis daarvan (althans een van de vele) een gietvorm; door middel van de cast wordt de double-expressie als het ware in de gietvorm van een int geperst. Behalve voor conversie van double naar int, kan de cast-notatie ook worden gebruikt om conversies af te dwingen van long naar int, van int naar short, van short naar byte, en van double naar float; kortom in alle gevallen waarin de compiler het onverantwoord acht om een grote waarde in een kleine variabele te stoppen, maar waarin je als programmeur kan beslissen om de toekenning toch te laten plaatsvinden. Voor conversie van klein naar groot is een cast niet nodig, omdat daarbij nooit informatie verloren kan gaan. Operatoren en typering Bij het gebruik van rekenkundige operatoren hangt het van de typen van de argumenten af, op welke manier de operatie wordt uitgevoerd: zijn beide argumenten een int, dan is het resultaat ook een int; bijvoorbeeld: het resultaat van 2*3 is 6, en het type daarvan is int. zijn beide argumenten een double, dan is het resultaat ook een double; bijvoorbeeld: het resultaat van 2.5*1.5 is 3.75, en het type daarvan is double. is e en van de argumenten een int en de andere een double, dan wordt eerst de int geconverteerd naar double, waarna de berekening op doubles wordt uitgevoerd; het resultaat is dan ook een double. Bijvoorbeeld: het resultaat van 10.0/4 is 2.5, en het type daarvan is double. Vooral bij een deling is dit van belang: bij een deling tussen twee integers wordt het resultaat naar beneden afgerond. Bijvoorbeeld: het resultaat van 10/4 is 2, met als type int. Als het resultaat daarna in een double variabele wordt opgeslagen, bijvoorbeeld met de toekenningsopdracht d=10/4; dan wordt de int 2 weer teruggeconverteerd naar de double 2.0, maar dan is het kwaad al geschied. Een dergelijke regel geldt voor alle expressies waar een operator wordt toegepast op verschillende numerieke typen, bijvoorbeeld een int en een long: eerst wordt het kleine type geconverteerd naar het grote type, daarna wordt de operatie uitgevoerd, en het resultaat is het grote type. De rekenkundige operatoren - ,*, / en % kunnen alleen worden toegepast op numerieke basistypen. Een uitzondering is de operator + . Deze kan behalve op numerieke typen ook worden toegepast op twee String-objecten. De betekenis van het optellen van twee strings is dat de twee teksten aan elkaar worden geplakt; het resultaat is dus ook weer een String. Bijvoorbeeld, na
String s, t; s = "Jeroen"; t = "Hallo " + s;

bevat de variabele t een verwijzing naar de string "Hallo Jeroen".


s t J e r o e n H a l l o J e r o e n

56

Objecten en methoden

Is e en van de argumenten een String, en de andere een numerieke waarde, dan wordt het getal eerst geconverteerd naar een String, en worden de strings daarna aan elkaar geplakt. Je kunt dat goed gebruiken om in de methode paint het resultaat van een berekening op het scherm te zetten, compleet met toelichting:
double x; x = 3.1415926 * 5 * 5; g.drawString("oppervlakte is " + x, 10, 10);

Wil je de een numerieke waarde converteren naar een String, dan kun je dit mechanisme gebruiken door het getal samen te voegen met een lege String: de conversie vindt dan wel plaats, maar er wordt geen extra tekst toegevoegd:
g.drawString("" + x, 10, 10);

5.3

Methoden

Methoden hebben een object onder handen Methoden hebben een object onder handen. Anders gezegd: methoden bewerken een object. Het type van dat object moet de klasse zijn waarin de methode gedenieerd is. Het object dat door de methode wordt bewerkt, wordt in de methode-aanroep vermeld v o or de punt. Een voorbeeld is
g.drawLine(10, 10, 30, 50);

blz. 25 blz. 36

Het object dat door drawLine onder handen wordt genomen is het object waar de variabele g naar verwijst. Die variabele moet dan wel zijn gedeclareerd met Graphics g, want drawLine is een methode in de klasse Graphics. In de voorbeelden in hoofdstuk 3 en hoofdstuk 4 was dat inderdaad het geval: de Graphics-variabele was daar gedeclareerd als parameter van de methode paint. De methoden kunnen de waarde van variabelen waaruit het object is opgebouwd inspecteren, om ze te gebruiken in bijvoorbeeld een berekening of handeling. De aanroep van drawLine is daarvan een voorbeeld: het Graphics-object waar g naar wijst is wel nodig bij het tekenen van de lijn, maar het object wordt er niet door veranderd. Het is ook mogelijk dat methoden een object veranderen, door toekenningen te doen aan een of meer variabelen die deel uitmaken van het object. Een voorbeeld daarvan is de methode setColor. Immers, na een aanroep als
g.setColor(Color.GREEN);

weet het object waar g naar wijst dat in de toekomst de kleur groen gebruikt moet worden bij teken-activiteiten. Blijkbaar is deze kleur opgeslagen in een van de variabelen van het object. Resultaatwaarde van een methode Methoden kunnen een waarde als resultaat opleveren. De (meestal laatste) opdracht van de methode-body moet dan een return-opdracht zijn, zoals in sectie 4.3 is besproken. Het resultaat van de methode heeft, zoals elke expressie, een type. In de header van de methode is aangegeven wat het type van het resultaat van de methode is. Een voorbeeld van een methode met een int als resultaat is de methode length in de klasse String. Met deze methode kan de lengte, dat wil zeggen het aantal lettertekens van een String bepaald worden. Bijvoorbeeld, als je een String hebt gedeclareerd en van een waarde hebt voorzien met
String s; s = "deze zin bevat vierendertig tekens";

dan kun je de lengte van de string bepalen met


int x; x = s.length();

Het resultaat van de aanroep van methode length is een int-waarde. Die moet dus gebruikt worden in een context waarin een int-waarde zinvol is. Dat kan de rechterkant van een toekenningsopdracht aan een int-variabele zijn, zoals in het voorbeeld hierboven. Maar er zijn vele andere contexten denkbaar voor een int-waarde, bijvoorbeeld als parameter van een methode die een int-waarde verwacht, of in een berekening (waarvan het resultaat dan weer in een context gebruikt moet worden):

5.3 Methoden

57

g.drawString(s, 10, s.length() ); // hoe langer de string, hoe lager op het scherm g.drawString("aantal tekens: " + s.length(), 5, 5 ); // toon de lengte van de string

In plaats van een getal kan een methode ook een object als resultaat hebben. In de methodeheader staat dan een klasse als resultaattype. Bijvoorbeeld, een TextComponent-object kan bewerkt worden door de methode getText, met een String als resultaat. (Een TextComponent is een veld op het scherm waar de gebruiker tekst kan invullen. Met de methode getText kan de tekst worden opgevraagd die door de gebruiker op een bepaald moment is ingevuld. Meer hierover in hoofdstuk 7).
TextComponent tc; String s; . . . en verderop in de methode. . . s = tc.getText();

blz. 77

Ook hier geldt weer dat het resultaat van de aanroep van getText in een context gebruikt moet worden waar een String-waarde nodig is. Dat kan een toekenningsopdracht aan een Stringvariabele zijn, zoals in het voorbeeld hierboven. Maar ook zou je de zojuist bepaalde string meteen weer onder handen kunnen laten nemen door een methode uit de klasse String, bijvoorbeeld de methode length:
x = tc.getText().length();

Het resultaattype van een methode kan toevallig hetzelfde zijn als de klasse waar de methode deel van uitmaakt; met andere woorden: het resultaattype kan hetzelfde zijn als het type van het object dat onder handen wordt genomen. Een voorbeeld is de methode substring in de klasse String. Het resultaat daarvan is een deel van de oorspronkelijke string, waarvan een nader op te geven aantal tekens (vanaf het begin) komt te vervallen. Bijvoorbeeld:
staartstuk = s.substring(10); tweedeHelft = s.substring( s.length()/2 );

Een ander voorbeeld is de methode toUpperCase, die een naar hoofdletters omgezette kopie van de string oplevert:
h = s.toUpperCase();

s h

H a l l o ! H A L L O !

Let op dat zowel substring als toUpperCase de string die onder handen wordt genomen onveranderd laat. Het resultaat is een nieuwe string. Je kunt natuurlijk wel besluiten om de oorspronkelijke variabele naar die nieuwe string te laten wijzen:
s = s.toUpperCase();

H a l l o ! H A L L O !

Iets dergelijks geldt voor de methode darken in de klasse Color. Het onder handen genomen Colorobject blijft onveranderd; de methode levert een nieuw Color-object op, die een iets donkerdere kleur (in dezelfde tint) van de oorspronkelijke kleur bevat. Bijvoorbeeld:
Color geel, donkerGeel; geel = Color.YELLOW; donkerGeel = geel.darken();

58

Objecten en methoden

Het is aan de naam van de methode niet altijd goed te zien of het onder handen genomen object wordt veranderd, of dat er een gewijzigde kopie van het object wordt opgeleverd. Dat is dus iets om goed op te letten bij het lezen van de beschrijving van een methode in de handleiding. Statische methoden hebben geen object onder handen In uitzonderlijke gevallen is het niet zinvol dat een methode een object onder handen neemt. Een voorbeeld is de methode sqrt, die de vierkantswortel (square root) van een getal berekent. Als parameter gaat er een double-waarde in, en als resultaat komt er een double-waarde uit, maar er is geen relevant object dat onder handen genomen wordt door de methode. Dit soort methoden heten statische methoden (Engels: static methods). In de header van de methode wordt, v o or het resultaattype door middel van het speciale woord static aangegeven dat een methode statisch is. Bij de aanroep van een statische methode hoeft er voor de punt geen object te staan; er is immers geen object dan onder handen genomen wordt. In plaats daarvan staat er in de aanroep voor de punt de naam van de klasse waarin de methode zich bevindt. De methode sqrt bevindt zich bijvoorbeeld in de klasse Math, en kan dus als volgt worden aangeroepen:
double wortelTwee; wortelTwee = Math.sqrt(2.0);

In de klasse Math zijn behalve sqrt nog veel meer statische methoden gedenieerd voor allerlei wiskundige functies: logaritme, sinus, absolute waarde, machtsverheen, minimum, random waarde, enzovoort. Manieren om objecten te verkrijgen We inventariseren nog eens de verschillende manieren die er zijn om een object te pakken te krijgen. Tot nu toe zijn we er drie tegengekomen: gebruik van een object-parameter van een methode (bijvoorbeeld de Graphics-parameter van de methode paint); aanroep van een methode met een object als resultaatwaarde (bijvoorbeeld de methode substring, die een String-object oplevert); aanroep van een operator met een object als resultaatwaarde (bijvoorbeeld de operator + die, als je er twee strings instopt, een nieuw String-object oplevert. Er is echter nog een vierde manier: creatie van een nieuw object vanuit het niets. Constructormethoden cre eren nieuwe objecten Voor het maken van gloednieuwe objecten, dus zonder daarbij andere, reeds bestaande objecten te gebruiken, is in Java een speciaal mechanisme beschikbaar. In bijna alle klassen is er een zogenaamde constructormethode aanwezig, die je kunt gebruiken om een nieuw object van die klasse te cre eren. Bij aanroep van een constructormethode gebeurt er het volgende: ergens in het geheugen wordt een stukje ruimte gereserveerd voor een nieuw object; het nieuwe object wordt door de constructormethode meteen even onder handen genomen, zodat het object in een zinvolle begintoestand komt; het nieuwe object (althans een verwijzing daarnaar) wordt als resultaat van de methode opgeleverd. De constructormethode heeft altijd dezelfde naam als de klasse (en deze naam begint dus, bij wijze van uitzondering voor methode-namen, ook met een hoofdletter). Bijvoorbeeld: de constructormethode waarmee een nieuw Color-object gemaakt kan worden heet Color, en de constructormethode waarmee een nieuw Button-object gemaakt kan worden heet Button. Aanroep van een constructor-methode gebeurt op een speciale manier. Er is immers nog geen object dat onder handen genomen kan worden (dat willen we juist construeren), dus de normale vorm van methode-aanroep (object-punt-methodenaam) werkt niet. Om een constructormethode aan te roepen schrijf je het woord new, gevolgd door de naam van constructormethode, gevolgd door eventuele parameters. Let op: er staat geen punt tussen het woord new en de methodenaam! Het woord new is weer zon gereserveerd woord, dat alleen gebruikt kan worden voor dit speciale doel. Hier zijn twee voorbeelden van aanroepen van constructormethoden:

5.4 Constanten

59

new Color(178,255,152) new Button("druk hier")

De new-constructie is een expressie De aanroep van een constructormethode met behulp van new heeft de status van een expressie: kijk het maar na in het syntax-diagram van expressie in bijlage G. De twee voorbeelden hierboven zijn dus geen complete opdrachten (er stond dan ook geen puntkomma achter!). De aanroep is een expressie, omdat de methode een resultaatwaarde heeft: een (verwijzing naar) een nieuw object. Die resultaatwaarde kan ergens worden opgeslagen met een toekenningsopdracht, en het ligt voor de hand om daar eerst een variabele voor te declareren:
Color lichtgroen; Button bevestig; lichtgroen = new Color(178,255,152); bevestig = new Button("druk hier");

lichtgroen

r g

178

255 b 152 d r u k w h 100 20 h i e r

bevestig

caption size action

enzovoorts... ...
Maar het is ook mogelijk om het zojuist geconstrueerde object in een andere context te gebruiken waar een Color-object nodig is, bijvoorbeeld als parameter van een methode die een object van dat type verwacht:
g.setColor( new Color(178,255,152) );

5.4

Constanten

Numerieke constanten Constanten van het type int kun je in het programma gewoon opschrijven als een rijtje cijfers. Dat ligt zo voor de hand dat we het al vele malen hebben gebruikt. Om negatieve getallen aan te geven zet je een minteken voor de cijfers. Dit zijn een paar voorbeelden van int-constanten:
0 3 17 1234567890 -5 -789

De grootst mogelijke int-waarde is ruim 2 miljard; ga daar niet overheen! Wil je een long-getal groter dan 2 miljard aanduiden, zet dan de letter L achter het getal:
long wereldPopulatie; wereldPopulatie = 6772000000L;

In bijzondere gevallen wil je een getal misschien in de 16-tallige (hexadecimale) notatie aangeven. Dat kan; je begint het getal dan met 0x, waarachter behalve de cijfers 0 tot en met 9 ook de cijfers a tot en met f mogen volgen. Voorbeelden: 0x10 (waarde 16) 0xa0 (waarde 160) 0xff (waarde 255) 0x100(waarde 256) Ook het 8-tallige (octale) stelsel kun je gebruiken. Je begint het getal dan met een 0, waarachter alleen de cijfers 0 tot en met 7 mogen volgen. Het octale stelsel is een beetje in onbruik geraakt,

60

Objecten en methoden

en de enige reden om dit feit te weten is eigenlijk om niet per ongeluk een getal met een 0 te laten beginnen: het zou als octaal getal worden beschouwd! Voorbeelden: 010 (waarde 8) 0377 (waarde 255) 0400 (waarde 256) Constanten zijn van type double zodra er een decimale punt in voorkomt. Een nul voor de punt mag je weglaten (maar waarom zou je?). Voorbeelden van double-waarden zijn:
0.0 123.45 -7.0 .001

Voor hele grote, of hele kleine getallen kun je de wetenschappelijke notatie gebruiken, bekend van de rekenmachine: 1.2345E3 betekent: 1.2345 103 , oftewel 1234.5 6.0221418E23 het aantal atomen in een gram waterstof: 6.022 1023 3E-11 de straal van een waterstofatoom: 3 1011 meter Net als op een rekenmachine worden hele grote getallen niet meer exact opgeslagen. Er zijn circa 15 signicante cijfers beschikbaar. Behalve gewone getallen zijn er speciale waarden voor plus en min oneindig, en een waarde genaamd NaN (voor Not a Number), die als resultaat gebruikt wordt bij onmogelijke berekeningen. String constanten Letterlijke teksten in een programma zijn constanten van het type String. Ook die hebben we al de nodige keren gebruikt. Je moet de tekst tussen dubbele aanhalingstekens zetten. Daartussen kun je vrijwel alle symbolen die op het toetsenbord zitten neerzetten. Voorbeelden: "hallo" "h o i !" "Grr#$%]&*{" "1234" "" een gewone tekst spaties tellen ook als symbool in een tekst mag alles. . . dit is ook een String, geen int een String met nul symbolen

Alleen een aanhalingsteken in een String zou problemen geven, omdat de compiler dat zou beschouwen als het einde van de string. Daarom moet je, als je toch een aanhalingsteken in een string wilt zetten, daar een backslash-symbool (omgekeerde schuine streep) v o or zetten. Dat roept een nieuw probleem op: hoe zet je het backslash-symbool zelf dan in een string? Antwoord: zet daar een extra backslash-symbool voor, dus verdubbel de backslash. Voorbeelden:
"Ze zei \"meneer\" tegen me!" "gewone slash: / backslash: \\ hekje: # "

Met behulp van de backslash kunnen nog een aantal andere bijzondere tekens worden aangeduid: een regelovergang door \n, een tabulatie door \t en het symbool met Unicode-nummer (hexadecimaal) 12ab door \u12ab. Dat laatste is vooral handig om symbolen die niet op het toetsenbord voorkomen in een string te zetten. Statische nal variabelen in klassen Behalve methoden hebben sommige klassen ook declaraties als member. Vaak zijn die bovendien static, dat wil zeggen dat ze bij de klasse als geheel horen, en niet bij elk object uit die klasse afzonderlijk. Bijvoorbeeld, de waarde van is beschikbaar in de klasse Math. Je kunt de waarde gebruiken met de punt-notatie: Math.PI is een expressie met double als type. Let op de hoofdletters: de gewoonte wil dat numerieke constanten geheel in hoofdletters worden geschreven. Ook in andere klassen zijn numerieke constanten beschikbaar. Bijvoorbeeld, de klasse Font bevat een constante Font.ITALIC, die gebruikt kan worden om een cursief lettertype te maken (meer daarover in sectie 5.5). Constanten hoeven niet altijd numeriek te zijn; het kunnen ook objectverwijzingen zijn. Zo zijn er een aantal veel gebruikte kleuren beschikbaar als constante in de klasse Color. Bijvoorbeeld:

5.5 Toepassing: Intro-scherm

61

Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW en zo nog een aantal meer. Let op de dubbele rol die Color hier speelt: deze constanten zijn zelf Colors, en ze zijn bovendien ondergebracht in de klasse Color. Vergelijk dat met Math.PI: dat is een double, die is ondergebracht in de klasse Math.

5.5

Toepassing: Intro-scherm

Intro-scherm: strings en fonts We gaan de besproken concepten gebruiken in een klein programma. Het programma heet Intro, omdat het applet gebruikt zou kunnen worden om een intro-scherm te maken bij een website, waarop de woorden van een tekst op speelse wijze in verschillende kleuren en lettertypes door elkaar heen verschijnen. Behalve objecten van type Color en Font, gaan we gebruik maken van een object van type Scanner, om een String-object in onderdelen te splitsen. Door het gebruik van de wiskundige functie random is het eect elke keer dat we het programma gebruiken een beetje anders. Zie listing 6 voor de tekst van het programma, en guur 12 voor twee snapshots. Objecten van type Scanner Het programma bestaat uit een methode paint, die zoals altijd automatisch door de browser wordt aangeroepen om het hele plaatje te tekenen. Voor het tekenen van de aparte woorden is er in dit programma een extra methode woord gemaakt, die vanuit de methode paint meermalen wordt aangeroepen. De methode woord heeft een Graphics-object als parameter, omdat die nodig is om u berhaupt te kunnen tekenen. Verder krijgt de methode een String s als parameter die het woord bevat dat getekend moet worden, een kleur c die bij het tekenen gebruikt moet worden, de verticale positie y, en de hoogte h van het lettertype dat gebruikt moet worden. De methode woord wordt vanuit paint negen keer aangeroepen, steeds met andere parameters. Op de plaats van de vierde parameter wordt steeds een ander getal gebruikt, zodat elk woord op een andere verticale positie getekend wordt. Op de plaats van de tweede parameter hadden we als letterlijke strings de respectievelijke woorden kunnen schrijven. In plaats daarvan is echter een meer exibele aanpak gekozen, die het gemakkelijker maakt om later nog eens een andere tekst te kiezen. De tekst die in het programma getoond wordt, wordt eenmalig opgeslagen in de variabele zin. Daarna wordt een zogeheten Scanner-object gemaakt, door met behulp van het speciale keyword new de constructormethode daarvan aan te roepen. Een Scanner-object kent de methode next. Die methode levert het eerstvolgende woord op van de tekst, die als parameter bij de constructie van het Scanner-object werd aangeboden. Een Scanner kan dus zinnen in woorden splitsen, en dat is de reden dat we het object splitser hebben genoemd. Elke keer als je next aanroept, met het object splitser onder handen, krijg je het volgende woord. De splitser onthoudt dus waar je bent gebleven. Daar worden de variabelen, waaruit het splitser-object bestaat (elk object bestaat uit variabelen) blijkbaar voor gebruikt. Het resultaat van aanroep van de methode next is een String. Die kan direct worden meegegeven als tweede parameter van woord, die immers op die positie een String-object verwacht. Een alternatief wordt ook in het programma getoond: twee resultaten van aanroepen van next worden met de + operator aan elkaar geplakt, waarna de variabele woorden naar het resultaat gaat wijzen. Die variabele wordt vervolgens gebruikt als parameter van woord, zodat er twee woorden tegelijk getekend worden. Objecten van type Color Voor de kleur die wordt meegegeven aan de methode woord, gebruikt het programma ook diverse manieren (meer om de mogelijkheden te demonstreren, dan dat dat nou erg nuttig is. . . ) Als eerste wordt door aanroep van de constructor van Color een geheel nieuwe mengkleur gemaakt: veel rood, een beetje groen en helemaal geen blauw levert een oranje-achtige kleur op. Dat nieuwe kleurobject wordt opgeslagen in de variabele kleur, of preciezer gezegd: de objectverwijzing kleur gaat naar het nieuwe object wijzen. Vervolgens wordt dat object als parameter meegegeven aan de methode woord. Daarna wordt door aanroep van Colors methode darken een nieuw kleur-object gemaakt, en met een toekenning gaat kleur naar dat nieuwe object wijzen. De tweede maal dat kleur wordt

blz. 62

62

Objecten en methoden

import import import import import

java.awt.Graphics; java.awt.Color; java.awt.Font; java.applet.Applet; java.util.Scanner;

public class Intro extends Applet {


10

private void woord(Graphics g, String s, Color c, int y, int h) { double plek; plek = 10 + 50 * Math.random(); int x; x = (int) plek; g.setColor(c); g.setFont(new Font("Tahoma", Font.BOLD, h)); g.drawString(s, x, y); } public void paint(Graphics g) { String zin, woorden; Scanner splitser; Color kleur; zin = "Een object is een groepje variabelen die je kunt bewerken met methoden"; splitser = new Scanner(zin); kleur = new Color(255,164,0); this.woord(g, splitser.next(), kleur, 10, 10); kleur = kleur.darker(); this.woord(g, splitser.next(), kleur, 25, 20); kleur = kleur.darker(); woorden = splitser.next() + splitser.next(); this.woord(g, woorden, kleur, 40, 10); this.woord(g, splitser.next(), Color.RED, 55, 10); this.woord(g, splitser.next(), Color.BLUE, 70, 15); woorden = splitser.next() + splitser.next() + splitser.next(); this.woord(g, woorden, new Color(128,128,128), 85, 10); this.woord(g, splitser.next(), Color.BLUE, 100, 15); this.woord(g, splitser.next(), Color.BLUE, 115, 10); this.woord(g, splitser.next(), Color.BLUE, 130, 20); } private static final long serialVersionUID = 1; } Listing 6: Intro/src/Intro.java

15

20

25

30

35

40

45

5.5 Toepassing: Intro-scherm

63

Figuur 12: Twee snapshots van de applet Intro

meegegeven aan woord, ontvangt die methode dus een donkerder oranje kleur-object. De derde aanroep van woord ontvangt een nog donkerder oranje kleur-object. (De oude oranjes zijn inmiddels onbereikbaar geworden, want daar wijst geen enkele variabele meer naar!) In plaats van al dat gedoe kun je natuurlijk ook gewoon een constante, zoals Color.RED of Color.BLUE meegeven als derde parameter van woord. Nog een andere mogelijkheid is om ter plaatse een new Color te maken, en die meteen mee te geven (dus zonder hem eerst op te slaan in een variabele). Objecten van type Font Richten we nu onze aandacht op de body van methode woord. De aanroep van Math.random levert een willekeurige double-waarde tussen 0.0 en 1.0 op. Door die met 50 te vermenigvuldigen, onstaat een getal tussen 0.0 en 50.0. Door daar nog eens 10 bij op te tellen, onstaat een getal tussen 10.0 en 60.0. Die wordt opgeslagen in de variabele plek, welke daarna met de cast-notatie wordt afgerond om in een int-variabele x opgeslagen te kunnen worden. De variabele x wordt gebruikt als x-coordinaat in de aanroep van drawString, en daardoor verschijnen de woorden met een gezellig rommelige kantlijn op het scherm. Let op het verschil tussen de declaratie van de variabele y en de variabele x. De variabele y is een parameter van de methode, want hij wordt gedeclareerd in de header van de methode. De waarde ervan wordt dus bepaald door de waarde op de overeenkomstige plaats in de aanroep: de eerste keer is die 10, de tweede keer 25, de derde keer 40, enzovoort. In de body van woord is dus geen toekenning aan y nodig: parameters krijgen bij aanroep van de methode hun waarde. De variabele x daarentegen wordt in de body van de methode woord gedeclareerd. Het is dus een extra variabele voor intern gebruik binnen de methode, die niet een waarde krijgt door aanroep van de methode. Daarom staat er in de body een toekenningsopdracht aan x, alvorens de waarde van x gebruikt kan worden in de aanroep van drawString. Voordat het woord getekend wordt met drawString, wordt de gewenste kleur gekozen met een aanroep van setColor. Op een dergelijke manier kan het gewenste lettertype worden gekozen met een aanroep van setFont. Waar setColor een Color-object als parameter nodig heeft, heeft setFont een Font-object nodig. Die wordt ter plaatse new gecre eerd. De constructor van Font heeft drie parameters nodig: de naam van het lettertype, de stijl, en de grootte. De stijl is een int die aangeeft of het font bold en/of cursief is. De getallen die die eigenschappen coderen hoef je niet uit je hoofd te kennen, want ze zijn als constante beschikbaar in de klasse Font: Font.PLAIN voor normale letters, Font.BOLD voor vette letters en Font.ITALIC voor cursieve letters. Wil je vette ` en cursieve letters, dan kun je Font.BOLD en Font.ITALIC optellen: het zijn immers getallen! Welke getallen precies gebruikt worden in de codering hoef je niet te weten. Je kunt ze natuurlijk eens afdrukken (je ziet dan de waardes 0, 1 en 2). Toch is het niet slim om de volgende keer dan maar de waarde 2 te gebruiken in plaats van Font.BOLD. Als in een volgende versie van Java de codering anders zou worden (en die vrijheid heeft de auteur van een package!), dan zou je

64

Objecten en methoden

programma zich ineens anders gaan gedragen. Gebruik je netjes de daarvoor bedoelde constanten, dan hoef je het niet eens te merken als de codering zou zijn veranderd. Het is net als in de spionage: hoe minder informatie je weet, des te minder informatie kun je per ongeluk verkeerd gebruiken. . . Nieuwe Color-objecten Zoals uit de voorbeelden hierboven blijkt, heeft de constructor-methode Color drie int-waarden als parameter. De drie waarden moeten tussen 0 en 255 liggen. Ze staan voor de hoeveelheid rood, groen en blauw licht die in de nieuwe kleur aanwezig zijn. Je moet je daarbij voorstellen dat de kleur gemaakt wordt door het licht van drie gekleurde spots te mengen (en dat werkt net andersom dan je zou verwachten als je aan mengen van verf denkt). Een paar voorbeelden van mengkleuren zijn: new new new new new new new new new new new Color( 0, 0, 0) Color(255, 0, 0) Color( 0,255, 0) Color( 0, 0,255) Color(128, 0, 0) Color(255,255, 0) Color(255,128, 0) Color(255,255,255) Color(128,128,128) Color(255,128,128) Color(123, 37, 95) geen licht, dus zwart intens rood intens groen intens blauw tussen rood en zwart, dus donkerrood rood plus groen licht geeft geel tussen rood en geel, dus oranje alle licht aan is wit tussen zwart en wit is grijs tussen rood en wit is ets rood probeer maar. . .

Als je dus niet uit de voeten kunt met de dertien standaardkleuren die in de klasse Color als constante aanwezig zijn (Color.BLUE enzovoorts) dan kun je door het aanroepen van de constructormethode Color elke tint mengen die je wilt. Er zijn in totaal 2563 =ruim 16 miljoen kleuren mogelijk. (In hardware-kringen heet dat true color).

Opgaven
5.1 This a. Wat wordt er in een Java-programma aangeduid door het woord this ? b. In welke context kun je het woord this niet gebruiken, en waarom niet?

65

Hoofdstuk 6

Invloed van buiten


6.1 Applets parametriseren
Methode paint heeft e en parameter De applets die we tot nu toe hebben bekeken doen, elke keer dat ze worden gerund, steeds hetzelfde. Op deze manier hebben ze weinig meerwaarde boven een plaatje, dat je in de html-pagina kunt opnemen met een <img>-tag. Het zou wel leuk zijn als je de applet als het ware van parameters kunt voorzien. Je zou dan dezelfde applet tweemaal kunnen starten, en in de twee gevallen van verschillende parameters voorzien. Je kunt echter niet zomaar de methode paint van extra parameters voorzien. De browser gaat er namelijk van uit dat paint precies e en parameter heeft, namelijk een object van het type Graphics. Als de header van de methode paint niet precies is zoals door de browser wordt verwacht (public, resultaattype void, e en Graphics-parameter) dan herkent de browser de methode niet, en gebeurt er dus helemaal niets. Toch is het mogelijk om informatie door te spelen vanuit de html-le naar de applet. We gaan weer een applet maken die Hallo! op het scherm zet, maar ditmaal moet de tekst van de begroeting toegesneden kunnen worden op de naam van een persoon, dus bijvoorbeeld Hallo, Jeroen!. De te gebruiken naam zal in de html-le worden gespeciceerd, en daarmee is dit een mooi voorbeeld van het doorgeven van parameters aan een applet. Klasse-extensies erven methoden We moeten, om te begrijpen hoe je een applet van parameters kunt voorzien, eerst even kijken naar de betekenis van extends in de klasse-header. Een klasse-header zoals
class Huizen extends Applet

betekent dat er een aantal methoden volgen die een Huizen-object onder handen kunnen nemen. De aanduiding extends Applet betekent dat een Huizen-object bovendien alle methoden kent die in de klasse Applet al zijn gedenieerd. Met extends in de klasse-header kun je dus een bestaande klasse uitbreiden: elk Huizen-object is ook een Applet-object, en wel een heel bijzonder Applet-object dat nog een paar extra kunstjes kent. Methoden in de klasse Applet Het is dus wel de moeite waard om de methoden in de klasse Applet te leren kennen; die kunnen namelijk vanuit iedere methode in een extensie-klasse van Applet worden aangeroepen, gebruik makend van het object this. E en van de methoden uit de klasse Applet is van belang voor het doorgeven van informatie uit de html-le aan een applet: de methode getParameter. Er kunnen meerdere parameters worden doorgegeven aan de applet, en door aanroep van de methode getParameter kun je er steeds e en tegelijk te pakken krijgen. De methode getParameter heeft als (methode-)parameter de naam van de (programma-)parameter die je wilt hebben. Als methode-resultaat levert getParameter dan de waarde van de gevraagde programma-parameter. Zowel de naam als de waarde van programma-parameters zijn teksten. Teksten spelen een belangrijke rol in veel Java-programmas, en er is dan ook een bibliotheek-klasse waarin vele methoden zijn ondergebracht die iets met tekst te maken hebben. Deze klasse heet String; een String-object stelt dus een tekst voor.

66

Invloed van buiten

import java.awt.Graphics; import java.applet.Applet; public class Rechthoek extends Applet { public void paint(Graphics g) { String lengteTekst, breedteTekst; int lengte, breedte, omtrek, oppervlakte; double diagonaal; lengteTekst = this.getParameter("lengte"); breedteTekst = this.getParameter("breedte");
15

10

lengte = Integer.parseInt(lengteTekst); breedte = Integer.parseInt(breedteTekst); omtrek = 2*(lengte+breedte); oppervlakte = lengte*breedte; diagonaal = Math.sqrt(lengte*lengte + breedte*breedte); g.drawString("RECHTHOEK" , 20, 20); g.drawString("Afmetingen " + lengte + " bij " + breedte, 20, 40); g.drawString("Omtrek is " + omtrek , 20, 60); g.drawString("Oppervlakte is " + oppervlakte , 20, 80); g.drawString("Diagonaal is " + diagonaal , 20, 100); } private static final long serialVersionUID = 1; } Listing 7: Rechthoek/src/Rechthoek.java

20

25

10

15

<html> <head><title>Rechthoek</title></head> <body><h2>Rechthoek</h2><hr> Hier is een applet dat de eigenschappen van een rechthoek uitrekent: <br> <applet codebase="bin" code="Rechthoek.class" width="200" height="120"> <param name="lengte" value="12"> <param name="breedte" value="8"> </applet> <hr> Hier is het applet nog eens, maar nu voor een andere rechthoek: <br> <applet codebase="bin" code="Rechthoek.class" width="200" height="120"> <param name="lengte" value="7"> <param name="breedte" value="3"> </applet> </body> </html> Listing 8: Rechthoek/Rechthoek.html

6.2 Utility-klassen

67

Figuur 13: Twee versies van het Rechthoek-applet in e e n html-le

Zowel parameter als resultaat van de methode getParameter zijn dus een String. De String die als resultaat wordt opgeleverd, kunnen we tijdelijk opslaan in een variabele, die daarvoor gedeclareerd moet worden met String als object-type. Deze declaratie en de daaropvolgende aanroep kunnen er bijvoorbeeld als volgt uitzien:
String persoon; persoon = this.getParameter("voornaam");

Nu moeten we nog zorgen dat in de html-le inderdaad een programma-parameter wordt aangeboden met die naam. Voor dit doel bestaat er een speciale html-tag <param>, die tussen de <applet>-tag en de </applet>-tag moet staan, als volgt:
<applet code="Groet.class" width="200" heigth="50"> <param name="voornaam" value="Jeroen"> </applet>

De naam die we in de html-le voor de programma-parameter kiezen (in dit voorbeeld: voornaam), moet in het Java-programma als String worden meegegeven aan de methode getParameter, dus met aanhalingstekens: "voornaam". De waarde die in de html-le achter value= staat gespeciceerd (in het voorbeeld: Jeroen) is in het Java-programma beschikbaar als methode-resultaat van getParameter. De situatie is een beetje verwarrend doordat er twee soorten parameters een rol spelen: methodeparameter en programma-parameters. Let speciaal op het feit dat de naam van de programmaparameter (in het voorbeeld: voornaam) geen Java-variabele of -parameter is, en dus in het programma ook niet gedeclareerd is. De naam van een programma-parameter wordt in het Java-programma als tekst behandeld, vandaar de aanhalingstekens in "voornaam" in het Javaprogramma.

6.2

Utility-klassen

Rekenen aan rechthoeken Als een iets uitgebreider voorbeeld van programma-parameters bekijken we een ander applet (zie guur 13). Het programma (zie listing 7) heet Rechthoek, en berekent een paar eigenschappen van een rechthoek (omtrek, oppervlakte en lengte van de diagonaal). De afmetingen van de rechthoek worden als programma-parameter aan de applet meegegeven (zie listing 8). Dit programma demonstreert bovendien het gebruik van een paar belangrijke bibliotheek-klassen.

blz. 66 blz. 66

68

Invloed van buiten

De klasse Integer Strings en int-waarden zijn van een verschillend type. Er is een wezenlijk verschil tussen het getal 37 (een int-waarde) en de tekst "37" (een String-waarde, die toevallig alleen maar cijfertekens bevat). Het verschil merk je als je ermee probeert te rekenen: 37+1 is 38, maar "37"+"1" is "371". In het voorbeeldprogramma krijgen we de lengte en breedte van de rechthoek als programmaparameter binnen, en dus in de vorm van een String-object. Alvorens we met deze waarden kunnen gaan rekenen (de oppervlakte, bijvoorbeeld, is de lengte maal de breedte), zullen we de Stringobjecten dus moeten converteren naar int-waarden. Gelukkig is er een bibliotheek-methode die deze conversie uitvoert. De methode heet parseInt (to parse betekent ontleden), en is te vinden in de klasse Integer. Die bevindt zich in de package java.lang, maar deze klasse hoeft niet apart ge mporteerd te worden, omdat alle klassen in java.lang automatisch zijn ge mporteerd. Ook de mporteerd. klasse String bevindt zich in deze package, en hoeft dus niet te worden ge De methode parseInt uit de klasse Integer is een static methode. Hiervan zou een aanroep kunnen zijn:
String lengteAlsTekst; int lengteAlsGetal; lengteAlsTekst = this.getParameter("lengte"); lengteAlsGetal = Integer.parseInt(lengteAlsTekst);

De klasse Math Voor het zwaardere rekenwerk is er een klasse Math, vol met methoden die allerlei wiskundige functies berekenen. In deze klasse zijn onder andere de volgende methoden te vinden: abs absolute waarde, d.w.z. laat het eventuele minteken weg sqrt wortel (square root) sin sinus exp e-tot-de-macht pow machtsverheen, met als parameters grondtal en exponent log logaritme met grondtal e al deze methoden zijn ook static, en hebben dus niet betrekking op een bepaald object. Rekenen aan rechthoeken Het rechthoeken-programma spreekt nu verder voor zich. Als eerste worden de programmaparameters lengte en breedte opgehaald door aanroep van getParameter. Omdat deze methode de waardes oplevert in String-vorm, moeten deze met parseInt eerst worden omgezet naar de overeenkomstige getalwaarde. Met die getallen kunnen vervolgens de berekeningen worden uitgevoerd. De sqrt-methode wordt daarbij gebruikt om, conform de stelling van Pythagoras, de diagonaal van de rechthoek te berekenen. De resultaten kunnen tenslotte met de methode drawString op het scherm worden getekend. De strings die getekend moeten worden, worden opgebouwd met de operator + , die hierbij de betekenis strings-samenplakken heeft. Omdat als e en van de twee operanden van + een int-waarde is, worden er strings samengeplakt, zoals in de expressie
"Afmetingen " + lengte

Het getal wordt zo eerst automatisch geconverteerd naar een String-waarde.

6.3

Interactie via objecten

Een kleuren-mixer In de vorige sectie voorzagen we het programma vanuit de html-le van parameters, zodat het gedrag van het programma van buiten af was te be nvloeden. Eenmaal gestarte programmas gingen echter gewoon hun gang; de gebruiker van het programma kon niets anders doen dan toekijken. In de meeste programmas wordt de gebruiker echter in staat gesteld om actief in te grijpen in het programmaverloop. Daartoe staan er allerlei interactie-componenten op het scherm, zoals buttons, schuifregelaars, invulbare teksten, menus, enzovoorts. In dit hoofdstuk bekijken we een programma waarin de gebruiker met drie schuifregelaars de kleur van een vlak kan bepalen: het is dus een kleurenmixer. Met een druk op een speciale button op

6.3 Interactie via objecten

69

Figuur 14: De Mixer-applet in werking

het scherm kan de gebruiker bovendien de drie schuifregelaars in e en klap weer in de stand zwart zetten. Het programma is niet zo lang, maar er komen veel principes aan de orde. De technieken die in dit programma worden toegepast, zijn nodig in elk programma dat interactie met een gebruiker pleegt. Het complete programma staat in listing 9. Alle interactie-componenten (de drie schuifregelaars en de button) worden in het programma gemodelleerd als aparte objecten. In de Java-bibliotheek zitten aparte klassen voor alle standaard interactie-componenten, dus met die klassen en de methoden daarvan krijgen we in ieder geval te maken. Her-denitie van de methode paint Er zijn in de klasse Applet een aantal methoden die van belang zijn voor interactie met de gebruiker. Om te beginnen is er in Applet een methode paint, met een Graphics-object als parameter. In de klasse Applet is deze methode weliswaar gedenieerd, maar staat er geen enkele opdracht in de body. Als de methode wordt aangeroepen gebeurt er dus helemaal niets. Als je een applet wilt maken dat w el wat tekent, dan moet je een extensie maken van de klasse eren. Dat hebben we in alle programmas in de Applet, en daarin de methode opnieuw deni eerdere hoofdstukken dan ook gedaan. Bedenk dat je in zon uitgebreiding van een klasse nieuwe methoden kunt toevoegen (zoals tekenHuis in de klasse Huizen), maar dat je ook alle methoden erft van de oorspronkelijke klasse (zie sectie 6.1). Je kunt in een extensie van een klasse ook een methode die je eigenlijk al had ge erfd opnieuw deni eren. Bij aanroep van de methoden worden dan de opdrachten in de body van deze herdenitie uitgevoerd. Herdenitie van de methode paint in een extensie-klasse van Applet is een slimme truc. De browser roept immers in goed vertrouwen de methode paint aan, en als die is hergedenieerd hebben we hem dus mooi in de val laten lopen om de macht over te dragen aan ons programma. . . Indirecte aanroep van paint via repaint De methode paint (de herdenitie, of als die er niet is, de lege standaard-versie die in Applet al bestond) wordt door de browser automatisch aangeroepen op elk moment dat het scherm getekend moet worden. Dat is in ieder geval aan het begin, maar bijvoorbeeld ook als het window uit beeld is geweest en nu weer in zicht raakt. Het komt regelmatig voor dat we vanuit het programma er voor willen zorgen dat het scherm opnieuw getekend wordt. Bijvoorbeeld, als de gebruiker een van de interactie-componenten heeft gebruikt, en als reactie daarop de afbeelding op het scherm veranderd moet worden. Je zou in die situatie de methode paint wel zelf willen aanroepen. Maar dat kan niet, want paint heeft een Graphics-object als parameter nodig, en waar haal je die zo ineens vandaan? Gelukkig is het toch mogelijk om vanuit het programma er voor te zorgen dat paint nog een keer extra wordt aangeroepen. Je kunt dat doen door aanroep van de methode repaint, die in Applet beschikbaar is. Deze methode tovert ergens een Graphics-object vandaan (vraag niet hoe dat kan, maar proteer ervan) en roept vervolgens de methode paint aan met dat Graphicsobject als parameter. Als we de paint-methode hadden hergedenieerd, dan roept repaint de hergedenieerde versie aan, en dat is precies het eect dat we willen bereiken.

blz. 71

70

Invloed van buiten

Her-denitie van de methode init Naast paint is er nog een Applet-methode die wordt aangeroepen door de browser. Dit is de methode init: een void-methode zonder parameters. Net als paint heeft init bij de denitie in Applet een lege body. De methode is dan ook, net als paint, bedoeld om hergedenieerd te worden in een extensie-klasse van Applet. Op die manier kunnen we n og een valkuil graven waar de browser op een goed moment intrapt. De methode init wordt door de browser eenmalig aangeroepen op het moment dat de applet voor het eerst in beeld wordt gebracht, dus nog v o or de eerste aanroep van paint. Dat maakt de methode init de ideale plaats om opdrachten neer te zetten die niet elke keer dat het window aan het licht komt opnieuw uitgevoerd moeten worden, maar die slechts eenmalig nodig zijn. Het cre eren van nieuwe objecten voor de interactie-componenten (door aanroep van new Button en dergelijke) is iets wat typisch in de methode init thuishoort. Aanroep van de methode add In een programma waarin een button nodig is, kan de methode init als volgt hergedenieerd worden:
public void init( ) { Button b; b = new Button("druk hier");

Maar dat is nog niet genoeg. De nieuwe button is nu wel ergens in het geheugen als object aanwezig (en bereikbaar via de object-referentie b), maar dat wil nog niet zeggen dat de button al direct op het scherm zichtbaar is. Om dat voor elkaar te krijgen, moet je ook nog de methode add aanroepen. Dat is een methode uit de klasse Applet, en omdat init dat ook is, kan add vanuit init worden aangeroepen met this als object dat onder handen wordt genomen. Als parameter krijgt add de interactie-component die zichtbaar gemaakt moet worden:
this.add( b ); }

Interactie-componenten die aldus aan de applet zijn toegevoegd worden automatisch in beeld gebracht; je hoeft ze dus niet zelf in paint te tekenen.

6.4

Interactie-componenten

De klasse Button In de package java.awt (abstracte window toolkit) zitten een aantal klassen die interactiecomponenten beschrijven. De klasse Button is een beschrijving van drukknoppen die op het scherm worden afgebeeld. Het programma kan zodanig ingericht worden dat er bepaalde opdrachten worden uitgevoerd als de gebruiker op de button drukt. Op deze manier kan het programma dus reageren op de acties van de gebruiker. Zoals in de vorige sectie werd beschreven, kun je door aanroep van de constructor-methode, die ook Button heet, nieuwe objecten maken voor elke drukknop die je in het programma wilt gebruiken. Twee methoden uit de klasse Button zijn met name van belang: Button: de constructormethode setLabel: om de tekst op de button te veranderen Beide methoden hebben een string als parameter, die op de button getoond wordt zodra die met add aan de applet wordt toegevoegd. De klasse Scrollbar Een tweede interactie-component die we in het voorbeeld-programma in dit hoofdstuk zullen gebruiken is Scrollbar. Scrollbars worden vaak langs de randen van windows gebruikt om de ligging van het window te verschuiven, maar je kunt een scrollbar ook los gebruiken. Het is dan een soort schuifregelaar, waarmee de gebruiker een waarde kan instellen. Drie methoden uit de klasse Scrollbar zijn met name van belang:

6.4 Interactie-componenten

71

import java.awt.*; import java.awt.event.*; import java.applet.Applet;


5

public class Mixer extends Applet implements AdjustmentListener, ActionListener { private Scrollbar rood, groen, blauw; private Button zwart; public void init() { rood = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, 255); groen = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, 255); blauw = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, 255); zwart = new Button("Zwart"); this.add(rood); this.add(groen); this.add(blauw); this.add(zwart); rood .addAdjustmentListener(this); groen.addAdjustmentListener(this); blauw.addAdjustmentListener(this); zwart.addActionListener(this); } public void paint(Graphics g) { int rw, gw, bw; rw = rood .getValue(); gw = groen.getValue(); bw = blauw.getValue(); g.drawString("R="+rw+" G="+gw+" B="+bw, 20, 40 ); g.setColor(new Color(rw, gw, bw)); g.fillRect(20, 60, 260, 220); } public void adjustmentValueChanged(AdjustmentEvent e) { this.repaint(); } public void actionPerformed(ActionEvent e) { rood .setValue(0); groen.setValue(0); blauw.setValue(0); this.repaint(); } private static final long serialVersionUID = 1; } Listing 9: Mixer/src/Mixer.java

10

15

20

25

30

35

40

45

50

72

Invloed van buiten Scrollbar: de constructormethode, met maar liefst vijf int-parameters: De ligging van de scrollbar: horizontaal of verticaal. Je kunt de gewenste ligging aangeven door voor deze parameter e en van de twee constanten te gebruiken die voor dit doel in de klasse Scrollbar aanwezig zijn: Scrollbar.HORIZONTAL of Scrollbar.VERTICAL. De beginwaarde van de schuifregelaar De afstand die de schuifregelaar verschuift als de gebruiker naast het schuivertje klikt De minimale waarde (schuivertje helemaal naar links) De maximale waarde (schuivertje helemaal naar rechts) setValue: een methode om de waarde van de schuifregelaar, en dus de positite van het schuivertje, te veranderen. De methode heeft e en parameter van type int, waarmee de gewenste waarde kan worden aangegeven. Die waarde moet natuurlijk tussen de minimale en maximale waarde liggen zoals die bij de constructormethode werd gespeciceerd. getValue: een methode om de huidige waarde van de schuifregelaar op te vragen. De methode heeft geen parameters, maar wel een int als resultaatwaarde.

Alles importeren Programmas gebruiken vaak vrij veel klassen uit de awt-bibliotheek. Die moeten in principe allemaal ge mporteerd worden:
import java.awt.Button; import java.awt.Scrollbar;

enzovoorts. Dat kan in de praktijk een lange opsomming worden. Daarom is het toegestaan om in e en klap alle klassen uit een library te importeren:
import java.awt.*;

Je krijgt dan ook de klassen die je niet nodig hebt, maar daar heb je over het algemeen geen last van. Constructie en gebruik van Scollbar-objecten In het voorbeeldprogramma gaan we drie nieuwe scrollbar-objecten maken, waarmee de gebruiker de hoeveelheid rood, groen en blauw licht kan aanmaken. We declareren daarom drie variabelen van het juiste object-type:
Scrollbar rood, groen, blauw;

In de methode init worden deze variabelen gebruikt om de verwijzing naar drie nieuw gecre eerde Scollbar-objecten te bewaren:
rood = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, 255); groen = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, 255); blauw = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, 0, 255);

Door drie aanroepen van add worden de objecten zichtbaar gemaakt in de applet:
this.add(rood); this.add(groen); this.add(blauw);

In de methode paint gaan we een gekleurd vlak tekenen met een kleur zoals die door de drie schuifregelaars wordt aangegeven. Om te weten op welke positie de schuivertjes zich bevinden, kunnen we de methode getValue aanroepen:
int rw, bw, gw; rw = rood .getValue(); gw = groen.getValue(); bw = blauw.getValue();

Met de drie verkregen int-waarden (rood-waarde, groen-waarde en blauw-waarde) maken we een nieuw Color-object aan, om de kleur van het daarna te tekenen vlak te bepalen:
Color c; c = new Color(rw, gw, bw); gr.setColor(c); gr.fillRect(0,0,100,100);

6.5 Interactie met de gebruiker

73

Object-variabelen Er is echter een probleem met de hierboven geschetste aanpak: waar zetten we de declaraties van de drie Scrollbar-objecten rood, groen en blauw? Deze variabelen zijn nodig in de methode init (in de toekenningsopdrachten en als parameter bij de aanroepen van add). Maar ze zijn ook nodig in de methode paint (om de huidige waarde van de schuivertjes op te vragen). Als we de variabelen declareren in e en van beide methoden, zal de compiler klagen dat er in de andere methode variabelen worden gebruikt die niet zijn gedeclareerd: methoden mogen immers alleen maar hun eigen variabelen (en parameters) gebruiken. Maar ook het declareren van de variabelen in beide methoden biedt geen uitweg: variabelen zijn lokaal geldig binnen een methode, een als ze toevallig dezelfde naam hebben als de lokale variabelen van een andere methode, blijft het toch om verschillende variabelen gaan. De oplossing van het probleem is om de variabelen dan maar in geen van beide methoden te declareren. Variabelen kunnen namelijk ook buiten de methoden worden gedeclareerd, dus als losse members in de klasse. Dat soort variabelen maken permanent deel uit van het object: sterker nog, ze zijn het object. Ze worden de membervariabelen genoemd. De opzet van het programma wordt dus als volgt:
Class Mixer extends Applet { Scrollbar rood, groen, blauw; // membervariabelen public void init() { rood = new Scrollbar(iets ,iets ,iets ,iets ,iets ); this.add(rood); enzovoort } public void paint(Graphics gr) { int rw; rw = rood.getValue(); enzovoort } }

Object-variabelen kunnen door alle methoden worden bekeken en veranderd. Dat is iets wat we tot nu toe een beetje vaag genoemd hebben: het object wordt door de methoden onder handen genomen. Uitzondering hierop zijn de statische methoden; die hebben geen object onder handen, of meer precies: ze mogen de membervariabelen niet gebruiken. Elk object heeft zijn eigen setje membervariabelen. Die worden gemaakt op het moment dat het eerd wordt, en blijven net zo lang bestaan als ze nog bereikbaar zijn via variabelen. object gecre Het is een goede gewoonte om het aantal membervariabelen zoveel mogelijk te beperken. Als dat mogelijk is, kunnen variabelen beter lokaal in een methode gedeclareerd worden. Op die manier weet je zeker dat methoden niet met elkaars variabelen gaan rommelen.

6.5

Interactie met de gebruiker

Event: actie van de gebruiker Met de tot dusver besproken opdrachten in init en paint staat het programma helemaal klaar voor de gebruiker, alleen reageert het programma nog niet als de gebruiker een schuifregelaar bedient of op een button drukt. Zon handeling van de gebruiker (of een ander van buiten komende actie) heet een event. We moeten het programma nu zo inrichten dat het aan event-handling (afhandelen van gebruikers-acties) gaat doen. Event-listener: object dat een seintje krijgt De gebruiker genereert events door een interactie-component te gebruiken. Om daarop te kunnen reageren, moet je in het programma een event-listener koppelen aan de interactie-component. Een event-listener is een object dat een seintje krijgt als er wat met de interactie-component gebeurt. Het koppelen van een event-listener aan een interactie-component gebeurt door aanroep van een methode van de betreende component. De naam van die methode verschilt per component-type: in de klasse Button zit een methode addActionListener in de klasse Scrollbar zit een methode addAdjustmentListener

74

Invloed van buiten

Deze methoden krijgen als parameter de event-listener die op de hoogte gesteld moet worden van het feit dat de gebruiker op de button drukt, respectievelijk de schuifregelaar verplaatst. De methode init komt er dus als volgt uit te zien (er van uitgaande dat er bij de membervariabelen een Button zwart en een Scrollbar rood is gedeclareerd):
public void init() { zwart = new Button("terug naar zwart"); rood = new Scrollbar(iets ,iets ,iets ,iets ,iets ); this.add(zwart); this.add(rood); zwart.addActionListener(iets ); rood .addAdjustmentListener(iets ); }

De vraag is nu alleen nog welk object gaat dienen als event-listener, en dus als parameter moet worden meegegeven aan deze twee methoden. Event-listeners krijgen seintje via methode-aanroep Interactie-componenten, zoals buttons en scrollbars, geven de event-listener die er aan gekoppeld is een seintje zodra de gebruiker daar aanleiding toe geeft. Dat seintje wordt gegeven door een speciale methode van de event-listener aan te roepen: Een button roept de methode actionPerformed aan van zijn action-listener Een scrollbar roept de methode adjustmentValueChanged aan van zijn adjustmentlistener. Ze moeten er dan wel op kunnen vertrouwen dat die event-listener inderdaad de bewuste methode heeft. Hoe kunnen we dat garanderen? Methodes beloven met implements Groepjes methoden kun je bundelen. Dat is niets nieuws: zon bundel methoden heet een klasse. Wel nieuw is dat je ook groepjes methode-headers kunt bundelen. Dat staat bekend als een interface. Dit begrip is nodig om precies te omschrijven wat een action-listener nu eigenlijk is. Dat gebeurt in de Java-bibliotheek:
interface ActionListener { public void actionPerformed(ActionEvent e); }

oftewel: een ActionListener moet de methode actionPerformed kennen zoals beschreven in de interface-denitie. In de header van een klasse kun je nu beloven dat deze klasse inderdaad zon methode zal gaan deni eren. Dat gebeurt door in de header de tekst implements ActionListener op te nemen. Dit is een gedeelte van de klasse-header die we in hoofdstuk 2 nog niet genoemd hadden, maar die blijkt uit het volledige syntax-diagram van klasse-header:
klasse

header
implements class

naam

naam
extends

naam

In het voorbeeld-programma gaan we dat doen met de klasse die we toch al aan het deni eren zijn. (Het wordt een lange header, want daar stond ook al extends Applet, maar dat is helemaal niet erg; desnoods splits je hem in twee regels):
public class Mixer extends Applet implements ActionListener {

Met deze header doen we de belofte dat in de klasse Mixer alle methoden uit de interface ActionListener zullen worden ge mplementeerd. Dat is er trouwens maar e entje, namelijk de methode actionPerformed.

6.5 Interactie met de gebruiker

75

De implements-belofte nakomen De gedane belofte moet natuurlijk wel worden nagekomen. In de body van de klasse Mixer deeren we daarom inderdaad de beloofde methode: ni
public void actionPerformed(ActionEvent e) {

Deze methode wordt aangeroepen als de gebruiker op de button drukt. Hoe moeten we daar ook alweer op reageren? O ja, de schuifregelaars in de uitgangspositie zetten:
rood .setValue(0); groen.setValue(0); blauw.setValue(0);

De nieuwe situatie moet ook nog tot uiting komen op het scherm, en dat doen we door een aanroep van paint te forceren. We kunnen paint niet zelf aanroepen, maar wel indirect door een aanroep van repaint (zie sectie 6.3). Daarmee is de afhandeling van het indrukken van de button dan voltooid.
this.repaint(); }

Proteren van de implements-belofte Met het deni eren van de methode actionPerformed is de klasse Mixer de belofte om ActionListener te implementeren, nagekomen. Dit betekent dat elk Mixer-object mag dienen als action-listener. Nu hadden we net een action-listener nodig in de methode init, als parameter van addActionListener:
public void init() { enzovoort zwart.addActionListener(iets ); }

De methode init is zelf een methode in de klasse Mixer, en dat betekent dat init een Mixer-object onder handen heeft. Dat object kan met het woord this worden aangeduid. In deze context is this een Mixer-object, en daarom mag this dus dienen als action-listener. Daarmee is de puzzel compleet: de creatie van de button, het tonen op het scherm en het koppelen van een action-listener daar aan kan als volgt gebeuren:
public void init() { enzovoort zwart = new Button("terug naar zwart"); this.add(zwart); zwart.addActionListener(this); }

Reageren op een Scrollbar Het reageren op events die de gebruiker met een van de drie scrollbars laat gebeuren, verloopt langs dezelfde lijnen als het afhandelen van het indrukken van de button: We koppelen in de methode init het Mixer-object this als event-listener aan de scrollbars:
rood .addAdjustmentListener(this); groen.addAdjustmentListener(this); blauw.addAdjustmentListener(this);

Dat mag alleen maar als Mixer-objecten zich als echte adjustment-listener kunnen gedragen, dus dat beloven we in de header van de klasse Mixer:
public class Mixer extends Applet implements ActionListener, AdjustmentListener

Om die belofte na te komen deni eren we de in de interface AdjustmentListener gevraagde methode, te weten adjustmentValueChanged:
public void adjustmentValueChanged (AdjustmentEvent e) { this.repaint(); }

76

Invloed van buiten Het afhandelen van het schuiven aan de scrollbar bestaat alleen maar uit het aanroepen van repaint. Die roept op zijn beurt paint aan, en daarin wordt met getValue de huidige waarde van de scrollbars opgevraagd en gebruikt.

De term interface Het begrip interface wordt op nogal wat plaatsen in de informatica gebruikt. Op het eerste gezicht in totaal verschillende betekenissen: In hardware-kringen: de stekkertjes aan de achterkant van de computer vormen samen de interface. Zo kan er een printer worden aangesloten op de parallelle interface, en een muis ele interface. De interface is als het ware het gezicht dat de computer aan de op de seri randapparatuur vertoont. In ontwerpers-kringen: de windows, knopjes, dialogen, menus, kortom: de interactiecomponenten waarmee de gebruiker met het programma communiceert vormen samen de grasche user interface, ook wel GUI (spreek uit: koewie) genoemd. De interface is als het ware het gezicht dat het programma aan de gebruiker vertoont. In kringen van Java-programmeurs: de methoden die van een bepaald object aangeroepen kunnen worden vormen samen de interface, ook wel API genoemd (application programmers interface). De interface is als het ware het gezicht dat de klasse aan de programmeur die de klasse gebruikt, vertoont. Omdat er in alle gevallen sprake is van een gezicht dat [de computer / het programma / de klasse] vertoont aan de buitenwereld, is het bij nader inzien niet verwonderlijk dat daar dezelfde term voor wordt gebruikt: interface. Wel even goed uitkijken in welke kringen je je beweegt.

Opgaven
6.1 Smiley exibel Pas het smiley-programma uit opgave 3.4 aan, zodat de gebruiker de afmeting van de smiley met een schuifregelaar kan instellen. Maak een tweede schuifregelaar voor de grootte van de ogen, en eventueel een derde voor de vrolijkheid van de mond. 6.2 Eyes Om op een button re reageren, maak je een methode actionPerformed. Daarmee heb je voldaan aan de belofte om een ActionListener te implementeren. Je kunt dan in init de methode addActionListener aanroepen, met de button als object voor de punt. Als parameter geef je daarbij this mee, die immers een geldige ActionListener is, omdat hij een methode actionPerformed kent. Het reageren op beweging van de muis gebeurt op een soortgelijke manier: in plaats van de methode actionPerformed schrijf je de methoden mouseMoved en mouseDragged (zie het overzicht in de bijlage) je hebt daarmee niet een ActionListener ge mplementeerd, maar een MouseMotionListener je kunt dan in init de methode addMouseMotionListener aanroepen, ditmaal met de hele applet als betrokken object. Maak met deze techniek een applet genaamd eyes. De applet tekent twee grote stripguur-ogen: een grote open cirkel met een zwarte cirkel als pupil. De pupillen moeten allebei de bewegingen van de muis volgen: ze kijken naar rechtsonder als de muis rechtsonder staat, enzovoorts. Je kunt de ogen scheel laten kijken door de muis ertussenin te bewegen.

77

Hoofdstuk 7

Herhaling
Dit hoofdstuk is niet een herhaling, maar gaat over herhaling in Java, en is dus nieuw!

7.1

De while-opdracht

Opdrachten herhalen Met behulp van event-listeners kunnen we het programma nu weliswaar laten reageren op de gebruiker, maar nadat de afhandelings-methode is afgelopen, staat het programma toch weer stil (totdat de gebruiker een nieuw event genereert). ` Om het programma gedurende langere tijd bezig te houden, zijn erg veel opdrachten nodig. Of: we moeten er voor zorgen dat e en (of een paar) opdrachten steeds opnieuw worden uitgevoerd. Dit is mogelijk met een speciale opdracht: de while-opdracht. Een voorbeeld van het gebruik van zon while-opdracht is het volgende programma-fragment:
public void paint(Graphics gr) { int x; x = 1; while (x<1000) x = 2*x; gr.drawString("eindwaarde: " + x, 10, 10); }

In deze methode staan een declaratie, een toekenningsopdracht, dan zon while-opdracht, en tenslotte een aanroep van de methode drawString. De programma-tekst
while (x<1000) x = 2*x;

is dus e en opdracht, bestaande uit een soort header: while (x<1000) en een body: x=x*2; . De header bestaat uit het woord while gevolgd door een voorwaarde tussen haakjes; de body is z elf een opdracht: hier een toekenningsopdracht, maar dat had ook bijvoorbeeld een methode-aanroep kunnen zijn. Bij het uitvoeren van zon while-opdracht wordt de body steeds opnieuw uitgevoerd. Dit blijft net zolang doorgaan als dat de voorwaarde in de header geldig is. Daarom heet het ook een while-opdracht: de body wordt steeds opnieuw uitgevoerd while de voorwaarde geldt. In het voorbeeld krijgt de variabele x aan het begin de waarde 1. Dat is zeker kleiner dan 1000, dus wordt de body uitgevoerd. In die body wordt de waarde van x veranderd in zijn eigen dubbele; de waarde van x wordt daarmee gelijk aan 2. Dat is nog steeds kleiner dan 1000, en dus wordt de body nogmaals uitgevoerd, waardoor x de waarde 4 krijgt. Ook dat is kleiner dan 1000, dus nogmaals wordt de waarde van x verdubbeld tot 8. Dat is kleiner dan 1000, en zo doorgaand krijgt x daarna nog de waarden 16, 32, 64, 128, 256 en 512. Dat is kleiner dan 1000, en dus wordt de body weer opnieuw uitgevoerd, waarmee x de waarde 1024 krijgt. En dat is niet kleiner dan 1000, waarmee de herhaling eindelijk tot een eind komt. Pas dan wordt de volgende opdracht uitgevoerd: de aanroep van drawString. Met die opdracht wordt de waarde die x na al dat verdubbelen heeft gekregen (1024) op het scherm getekend. Groepjes opdrachten herhalen Je kunt ook meerdere opdrachten herhalen met behulp van een while-opdracht. Je zet de opdrachten dan tussen accolades, en maakt het hele bundeltje tot body van de while-opdracht. Het

78

Herhaling

volgende programmafragment bijvoorbeeld, bepaalt hoe vaak je het getal 1 kunt verdubbelen totdat het groter dan 1000 wordt:
int x, n; x = 1; n = 0; while (x<1000) { x = 2*x; n = n+1; } gr.drawString(n + " keer verdubbeld", 10, 10);

We gebruiken hier een variabele n om het aantal verdubbelingen te tellen. Voorafgaand aan de while-opdracht is er nog niets herhaald, en daarom maken we n gelijk aan 0. Elke keer dat de waarde van x in de body verdubbeld wordt, verhogen we ook de waarde van n, zodat die variabele inderdaad de tel bijhoudt. Na aoop van de while-opdracht bevat de variabele n dan het aantal uitgevoerde herhalingen. Twee dingen vallen op aan deze programmafragmenten: De variabelen die in de body gebruikt worden, moeten voorafgaand aan de herhaling een beginwaarde hebben gekregen De voorwaarde die de herhaling controleert moet een variabele gebruiken die in de body wordt veranderd (zo niet, dan is de herhaling ` of direct, ` of helemaal nooit afgelopen). Herhalen met een teller Variabelen die het aantal herhalingen tellen zijn ook heel geschikt om het verdergaan van de herhaling te controleren. Je kunt met zon teller een opdracht bijvoorbeeld precies tien keer laten uitvoeren. Dit fragment (nog steeds een stukje body van paint) tekent 10 smileys onder elkaar op het scherm:
int n; n = 0; while (n<10) { gr.drawString( ":-)", 0, 20*n ); n = n+1; }

Behalve om het aantal herhalingen tellen, komt de teller n hier ook goed van pas om de positie te bepalen waar de n-de smiley moet worden getekend: de y-co ordinaten 0, 20, 40, 60 enzovoorts kunnen eenvoudig uit n worden berekend. Opbouw van een resultaat Bij een while-opdracht wordt er vaak gedurende de herhaling een resultaat opgebouwd. Een voorbeeld hiervan is de volgende methode, die de faculteit berekent van een getal, dat als parameter wordt meegegeven. (De faculteit van een getal is het product van alle getallen tussen 1 en dat getal; bijvoorbeeld: de faculteit van 4 is 1*2*3*4=24.) Behalve een teller gebruikt deze methode een variabele result, waarin het resultaat langzaam wordt opgebouwd:
private static int faculteit(int x) { int n, result; n=1; result=1; while (n<=x) { result = result*n; n = n+1; } return result; }

7.2

Boolean waarden

Vergelijkings-operatoren De voorwaarde in de header van de while-opdracht is een expressie, die na berekening een waarheidswaarde oplevert: ja of nee. De herhaling wordt voortgezet zolang de uitkomst van de berekening ja is.

7.2 Boolean waarden

79

In voorwaarden kun je vergelijkings-operatoren gebruiken. De volgende operatoren zijn beschikbaar: < kleiner dan <= kleiner dan of gelijk aan > groter dan >= groter dan of gelijk aan == gelijk aan != ongelijk aan Deze operatoren kunnen worden gebruikt tussen twee getallen, zowel int-waarden als doublewaarden. Links en rechts van de operator mogen constante getallen staan, variabelen, of complete expressies met optellingen en vermenigvuldigingen en dergelijke. Let er op dat de gelijkheids-test met een dubbel is-teken wordt geschreven. Dit moet, omdat het enkele is-teken al in gebruik is als toekenningsopdracht. Het verschil tussen = en == is erg belangrijk: x=5; x==5 opdracht expressie maak x gelijk aan 5 ! is x op dit moment gelijk aan 5 ?

Logische operatoren Een voorwaarde is wat in de logica een predicaat wordt genoemd. De operatoren die in de logica gebruikt worden om predicaten te verbinden (en, of en niet) kunnen ook in Java gebruikt worden. De mooie symbolen die de logica ervoor gebruikt zitten helaas niet op het toetsenbord, dus we zullen het moeten doen met een ander symbool: && is de logische en || is de logische of ! is de logische niet Het type boolean Expressies waarin de vergelijkingsoperatoren gebruikt worden, of waarin vergelijkingen met logische operatoren worden gekoppeld, hebben evengoed een type als expressies waarin rekenkundige operatoren gebruikt worden. De uitkomst van zon expressie is immers een waarde: e en van de twee waarheidswaarden ja of nee. Logici noemen deze waarden waar en onwaar; de gangbare Engelse benamingen zijn true en false. Behalve gebruiken als voorwaarde in een while-opdracht, kun je allerlei andere dingen doen met logische expressies. Een logische expressie is namelijk net zoiets als een rekenkundige expressie, alleen van een ander type. Je kunt de uitkomst van een logische expressie bijvoorbeeld opslaan in een variabele, of als resultaat laten opleveren van een methode. Het type van logische waarden heet boolean. Dit is een van de primitieve typen van Java, het is dus niet een object-type. Het type is genoemd naar de Engelse logicus George Boole (zie guur 15). Een voorbeeld van een declaratie van en toekenning aan een boolean variabele:
boolean test; test = x>3 && y<5;

Wat nuttiger lijkt een methode met een boolean waarde als resultaat. Bijvoorbeeld een methode die het antwoord oplevert op de vraag of een getal een zevenvoud is:
private static boolean isZevenvoud(int x) { return x%7==0; }

Een getal is een zevenvoud als de rest bij deling door zeven nul is. De uitkomst van de boolean expressie die dat test is het resultaat van de methode. De methode kan vervolgens worden gebruikt als voorwaarde in een while-opdracht, om noem ns wat het eerste 7-voud groter dan 1000 te vinden:
n = 1000; while ( ! this.isZevenvoud(n) ) n = n+1;

Dit voorbeeld maakt duidelijk dat de voorwaarde in een while-opdracht niet altijd een vergelijking hoeft te zijn, maar ook een andere boolean expressie mag zijn; omgekeerd zijn voorwaarden van

80

Herhaling

Figuur 15: George Boole (1815-1864)

while-opdrachten niet de enige plaats waar vergelijkingen een rol spelen: dit kan ook op andere plaatsen waar een boolean expressie nodig is.

7.3

De for-opdracht

Verkorte notatie van teller-ophoging In de bodies van veel while-opdrachten, vooral die waarin een teller wordt gebruikt, komen opdrachten voor waarin een variabele wordt opgehoogd. Dit kan door middel van de opdracht
n = n+1;

(Even terzijde: alleen al vanwege dit soort opdrachten is het onverstandig om de toekenning uit te spreken als is. De waarde van n is namelijk niet gelijk aan n+1, maar de waarde van n wordt gelijk aan de (oude) waarde van n+1). Opdrachten zoals deze komen zo vaak voor, dat er een speciale, verkorte notatie voor bestaat:
n++;

Een adequate uitspraak voor ++ is wordt opgehoogd. Voor ophoging met meer dan 1 is er nog een andere notatie:
n += 2;

betekent hetzelfde als


n = n+2;

Automatisch tellen Veel while-opdrachten gebruiken een tellende variabele, en hebben dus de volgende structuur:
int n; n = beginwaarde ; while (n < eindwaarde ) { doe iets nuttigs gebruikmakend van n n++; }

Omdat zon tellende herhaling zo vaak voorkomt, is er een aparte notatie voor beschikbaar:
int n; for (n=beginwaarde; n<eindwaarde; n++) { doe iets nuttigs gebruikmakend van n }

De betekenis van deze for-opdracht is precies dezelfde als die van de hierboven genoemde whileopdracht. Het voordeel is echter dat de drie dingen die met de teller te maken hebben (de beginwaarde, de eindwaarde en het ophogen) netjes bij elkaar staan in de header. Dat maakt de kans veel kleiner dat je het ophogen van de teller vergeet op te schrijven.

7.3 De for-opdracht

81

In die gevallen waar doe iets nuttigs uit maar e en opdracht bestaat, kun je de accolades ook nog weglaten, wat de notatie nog iets compacter maakt. Syntax van while- en for-opdrachten Al met al zijn er drie soorten opdrachten bijgekomen. In dit syntax-diagram worden die samengevat:

opdracht
methode

object variabele
return while for ( (

. =

naam

( ; ;

expressie
,

expressie expressie

expressie expr
;

opdracht expr
;

expr

opdracht

blok blok
final

declaratie
{ }

opdracht

Opmerkelijk in dit syntax-diagram is dat in de header van een for-opdracht dus driemaal een expressie staat, met twee puntkommas ertussen. Hoe kan dat nou kloppen? De eerste van de drie is toch een toekenning, zoals n=0, en dat is een opdracht en niet een expressie! Ook de derde van de drie, iets als n=n+1 lijkt niet echt een expressie. Toch klopt het syntax-diagram. Technisch gesproken is het wordt-teken namelijk gewoon een operator, net zoals plus en maal dat zijn. Het fragment n=0 (zonder de puntkomma!) is dus een heuse expressie. Met de puntkomma erbij wordt het een opdracht. De syntax van opdracht is dus eigenlijk veel eenvoudiger: er is geen apart spoor nodig voor een toekennings-opdracht, en ook niet voor methode-aanroep-opdracht: dit zijn beide verschijningsvormen van de expressie-meteen-puntkomma-erachter-opdracht. Bij toekenningsopdracht wordt een operator-expressie met de bijzondere operator = gebruikt, en ook de methode-aanroep is een van de mogelijke expressies.

opdracht

return while for ( (

expressie expressie expr


; )

opdracht expr
;

expr

opdracht

blok
In bijlage G hebben we de toekennings-opdracht en de (void-)methode-aanroep-opdracht er in het syntax-diagram van opdracht er toch maar bij laten staan, voor de overzichtelijkheid, maar strikt genomen was dat niet echt nodig. Je kunt je afvragen of een expressie zoals 3+4 met een puntkomma erachter ook als opdracht gebruikt mag worden. In talen als C en C++ is dat inderdaad het geval, maar in Java is, buiten

82

Herhaling

de syntax om, nader bepaald dat dit niet mag: een expressie-met-puntkomma-opdracht, en de eerste en derde expressie in een for-header, mag alleen een operator-expressie met een toekenningsoperator (=, of een variant als += of ++) zijn, of een methode-aanroep (inclusief constructormethoden). Kortom: zon expressie moet een permanent eect (kunnen) hebben. Uit dit laatste syntax-diagram blijkt ook dat zelfs een losse puntkomma als een opdracht beschouwd kan worden. Dat maakt het mogelijk om ongestraft nog wat extra puntkommas neer te zetten (bijvoorbeeld achter de sluit-accolade van een blok, waar dat eigenlijk helemaal niet vereist is). Maar pas op: achter de header van een while-, for- of if-opdracht kun je niet zomaar een puntkomma toevoegen zonder dat de semantiek, soms ingrijpend, verandert!

7.4

Bijzondere herhalingen

Niet-uitgevoerde herhaling Het kan gebeuren dat de voorwaarde in de header van een while-opdracht meteen aan het begin al onwaar is. Dit is het geval in de volgende opdracht:
x=1; y=0; while (x<y) x++;

In deze situatie wordt de body van de while-opdracht helemaal niet uitgevoerd, zelfs niet e en keer. In het voorbeeld blijft x dus gewoon de waarde 1 houden. Oneindige herhaling Een gevaar van while-opdrachten is dat er soms nooit een einde aan komt (qua uitvoeren dan, niet qua programmatekst!). Zon opdracht is gemakkelijk te schrijven. Met
while (1==1) x = x+1;

wordt de waarde van x steeds maar verhoogd. De voorwaarde 1==1 blijft namelijk altijd waar, zodat de opdracht steeds opnieuw uitgevoerd wordt. In dit programma was die oneindige herhaling wellicht de bedoeling, maar vaak slaat een whileopdracht ook op hol als gevolg van een programmeerfout. Bijvoorbeeld in:
x = 1; aantal = 0; while (aantal<10) x = x*2; aantal = aantal+1;

// fout!

Het is de bedoeling dat de waarde van x tienmaal wordt verdubbeld. Helaas heeft de programmeur vergeten om de twee opdrachten van de body tussen accolades te zetten. De bedoeling wordt wel gesuggereerd door de lay-out, maar daar heeft de compiler geen boodschap aan. Daardoor wordt alleen de opdracht x=x*2; herhaald, en op die manier wordt de waarde van aantal natuurlijk nooit groter of gelijk aan 10. Na aoop van de while-opdracht zou de opdracht aantal=aantal+1; e enmaal worden uitgevoerd, maar daaraan komt de computer niet eens meer toe. De bedoeling van de programmeur was natuurlijk:
while (aantal<10) { x = x*2; aantal = aantal+1; }

// goed.

Het zou jammer zijn als je, na een computer met een vergeten accolade in coma gebracht te hebben, het dure apparaat weg zou moeten gooien omdat hij steeds maar bezig blijft met dat ene programma. Gelukkig is er een manier om met geweld de uitvoering van het programma te be eindigen, ook al is het nog niet voltooid. De manier waarop dat gaat verschilt per computersysteem. Als je je programma uittest met appletviewer kun je het window sluiten. Het programma wordt dan direct gestopt, en je kunt de oorzaak van het hangen van het programma gaan zoeken. In het algemeen moet je, als het programma bij het uittesten niets lijkt te doen, de while-opdrachten in je programma nog eens kritisch bekijken. Een beruchte fout is het vergeten van het ophogen van

7.5 Toepassing: renteberekening

83

de tellende variabele, waardoor de bovengrens van de telling nooit wordt bereikt, en de herhaling dus steeds maar doorgaat. Herhaalde herhaling De body van een while-opdracht en van een for-opdracht is zelf o ok weer een opdracht. Dat kan een toekenningsopdracht zijn, of een methode-aanroep, of een met accolades gebouwde samengestelde opdracht. Maar de body kan ook zelf weer een while- of for-opdracht zijn. Bijvoorbeeld:
int x, y; for (y=0; y<10; y++) for (x=0; x<y; x++) gr.drawString("+", 20*x, 20*y);

In dit fragment telt de variabele y van 0 tot 10. Voor elk van die waarden van y wordt de body uitgevoerd, en die bestaat zelf uit een herhaling, gecontroleerd door de teller x. Deze teller heeft als bovengrens de waarde van y. Daardoor zal de binnenste herhaling, naarmate y groter wordt, steeds langer doorgaan. De opdracht die herhaald herhaald wordt, is het tekenen van een plus-symbool op posities evenredig met x en y. Het resultaat is een driehoek-vormig tableau van plus-tekens: + + + + + + + + + + + + + + + + +

+ + + + + + +

+ + + + + +

+ + + + +

+ ++ +++ ++++

Op de bovenste rij in dit tableau staan nul plus-tekens. De waarde van y is op dat moment nog 0, en de eerste keer dat de for-x-opdracht wordt uitgevoerd, betreft het een herhaling die nul keer wordt uitgevoerd. Zon niet-uitgevoerde herhaling past hier prima in de regelmaat van het schema.

7.5

Toepassing: renteberekening
blz. 84

Rente op rente Een aantal besproken idee en komt samen in de applet die te zien is in listing 10 en guur 16 (voor respectievelijk de programmatekst en het runnende programma). Deze applet laat de gebruiker een bedrag en een rentepercentage invoeren, en toont dan de ontwikkeling van het kapitaal (of als je wilt de schuld...) in de komende tien jaren. Door het eect van rente op rente komt er niet elk jaar een vast bedrag bij, maar stijgt het kapitaal/de schuld steeds sterker. De vermeerdering van het kapitaal wordt beschreven door de opdracht
kapitaal *= (1 + 0.01*rente);

De hierin gebruikte operator *= heeft de betekenis wordt vermenigvuldigd met, net zoals += de betekenis heeft wordt vermeerderd met. Deze opdracht is een verkorte schrijfwijze voor
kapitaal = kapitaal * (1 + 0.01*rente);

Bij een rentepercentage van 5 wordt het kapitaal door middel van deze opdracht vermenigvuldigd met 1.05. In een for-opdracht wordt de opdracht elfmaal uitgevoerd, en daaraan voorafgaand wordt steeds het tussenresultaat op het scherm getekend. Invoer via TextFields De gebruiker kan zelf het te gebruiken startbedrag en het rentepercentage invullen. Dat is mogelijk met invoercomponenten van het type TextField. In de methode init worden daarom twee eerd. Omdat deze objecten ook in de methode paint nodig zijn, zijn de TextField-objecten gecre verwijzingen ernaar gedeclareerd als membervariabelen. Parameters van de constructor-methode zijn de tekst die aan het begin in de tekstvelden wordt getoond, en de breedte van het tekstveld.

84

Herhaling

import java.applet.Applet; import java.awt.*; import java.awt.event.*;


5

public class Rente extends Applet implements ActionListener { TextField startTekst, renteTekst; public void init() { startTekst = new TextField("100", 8); renteTekst = new TextField("5", 4); this.add(startTekst); this.add(renteTekst); startTekst.addActionListener(this); renteTekst.addActionListener(this); } public void actionPerformed(ActionEvent e) { this.repaint(); } public void paint(Graphics gr) { int start, rente, jaar; double kapitaal; start = Integer.parseInt( startTekst.getText() ); rente = Integer.parseInt( renteTekst.getText() ); kapitaal = start; for (jaar=0; jaar<=10; jaar++) { gr.drawString( "Na " + jaar + " jaar: " + kapitaal, 10, 50+15*jaar); kapitaal *= (1 + 0.01*rente); } } private static final long serialVersionUID = 1; } Listing 10: Rente/src/Rente.java

10

15

20

25

30

35

Figuur 16: De applet Rente in werking

7.5 Toepassing: renteberekening

85

Net als aan een button kan aan een tekstveld een action-listener worden gekoppeld. Op de manier beschreven in het vorige hoofdstuk gebruiken we het applet als action-listener. De gebruiker genereert events door in het tekstveld op de Enter-toets te drukken. In de methode actionPerformed hebben we gespeciceerd dat op dat moment (via repaint) de methode paint aangeroepen wordt. In die methode pakken we met een aanroep van getText de tekst uit de tekstvelden, en converteren de daarmee verkregen Strings meteen (zonder ze eerst in variabelen op te slaan!) met de methode parseInt tot getallen. Met die getallen gaat de for-opdracht vervolgens aan de slag.

Opgaven
7.1 Muur Schrijf een applet die een bakstenen-muur tekent zoals in de guur hieronder

7.2 Driehoek Schrijf de methode paint van een applet met als output een driehoek met vierkantjes zoals in de guur hierboven. Gebruik daarbij geen while-, maar for-opdrachten. 7.3 Driehoek van Pascal Het aantal manieren waarop je k elementen kunt kiezen uit een verzameling van n elementen staat in de wiskunde bekend als n boven k . Je kunt dit aantal bepalen door n! te delen door k !(n k )!, waarbij n! staat voor de faculteit van n, d.w.z. alle getallen van 1 t/m n vermenigvuldigd. De driehoek van Pascal toont op de n-de rij de waarden van n boven k , voor k tussen 0 en n: 1 1 1 1 1 1 1 2 3 4 5

1 3 6 10

1 4 10

1 5

Schrijf een applet dat de eerste twintig rijen van de driehoek van Pascal toont. 7.4 Wortel berekenen Er is een wiskundige stelling die zegt: als y een benadering is voor de wortel van x, dan is het gemiddelde van y en x/y een betere benadering. Gebruik deze eigenschap om een eigen sqrtmethode te schrijven. Gebruik 1 als eerste benadering, en pas dan net zolang deze eigenschap toe, totdat y 2 nog maar erg weinig afwijkt van x. 7.5 Haakjes uitwerken Als je de haakjes uitwerkt in (1 + x)2 krijg je x2 + 2x + 1, en (1 + x)4 wordt x4 + 4x3 + 6x2 + 4x + 1. In de co eci enten kun je de getallen uit de driehoek van Pascal herkennen. Schrijf een methode met een getal n als parameter, die als methode-resultaat een String oplevert met de uitgewerkte versie van (1 + x)n . Bijvoorbeeld, als n de waarde 4 heeft, is het resultaat "x^4 + 4x^3 + 6x^2 + 4x +1". Hint: de operator += werkt ook op Strings! 7.6 Tafels van vermenigvuldiging Schrijf een applet waarbij de gebruiker in een tekstveld een getal kan invoeren, waarna de tafel van vermenigvuldiging (van 1 t/m 10) van dat getal wordt getoond. 7.7 Uitvoer voorspellen Bepaal de waarde van a na uitvoeren van het volgende programmafragment:

86

Herhaling

a=0; b=1; c=6; for (k=0; k<100; k++) { a += b; b += c; c += 6; }

Hint: voer een paar stappen met de hand uit en probeer de regelmaat te ontdekken. Extrapoleer dan naar het eind van de for-opdracht. 7.8 Boolean methoden Schrijf een methode deelbaar die bepaalt of een getal deelbaar is door een ander getal. Het resultaat moet als boolean waarde worden opgeleverd. Schrijf vervolgens een methode die bepaalt of een getal een priemgetal is (een priemgetal is een getal dat alleen maar deelbaar is door 1 en zichzelf). Gebruik deze methoden in een methode paint die de eerste honderd priemgetallen in 10 rijtjes van 10 op het scherm zet. 7.9 Random tekst Schrijf een applet dat een bepaalde tekst vele malen op het scherm weergeeft. Welke tekst wordt getoond, moet in de html-le kunnen worden gespeciceerd, zodat deze applet op diverse webpaginas kan worden gebruikt. Ook het aantal keren dat de tekst wordt weergegeven, moet in de html-le kunnen worden gespeciceerd. De positie van de teksten is kriskras door elkaar, dat wil zeggen op random posities. Sommige teksten worden klein weergegeven, andere groot: varieer tussen 5- en 30-punts letters. Ook deze keuze moet random geschieden. Als je wilt kun je ook de kleur van de teksten vari eren. Breid nu de applet uit, zodat ook een button met het opschrift shue wordt weergegeven. Elke keer als de gebruiker de button indrukt worden de posities en groottes van de teksten opnieuw bepaald.

87

Hoofdstuk 8

Keuze
8.1 De if-opdracht
Opdrachten voorwaardelijk uitvoeren Opdrachten in een programma worden normaal gesproken de e en na de ander uitgevoerd. Met een while-opdracht kun je er eens een paar herhalen, maar daarna gaat het (als de herhaling tenminste niet oneindig lang doorgaat) onverbiddelijk verder. Niet altijd is dat gewenst: soms moeten opdrachten alleen maar onder bepaalde omstandigheden worden uitgevoerd. Die omstandigheden kunnen afhangen van wat er voorafgaand in het programma is gebeurd, en hangen uiteindelijk af van invoer die de gebruiker heeft verstrekt. Stel bijvoorbeeld dat in het programma de variabele temperatuur een waarde heeft gekregen, bijvoorbeeld doordat de gebruiker dat via een tekstveld heeft ingevoerd. Met een speciale opdrachtvorm kunnen we het programma nu een opmerking over het weer laten maken, maar dan alleen als dat toepasselijk is:
if (temperatuur<0) gr.drawString("Het vriest!", 10, 10);

De opbouw van deze if-opdracht lijkt op die van een while-opdracht: er is een header met een voorwaarde, en een opdracht die de body vormt. De opdracht in de body wordt alleen uitgevoerd als de voorwaarde waar is, anders wordt-ie overgeslagen. Een tweede alternatief achter else Mocht het nodig zijn om in het andere geval, dus als de voorwaarde in de header juist niet waar is, een andere opdracht uit te voeren, dan kun je de if-opdracht uitbreiden met een else-gedeelte. Bijvoorbeeld:
if (temperatuur<0) gr.drawString("Het vriest!", 10, 10); else gr.drawString("Het dooit.", 10, 10);

Let wel, het geheel if+voorwaarde+opdracht+else+opdracht heeft zelf de status van e en opdracht. Het geheel, met of zonder else-gedeelte, kan dus zelf optreden als bijvoorbeeld de body van een for-opdracht, zonder dat er accolades nodig zijn:
for (n=1; n<20; n++) if (n%3==0) gr.drawString(n + " is deelbaar door 3",iets ,iets ); else gr.drawString(n + " is niet deelbaar door 3",iets ,iets );

Groepjes opdrachten voorwaardelijk uitvoeren Als je meerdere opdrachten voorwaardelijk wilt uitvoeren, dan kun je die net als bij de whileopdracht groeperen met accolades. Bijvoorbeeld:
if (temperatuur<0) { gr.drawString("Het vriest,", 10, 10); gr.drawString("Koud he!", 10, 25); }

Ook de opdracht achter else kan een met accolades samengebundelde groep opdrachten zijn.

88

Keuze

Een reeks alternatieven Als er meerdere categorie en van waarden zijn, dan kun je met if-opdrachten uittesten welk geval zich voordoet. De tweede test komt te staan achter de else van de eerste test, zodat de tweede test alleen maar wordt uitgevoerd als de eerste test mislukt is. Een eventuele derde test komt te staan achter de else van de tweede test. Het volgende fragment bijvoorbeeld bepaalt tot welke tarief-categorie iemand met een bepaalde leeftijd behoort. We gaan er van uit dat de variabele leeftijd al een waarde heeft. Het resultaat wordt voor de verandering in een tekstveld neergezet, die als membervariabele tf beschikbaar moet zijn:
if (leeftijd<4) tf.setText("Gratis"); else if (leeftijd<12) tf.setText("Railrunner"); else if (leeftijd<65) tf.setText("Vol tarief"); else tf.setText("Seniorenkaart");

Achter elke else (behalve de laatste) staat opnieuw een if-opdracht. Voor babies wordt de tekst Gratis getoond, en wordt de hele rest overgeslagen (die staat immers achter de else). Bejaarden daarentegen, doorlopen alle tests (kleiner dan 4? kleinder dan 12? kleiner dan 65?) voordat we tot een Seniorenkaart concluderen. In het programma is met inspringen duidelijk aangegeven welke else bij welke if hoort. Bij lange reeksen tests gaat de tekst van het programma dan wel erg naar rechts hangen. Als uitzondering op onze gewoonte om deel-opdrachten achter else naar rechts in te springen, zullen we bij zon herhaalde if-opdracht de lay-out iets simpeler houden:
if (leeftijd<4) tf.setText("Gratis"); else if (leeftijd<12) tf.setText("Railrunner"); else if (leeftijd<65) tf.setText("Vol tarief"); else tf.setText("Seniorenkaart");

Dat is ook wel mooi, want dan zie je alle alternatieven netjes op een rijtje staan. Stop als gevonden De if-opdracht kan natuurlijk ook in een methode staan. Je kunt dan de return-opdracht, waarmee een methode zijn resultaat bekendmaakt, voorwaardelijk maken. Hier is het treintarief-voorbeeld nog eens, maar nu in methode-vorm:
private static String tarief(int leeftijd) { if (leeftijd<4) return "Gratis"; if (leeftijd<12) return "Railrunner"; if (leeftijd<65) return "Vol tarief"; return "Seniorenkaart"; }

Omdat de return-opdracht direct terugkeert naar de aanroeper van de methode, is het hier niet eens nodig om de tweede test achter else te schrijven. Als de eerste test waar is, dan komt de methode dus niet eens toe aan de tweede test. In sectie 4.3 noemden we het nog zinloos om meer dan e en return-opdracht in een methode te schrijven, omdat de tekst achter de eerste return-opdracht unreachable code zou zijn. In dit geval is het echter wel zinvol, omdat de eerste return-opdracht niet altijd wordt uitgevoerd, en de tekst erachter dus onder sommige omstandigheden wel reachable is.

8.2
blz. 89

Toepassingen

Onderscheiden van Buttons In listing 11 en guur 17 staan (tekst en snapshot) van een programma dat een groene cirkel op

8.2 Toepassingen import java.awt.*; import java.awt.event.*; import java.applet.Applet;


5

89

public class Cirkel extends Applet implements ActionListener { Button kleiner, groter; int straal; public void init() { kleiner = new Button("Kleiner"); groter = new Button("Groter"); this.setLayout(new FlowLayout()); this.add(kleiner); this.add(groter); kleiner.addActionListener(this); groter .addActionListener(this); straal = 100; } public void actionPerformed(ActionEvent e) { if (e.getSource()==kleiner && straal>10) straal -= 10; if (e.getSource()==groter && straal<150) straal += 10; this.repaint(); } public void paint(Graphics gr) { gr.setColor(Color.GREEN); gr.fillOval(150-straal, 150-straal, 2*straal, 2*straal); } private static final long serialVersionUID = 1; } Listing 11: Cirkel/src/Cirkel.java

10

15

20

25

30

35

het scherm tekent. Er zijn twee buttons met opschrift kleiner en groter. Met deze knoppen kan de gebruiker de cirkel kleiner en groter maken, mits hij niet onzichtbaar wordt of buiten het window gaat vallen. De opzet van het programma is zoals ieder interactief programma: de methode init cre eert de nodige interactie-componenten, er is een action-listener die een aanroep van paint forceert, en een methode paint die de tekening verzorgt. Variabelen voor de twee buttons zijn als membervariabelen gedeclareerd, omdat deze variabelen zowel in init als in actionPerformed nodig zijn. Ditmaal is er bovendien een int-variabele (straal) als membervariabele gedeclareerd. Deze variabele is in alle drie de methoden nodig: in init krijgt straal zijn beginwaarde in paint wordt straal gebruikt om de grootte van de cirkel te bepalen in actionPerformed wordt straal groter of kleiner gemaakt, afhankelijk van welke button is ingedrukt. Voor dat laatste komt een if-opdracht goed van pas. We gebruiken bovendien iets nieuws: de parameter van actionPerformed. Dat is een object van het type ActionEvent, waarmee allerlei informatie over de gebeurtenis die is opgetreden is opgeslagen. Het belangrijkste is wellicht

90

Keuze

Figuur 17: De applet Cirkel in werking

met welke interactie-component de gebruiker de gebeurtenis heeft veroorzaakt. Dat is aan zon ActionEvent te vragen door aanroep van de methode getSource. Met behulp van twee if-opdrachten wordt getest of het bewuste object de groter- of de kleinerbutton is. Door middel van een tweede voorwaarde, achter de logische and-operator, wordt bovendien gecontroleerd dat de straal niet t e groot, respectievelijk klein wordt. Je ziet dat je met de == operator behalve getallen ook objecten, of liever gezegd objectverwijzingen, kunt testen op gelijkheid. Ook de ongelijkheids-operator != werkt op objecten. Je kunt objecten echter niet ordenen met operatoren zoals < en >=.
blz. 91

Controle van passwords In listing 12 staat de tekst van een programma dat een mooie tekening maakt, maar alleen als de gebruiker eerst het juiste password intikt in een daarvoor bestemd tekstveld. Als membervariabelen hebben we behalve een variabele voor het tekstveld nog twee variabelen. Er is een string waarin de sleutel tot de toegang wordt bewaard, en er is een boolean open, die aangeeft of het slot al is opengemaakt. In de methode init krijgt de sleutel-string zijn waarde (maar die is geheim, dus die verklappen we hier niet), en krijgt de variabele open de waarde false, omdat het slot initieel nog niet is opengemaakt. De methode actionPerformed wordt aangeroepen als de gebruiker op de Enter-toets drukt in het tekstveld, hopelijk nadat hij een password heeft ingetikt. Dat gaan we dus controleren in actionPerformed. Als de gebruiker het password goed heeft geraden, geven we de variabele open de waarde true, om aan te geven dat het slot nu is opengemaakt. In de methode paint, die door een aanroep van repaint aan het werk wordt gezet, controleren we de waarde van de variabele open. Alleen als die true is, wordt de tekening gemaakt. Omdat open een boolean variabele is, kan die direct dienen als voorwaarde in een if-opdracht: die voorwaarde moet immers een boolean expressie zijn. Als de boolean variabele de waarde true heeft, wordt de opdracht in de body uitgevoerd, als hij de waarde false heeft, wordt de opdracht achter else uitgevoerd. Speciale aandacht behoeft het vergelijken van het ingetikte password met de sleutel. Het ingetikte password kunnen we uit het tekstveld halen met de expressie password.getText(), en de sleutel is beschikbaar in de variabele sleutel. Deze twee strings worden echter niet vergeleken met
if ( password.getText() == sleutel ) // fout!

maar met
if (password.getText() .equals(sleutel) ) // goed.

Zouden we de vergelijking uitvoeren met ==, dan testen we of deze twee string-expressies hetzelfde object aanduiden (dezelfde plaats in het geheugen). Dat is hier echter zeker niet het geval: sleutel duidt een constante string aan, en het resultaat van getText is een speciaal door getText nieuw

8.2 Toepassingen

91

import java.awt.*; import java.awt.event.*; import java.applet.Applet;


5

public class Geheim extends Applet implements ActionListener { TextField password; boolean open; String sleutel; public void init() { password = new TextField(20); this.add(password); password.addActionListener(this); password.setEchoChar(*); sleutel = "geheim"; open = false; } public void actionPerformed(ActionEvent e) { if ( password.getText().equals(sleutel) ) { open = true; password.setVisible(false); this.repaint(); } else password.setText(""); } public void paint(Graphics gr) { if (open) { gr.setColor(Color.GREEN); gr.fillOval(50,50,100,100); gr.setColor(Color.BLUE); gr.fillOval(81,85,8,8); gr.fillOval(111,85,8,8); gr.drawArc(75,75,50,50,225,90); } else gr.drawString("please enter password", 50, 50 ); } private static final long serialVersionUID = 1; } Listing 12: Geheim/src/Geheim.java

10

15

20

25

30

35

40

92

Keuze

eerd string-object. Wat we willen weten is of de inhoud van de twee strings hetzelfde is (d.w.z. gecre of de twee teksten toevallig allebei dezelfde letters bevatten). Die test kan worden uitgevoerd met behulp van de methode equals uit de klasse String. Deze methode heeft e en string onder handen, en krijgt een tweede string als parameter. Het resultaat van equals is een boolean waarde. In het programma is handig gebruik gemaakt van het resultaat van de aanroep van getText. We hadden stapsgewijs eerst de ingetikte string kunnen ophalen, die vervolgens met de sleutel kunnen vergelijken, en tenslotte het boolean resultaat in de if-opdracht kunnen schrijven:
String ingetikt; boolean klopt; ingetikt = password.getText(); klopt = ingetikt.equals(sleutel); if (klopt)

// dit is... // wel erg... // omslachtig!

Maar al die extra variabelen maken het wel erg omslachtig. Het is veel handiger om de resultaatstring van getText direct onder handen te laten nemen door de methode equals. De boolean waarde die daar het resultaat van is, kan direct worden gebruikt als voorwaarde in de if-opdracht
if (password.getText() .equals(sleutel) ) // korter.

Als nishing touch maken we van het tekstveld een echt password-veld. Door de aanroep van setEchoChar kunnen we er voor zorgen dat er tijdens het intikken alleen sterretjes in beeld verschijnen, zoals gebruikelijk bij password-velden. Is de sleutel eenmaal geraden, dan halen we het tekstveld weg door een aanroep van de methode setVisible.
blz. 93

Minimum/maximum thermometer In listing 13 staat de tekst van een programma dat het gedrag vertoont van een maximum/minimum-thermometer. Met een scrollbar kan de gebruiker de temperatuur instellen. De maximale en minimale waarde ooit behaald wordt op het scherm getoond. Als de gebruiker op de reset-toets drukt, worden maximum en minimum weer gelijk aan de huidige temperatuur, en kunnen we op nieuwe records gaan jagen. Kern van dit programma is de if-opdracht in adjustmentValueChanged. Is de huidige waarde groter dan het maximum-tot-nu-toe? Dan moet het maximum worden aangepast! De variabelen minimum en maximum zijn in alle methoden nodig, en zijn dus gedeclareerd als membervariabelen. Ook de scrollbar is in meerdere methoden nodig, en is dus een membervariabele. De button-variabele is echter alleen in de methode init nodig, en kan dus locaal in init worden gedeclareerd.

8.3

Graek en nulpunten van een parabool

Beschrijving van de casus We gaan een wat groter programma maken, waarin zowel keuze als herhaling een belangrijke rol spelen. Maar bovendien interactiecomponenten met event-listeners, methoden en parameters, locale declaraties en membervariabelen. Ook komen enkele typische aspecten van het werken met double-waarden aan de orde. Het programma tekent een parabool. Een parabool is de graek van een wiskundige functie met het voorschrift y = a x2 + b x + c, waarbij a, b en c nog nader te bepalen constanten zijn. In de applet kan de gebruiker de waarden van a, b en c invoeren in tekstvelden, om dan meteen de graek te zien veranderen. Een klassiek onderwerp in de middelbare-schoolwiskunde is het bepalen van de nulpunten van een parabool. Die kunnen gemakkelijk worden bepaald met de zogenaamde abc-formule (genoemd naar de constanten a, b en c die er in gebruikt worden). De applet gaat deze formule gebruiken om naast de graek ook de waarden van de nulpunten aan de gebruiker te tonen. Structuur van het programma Er zijn membervariabelen voor drie tekstvelden, waarin de gebruiker de waarden van a, b en c in kan vullen. In de methode init worden de tekstveld-objecten gecre eerd. Bovendien zijn er de waarden a, b en c zelf, die in de methode init alvast een waarde krijgen, zodat de gebruiker al een parabool kan aanschouwen zonder eerst waarden te hoeven invullen. We gaan er voor zorgen dat de doublewaarden variabelen a, b, en c altijd overeenkomen met de ingevulde teksten in de drie tekstvelden. In init geven we de tekstvelden om te beginnen als startwaarde de (naar string geconverteerde) waarde van a, b, en c.

8.3 Graek en nulpunten van een parabool

93

import java.awt.*; import java.awt.event.*; import java.applet.Applet;


5

public class Thermo extends Applet implements ActionListener, AdjustmentListener { Scrollbar meter; int maximum, minimum; public void init() { this.setLayout(new BorderLayout()); Button reset; reset = new Button("Reset"); meter = new Scrollbar(Scrollbar.HORIZONTAL, 0, 1, -50, 50); this.add(reset, BorderLayout.SOUTH); this.add(meter, BorderLayout.NORTH); reset.addActionListener(this); meter.addAdjustmentListener(this); maximum = 0; minimum = 0; } public void adjustmentValueChanged(AdjustmentEvent e) { int waarde; waarde = meter.getValue(); if (waarde>maximum) maximum = waarde; if (waarde<minimum) minimum = waarde; this.repaint(); } public void actionPerformed(ActionEvent e) { maximum = meter.getValue(); minimum = maximum; this.repaint(); } public void paint(Graphics gr) { gr.drawString("hoogste: " + maximum, 50, 90); gr.drawString("laagste: " + minimum, 50,110); int w = this.getWidth(); double schaal = (w-32)/100.0; gr.setColor(Color.BLUE); gr.fillRect(0, 30, (int)(16+schaal*(minimum+50)), 20); gr.setColor(Color.RED); gr.fillRect((int)(w-16+schaal*(maximum-50)), 30, w, 20); } private static final long serialVersionUID = 1; } Listing 13: Thermo/src/Thermo.java

10

15

20

25

30

35

40

45

50

94

Keuze

import java.awt.*; import java.awt.event.*; import java.applet.Applet;


5

public class Parabool extends Applet implements ActionListener { TextField abox, bbox, cbox; double a, b, c; public void init() { a = 0.5; b = 2.0; c = -4.0; abox = new TextField(""+a, 8); bbox = new TextField(""+b, 8); cbox = new TextField(""+c, 8); this.add(abox); this.add(bbox); this.add(cbox); abox.addActionListener(this); bbox.addActionListener(this); cbox.addActionListener(this); } public void actionPerformed(ActionEvent e) { a = Double.parseDouble(abox.getText()); b = Double.parseDouble(bbox.getText()); c = Double.parseDouble(cbox.getText()); this.repaint(); } public void paint(Graphics gr) { this.oplossingen(gr); gr.setColor(Color.RED); this.assen(gr); gr.setColor(Color.BLUE); this.grafiek(gr); } private double parabool(double x) { return a*x*x + b*x + c; }

10

15

20

25

30

35

40

45

Listing 14: Parabool/src/Parabool.java, deel 1 van 2

8.3 Graek en nulpunten van een parabool

95

50

private void oplossingen(Graphics gr) { double discriminant, noemer, wortel; discriminant = b*b-4*a*c; noemer = 2*a; if (noemer==0) gr.drawString("rechte lijn!", 50,50); else if (discriminant<0) gr.drawString("geen nulpunten", 50, 50); else if (discriminant==0) gr.drawString("een nulpunt: " + -b/noemer, 50, 50); else { wortel = Math.sqrt(discriminant); gr.drawString("twee nulpunten: " + + (-b-wortel)/noemer + " en " + (-b+wortel)/noemer , 50, 50); } } private void assen(Graphics gr) { gr.drawLine(0,250,500,250); gr.drawLine(250,0,250,500); } private void grafiek(Graphics gr) { int xpixel, ypixel, oldy; double xwaarde, ywaarde, schaal; schaal = 0.03; oldy = 0; for (xpixel=-1; xpixel<500; xpixel++) { xwaarde = (xpixel-250)*schaal; ywaarde = parabool(xwaarde); ypixel = (int) (250-(ywaarde/schaal)); if (xpixel>=0) gr.drawLine(xpixel-1, oldy, xpixel, ypixel); oldy = ypixel; } } private static final long serialVersionUID = 1; } Listing 15: Parabool/src/Parabool.java, deel 2 van 2

55

60

65

70

75

80

85

90

96

Keuze

Figuur 18: De applet Parabool in werking

8.3 Graek en nulpunten van een parabool

97

Elke keer als de gebruiker in een van de tekstvelden op Enter drukt, wordt de methode actionPerformed aangeroepen (omdat actionPerformed is gedenieerd in de klasse Parabool, die dat beloofd had met implements ActionListener, en waardoor elk Parabool-object zich dus als action-listener kan gedragen; zo ook het object this, wat immers een Parabool is, en die daarom dus als action-listener kan worden gekoppeld aan de tekstvelden). Als reactie daarop maken we de waarde van a, b en c weer gelijk aan de double-versie van de, inmiddels mogelijk gewijzigde inhoud van de tekstvelden. Nadat actionPerformed de waarden van a, b en c weer up-to-date heeft gemaakt, roept hij repaint aan, die een aanroep van paint forceert. In de methode paint moet het eigenlijke werk gaan gebeuren: het bepalen van de nulpunten, en het tekenen van de graek. Omdat dat hele verschillende taken zijn, maken we aparte methoden voor deze twee dingen, en een derde methode voor het tekenen van de x-as en de y -as. Daardoor blijft de methode paint mooi overzichtelijk: het enige wat hier gebeurt is het kiezen van de kleuren, en het aanroepen van de drie methoden die het eigenlijke werk doen. Deze methoden krijgen het Graphics-object dat in paint beschikbaar is als parameter mee, zodat ze zelf dingen op het scherm kunnen tekenen. De nulpunten In de methode nulpunten kunnen we ons nu helemaal op het uitrekenen van de nulpunten concentreren, zonder gedoe met tekstvelden. We kunnen er op vertrouwen dat de variabelen a, b en c de benodigde constanten bevatten. De nulpunten kunnen we uitrekenen met behulp van de abc-formule, maar dan moeten er natuurlijk wel nulpunten zijn. In de abc-formule wordt de wortel getrokken uit b2 4ac, dus als die waarde negatief is, zijn er geen oplossingen. Bovendien wordt er gedeeld door 2a, dus als die waarde 0 is moet er ook speciale actie ondernomen worden. Met if-opdrachten worden al deze gevallen uitgesplitst, waarna in elk geval een toepasselijke melding op het scherm kan worden gezet. De graek Basisidee van het tekenen van de graek is dat we voor alle mogelijke x-waarden de bijbehorende y -waarde berekenen. Op dat punt zouden we een stipje kunnen zetten:
for (xpixel=0; xpixel<500; xpixel++) { ypixel = this.parabool(xpixel); gr.fillRect(xpixel, ypixel, 1, 1); }

Maar als de graek erg steil loopt, en y -waarden voor aangrenzende x-waarden meer verschillen dan de dikte van de stippen, dan komen de stippen los te staan. Beter is het daarom, om in plaats van een stip een lijn te trekken vanaf het vorige berekende punt. De y -waarde van het punt wordt daarom na gebruik bewaard in de variabele oldy voor de volgende ronde. Met een if-opdracht zorgen we ervoor om de eerste keer geen lijn te trekken (want dan was er geen vorige ronde, en heeft oldy dus nog geen zinvolle waarde). De oude x-waarde hoeven we niet apart te bewaren, want die is natuurlijk x 1.
for (xpixel=-1; xpixel<500; xpixel++) { ypixel = this.parabool(xpixel); if (xpixel>=0) gr.drawLine(xpixel-1, oldy, xpixel, ypixel); oldy = ypixel; }

De compiler is nogal streng, en blijft zeuren dat variable oldy may not have a value when used. Dat is omdat de compiler wel ziet dat in de while-body oldy gebruikt lijkt te worden voordat hij een waarde krijgt, maar niet slim genoeg is om te doorzien dat die if-opdracht dat nou juist voorkomt. Om de compiler tevreden te stellen geven we oldy voorafgaand aan de while-opdracht zomaar een waarde. Schaling Het interval [0, 500] is niet het interessantste gebied om een parabool te bekijken, althans niet voor de waarden van a, b en c die je als eerste te binnen schieten. Interessanter is bijvoorbeeld het interval [7, 7]. Daarom wordt de berekening van de functiewaarde niet gedaan met de waarde van xpixel, maar met de verschoven en opgeschaalde versie xwaarde daarvan. De verkregen

98

Keuze

blz. 94

ywaarde wordt teruggeschaald, teruggeschoven, en er wordt bovendien gecorrigeerd voor het Javaco ordinatensysteem dat de verkeerde kant oploopt. t Is een beetje lastig onder woorden te brengen; bekijk de formules in listing 14 en reken maar na.

8.4

Exceptions

Het afhandelen van fouten Bij het uitvoeren van methodes kunnen er uitzonderlijke omstandigheden zijn die het onmogelijk maken dat de methode compleet wordt uitgevoerd. In dat geval is er sprake van een exception. Zon exception wordt door de methode opgeworpen, en het is dan aan de aanroeper om daar een oplossing voor te vinden. Een voorbeeld van een methode die een exception opwerpt, is de methode parseInt. Deze werpt een exception op als de aangeboden string iets anders dan cijfers bevat. In dat geval kan het programma geen normale verdere doorgang vinden. De try-catch opdracht Je zou kunnen vermijden dat er exceptions ontstaan, door vooraf te controleren of aan alle voorwaarden is voldaan (in het geval van parseInt: of de aangeboden string uitsluitend cijfer-tekens bevat). Maar dan doe je dubbel werk, want parseInt doet die controle nogmaals. Beter is het om te reageren op het optreden van de exception. Dat gebeurt met de speciale try-catch-opdracht. Je kunt de aanroep die mogelijkerwijs een exception zal opwerpen in de body van een try-opdracht zetten. In het geval dat er inderdaad een exception optreedt, gaat het dan verder in de body van het catch-gedeelte. Gaat echter alles goed, dan wordt het catch-gedeelte overgeslagen. Bijvoorbeeld:
try { n = Integer.parseInt(s); uitvoer.setText("kwadraat van" + n + "is" + n*n); } catch (Exception e) { uitvoer.setText( s + " is geen getal"); }

In het catch-gedeelte wordt de opgeworpen exception als het ware opgevangen. Achter het woord catch moet een soort parameter worden gedeclareerd. Via deze parameter is te achterhalen wat er precies fout is gegaan. In het geval van parseInt is dat zo ook wel duidelijk, maar de parameter moet toch gedeclareerd worden, ook als we hem niet gebruiken. In de body van try kunnen meerdere opdrachten staan. Bij de eerste exception gaat het echter verder bij catch. De rest van de opdrachten achter try mag er dus van uitgaan dat er geen exception is opgetreden. Let op dat de bodies van het try- en het catch-gedeelte tussen accolades moeten staan, zelfs als er maar e en opdracht in staat. (Dat is wel onlogisch, want bij opdrachten zoals if en while mogen in die situatie de accolades worden weggelaten.) Het type Exception, waarvan achter catch een variabele wordt gedeclareerd, is een klasse met allerlei subklassen: NumberFormatException, NullPointerException, InterruptedException enzovoorts. Deze verschillen in het soort details dat je over de exception kunt opvragen (door het aanroepen van methodes van het exception-object). Ben je niet ge nteresseerd in de details, dan kun je ruwweg een Exception-object declareren, maar anders kun je het object van het juiste type declareren. Het is toegestaan om bij e en try-opdracht meerdere catch-gedeeltes te plaatsen. Die kun je dan parameters van verschillend (sub-)type geven. Bij het optreden van een exception wordt de eerste afhandeling gekozen met een passend type. Een voorbeeld vormt het lezen van les (dit wordt uitgebreider besproken in sectie 13.5). Fouten die daarbij kunnen optreden zijn dat de le in het geheel niet bestaat (FileNotFoundException), of dat de le weliswaar bestaat, maar toch niet gelezen kan worden (IOException). Met e en try-catch-catch opdracht kun je op beide mogelijkheden anticiperen:
try { /* opdrachten om de file te lezen */ } catch (FileNotFoundException fnfe) { uitvoer.setText("de file bestaat niet"); } catch (IOException ioe) { uitvoer.setText("de file is onleesbaar"); }

Bij sommige typen exception, zoals NumberFormatException en NullPointerException, is het

8.4 Exceptions

99

opvangen vrijwillig. Bij andere typen is het verplicht, en controleert de compiler dat je dit niet vergeet. Als je desondanks geen speciale actie wilt ondernemen als reactie op de exception, kun je de body van catch leeg laten, maar de try-catch moet er wel staan.

Opgaven
8.1 Teller-applet a. Schrijf een applet dat bestaat uit een button met het opschrift meer, en daaronder de waarde van een teller. Aan het begin heeft de teller de waarde 0. Elke keer als de gebruiker op de button drukt, wordt de waarde van de teller opgehoogd. b. Verander het programma nu zo, dat de teller niet met drawString wordt getekend, maar dat de teller een TextField-object is. c. De gebruiker kan het TextField-object ook zelf veranderen, door daar een ander getal in te tikken (en daarna op de Enter-toets te drukken). Maak het programma nu zo, dat de applet bij het ingevoerde getal verder begint te tellen als de meer-button weer wordt gebruikt. 8.2 Parabool Breid het programma parabool zo uit, dat de waarden van de nulpunten ook worden getoond in de gevallen dat a en/of b en/of c de waarde 0 hebben. 8.3 Foutmelding else without if Bij het compileren van onderstaand programma geeft de compiler de foutmelding
IfDemo.java:7: else without if.

Hoe komt dat?


class IfDemo extends Applet { public void paint(Graphics gr) { int getal; getal = 5; if (getal<0); gr.drawString("negatief", 10, 10); else gr.drawString("positief", 10, 10); } }

8.4 Range-test Iemand wil testen of een variabele x tussen 10 en 20 ligt, en schrijft:
if (10 <= x <= 20) gr.drawString("ja", 10, 10);

De compiler geeft echter een foutmelding. Waarom? Hoe moet het dan wel? 8.5 Equals en == Welk van de volgende uitspraken is waar: a. Als s==t geldt, dan geldt ook s.equals(t) b. Als s.equals(t) geldt, dan geldt ook s==t 8.6 Opdrachten uitsparen Iemand schrijft de volgende opdrachten in een programma:
s1 = invoer1.getText(); s2 = invoer2.getText(); n1 = Integer.parseInt(s1); n2 = Integer.parseInt(s2); r = n1*n2; uitvoer.setText("product: " + r );

Kun je hetzelfde ook met e en opdracht doen? Zo nee, waarom niet; zo ja, hoe dan? 8.7 Objectvariabelen uitsparen In het programma Cirkel in listing 11 wordt de variabele kleiner die de verwijzing naar het Button-object gedeclareerd als membervariabele. In het programma Thermo in listing 13 is de
blz. 89 blz. 93

100

Keuze

Button-variabele reset echter een lokale variabele in de methode init. Wat is de reden voor dit verschil?
blz. 91

8.8 Gelijkheid aan true Bekijk het programma Geheim in listing 12. Mag je in plaats van if (open) ook schrijven: if (open==true) ? Zo nee, waarom niet; zo ja, is dat beter? 8.9 Pyramide Schrijf een applet dat een pyramide van cirkeltjes weergeeft. Er zijn bovendien twee knoppen: indrukken van less verwijdert een rij, indrukken van more voegt een rij toe. Initieel zijn er 4 rijen, minimaal 1 en maximaal 10.

101

Hoofdstuk 9

Objecten en klassen
9.1 Klasse: beschrijving van een object
Object: groepje variabelen met methoden Een object is een groepje variabelen dat bij elkaar hoort. Je realiseert je dat niet voortdurend, maar iedere keer als je een object cre eert met new, maak je een nieuw groepje variabelen. Bijvoorbeeld, als je een nieuwe button maakt met de expressie new Button("druk hier"); dan ontstaat er een groepje variabelen waarin alle noodzakelijke administratie voor een button wordt bijgehouden: de afmetingen, de positie op het scherm, de kleur, de status (ingedrukt of niet), de bijbehorende action-listener, het opschrift, enzovoorts. Met die variabelen heb je echter niet direct te maken: om een button-object te kunnen gebruiken in je programma hoef je niet eens te weten welke variabelen er precies in een button-object zitten, en hoe die heten. Wel is het van belang om te weten welke methoden er zijn, die het object kunnen manipuleren. Voor een button-object zijn dat onder ander de methode setLabel (om het opschrift te veranderen) en de methode addActionListener. Via deze methoden verander je indirect de variabelen die deel uitmaken van het object, zonder daar echter direct een toekenningsopdracht aan te doen. Klasse: declaratie van variabelen plus denitie van methoden De klasse-denitie is een beschrijving van de objecten van die klasse. In de klasse-denitie staan daarom: een declaratie van de variabelen waaruit het object bestaat een denitie van de methoden waarmee het object gemanipuleerd kan worden Behalve voor de klassen in de Java-bibliotheek geldt dit ook voor de klassen die je zelf schrijft. Bijvoorbeeld, als je een klasse Thermo hebt gedenieerd die begint met:
class Thermo extends Applet { private Scrollbar meter; private int min, max;

dan bestaat elk Thermo-object dat daarna wordt gemaakt uit drie variabelen: een verwijzing naar een scrollbar en twee gehele getallen. Bovendien bevat elk Thermo-object, omdat de klasse een extensie is van Applet, alle variabelen die in de klasse Applet al waren gedeclareerd. Zon Thermoobject wordt aangemaakt door de browser, als deze een webpagina tegenkomt waarin een applet is opgenomen met de (bytecode van) deze klasse als code. Terwijl je het programma schrijft heb je misschien het gevoel dat van elk van de variabelen die bovenin de klasse zijn gedeclareerd, er maar e en aanwezig is. Dat is ook wel zo binnen e en object, maar het kan best zo zijn dat de browser twee exemplaren van het applet aanmaakt. Dat gebeurt als er tweemaal een <applet>-tag in de html-le staat met dezelfde code; we hebben daarvan een voorbeeld gezien in listing 7, waar met e en html-le twee Rechthoek-applets werden aangemaakt. Elk van de aangemaakte applets heeft zijn eigen setje variabelen. Objecten en objectverwijzingen Om een idee te krijgen van hoe objecten in het geheugen worden opgebouwd, bekijken we in detail wat er gebeurt bij het runnen van het programma Thermo (de minimum/maximum-thermometer in listing 13). Bij het verwerken van de <applet>-tag cre eert de browser een Thermo-object. De browser beheert een verwijzing, en die gaat naar het nieuwe object wijzen. Het Thermo-object bestaat uit de

blz. 66

blz. 93

102

Objecten en klassen

variabelen die in de klasse zijn gedeclareerd: twee int-variabelen min en max, en een verwijzing naar een Scrollbar-object, genaamd meter. Maar omdat de klasse Thermo een extensie is van de klasse Applet, bestaat het Thermo-object voor een deel ook uit de variabelen die al in de klasse Applet waren gedeclareerd. Dan roept de browser de methode init aan. In die methode is een locale variabele gedeclareerd: een verwijzing naar een Button-object, genaamd reset. Lokale variabelen staan ook in het geheugen, maar maken geen deel uit van een object. De situatie in het geheugen kun je je nu voorstellen als:
reset browsers current applet this

Thermo

gerfd zelf gedeclareerd


min 0 meter max null 0

De methode init heeft een Thermo-object onder handen gekregen, die te bereiken is via de naam this. Je kunt this beschouwen als een soort variabele (alleen mag je er geen nieuwe waarde aan toekennen). In de schets is niet verder uitgewerkt welke variabelen precies deel uitmaken van het van Applet ge erfde deel van het Thermo-object. Dat kan je en hoef je niet te weten bij het gebruiken van bibliotheek-klassen. De lokale variabele reset heeft nog geen waarde gekregen. De membervariabelen meter, min en max vam het het Thermo-object hebben ook nog geen waarde gekregen, maar membervariabelen krijgen automatisch een neutrale waarde: 0 voor getallen, false voor booleans, en null voor objectverwijzingen. Met de toekenningsopdrachten in de body van de methode init krijgt de variabele reset een waarde, en de variabele meter een wat nuttigere waarde dan het neutrale null:
reset = new Button("Reset"); meter = new Scrollbar(Scrollbar.HORIZONTAL,0,1,-50,50);

Hiermee onstaat de volgende situatie:


Button
reset browsers current applet this

Thermo Scrollbar

meter min 0 max 0

De vier variabelen waaraan een toekenning is gedaan hebben nu een waarde. In het geval van min en max is dat een int-waarde, in het geval van reset en meter is het een objectverwijzing naar een nieuw gecre eerd object. Die nieuwe objecten bevatten de variabelen die zijn gedeclareerd in respectievelijk de klasse Button en Scrollbar, maar dat is in de schets niet verder uitgewerkt. In een methode mogen zowel de lokale variabelen worden gebruikt (hier is dat reset), als de membervariabelen van het object onder handen (hier: meter, min en max in het object waar this naar wijst). In het geval dat een lokale variabele hetzelfde zou heten als een membervariabele, heeft de lokale variabele voorrang. In de volgende twee opdrachten wordt de methode add aangeroepen, met de twee zojuist gecre eerde objecten als parameter:
this.add(reset);

9.1 Klasse: beschrijving van een object

103

this.add(meter);

De methode add is een methode van Applet. De verwijzing this wijst weliswaar naar een Thermoobject, maar een deel van dat object (het ge erfde deel) is een Applet-object. Het is dat (deel van het) object dat door add onder handen wordt genomen. We hebben add niet zelf geschreven, en het is voor ons als applicatie-programmeur niet precies bekend welke variabelen in het Applet-object door add worden veranderd, omdat we die variabelen niet kennen. Wat er precies gebeurt is dus een beetje giswerk. Je kunt je voorstellen dat het Appletobject een kopie bewaart van de als parameter meegegeven objectverwijzingen, zodat altijd alle met add toegevoegde componenten beschikbaar zijn voor later gebruik. De situatie wordt dus ongeveer als volgt. (Het rijtje variabelen heet in het echt vast niet added, maar we kunnen, hoeven en willen niet weten wat de naam dan wel is).
Button
reset browsers current applet this

Thermo
added

Scrollbar

meter min 0 max 0

In de laatste twee opdrachten in de body van init worden er event-listeners gekoppeld aan de button- en scrollbar-objecten:
reset.addActionListener(this); meter.addAdjustmentListener(this);

Ditmaal wordt dus niet het this object, maar de twee andere objecten onder handen genomen. Het is alweer giswerk wat hier precies gebeurt. Het is echter redelijk om te verwachten dat in beide gevallen een kopie van de aangeboden parameter wordt bewaard voor later gebruik. Als in de toekomst de button ingedrukt zou worden, dan weet het button-object op die manier nog, van welk object de methode actionPerformed aangeroepen moet worden. Omdat dit de laatste opdrachten in de body van init waren, worden nu de lokale variabelen opgeruimd (de variabele reset, en ook de pseudo-variabele this verliest zijn betekenis). De situatie die na aoop van het uitvoeren van init is ontstaan, ziet er als volgt uit:
Button
reset actListener browsers current applet this

Thermo
added

Scrollbar
adjListener 0 value

meter min 0 max 0

De lokale variabelen zijn weliswaar opgeruimd, maar dat waren alleen maar verwijzingen. De objecten waar die verwijzingen naar verwezen zijn nog wel degelijk beschikbaar: ze zijn via-via te bereiken, omdat er door de aanroep van add een extra verwijzing naar bewaard is gebleven. Programmas met meerdere klassen Tot nu toe hebben we steeds programmas geschreven die uit e en klasse bestonden; steeds was de enige klasse een extensie van Applet, en verwachtten we dat de browser e en object van die klasse

104

Objecten en klassen

eren. In de programmas speelden weliswaar naast dat object ook nog andere objecten een zou cre rol, maar het type van die objecten was steeds een bibliotheek-klasse (Button, Scrollbar, Graphics, String enzovoorts). In de nu volgende toepassing verandert dat. We gaan behalve de welbekende extensie van Applet nog meer klassen maken, en in het programma gaan we objecten gebruiken die die klassen als type hebben. Op deze manier kunnen we zelfgemaakte objecten naar eigen ontwerp in het programma gebruiken.

9.2

Toepassing: Bewegende deeltjes

Beschrijving van de casus Het programma dat we in deze sectie zullen ontwikkelen is een simulatie van bewegende deeltjes in een begrensde ruimte. Je kunt denken aan moleculen in een afgesloten vat, of aan biljartballen op een (wrijvingsloze) tafel, of aan ratten in een val. Voor het gemak zullen we de deeltjes afbeelden als kleine gekleurde cirkels, en de ruimte als grote rechthoek. In het runnende programma zullen drie ruimtes zichtbaar zijn, ieder met verschillende afmetingen. In iedere ruimte bevinden zich drie deeltjes, ieder met een verschillende kleur. De deeltjes kunnen bewegen. Ze doen e en stap als de gebruiker op de button met het opschrift stap drukt. Er is bovendien een button met het opschrift start. Als de gebruiker daarop drukt, blijven de deeltjes bewegen, en ziet de gebruiker dus een animatie. Het opschrift van deze button verandert op dat moment ook in stop, en met nog een druk op deze knop kan de gebruiker de animatie weer stopzetten. In guur 19 is een snapshot van het programma te zien. De klasse Ruimte In het window van de simulatie (zie guur 19) zijn vijf dingen te zien: drie ruimtes met deeltjes, en twee buttons. Het zou het gemakkelijkste zijn als we deze vijf dingen op dezelfde manier konden maken: cre eren van een nieuw object met new, en deze aan de display toevoegen met add. Voor de buttons is dat geen probleem, maar er bestaat natuurlijk geen bibliotheek-klasse Ruimte. Toch is dat geen bezwaar, want we kunnen zon klasse zelf maken. We hoeven die klasse gelukkig niet van de grond af aan op te bouwen. We kunnen namelijk voortborduren op een al wel bestaande klasse: Canvas. Een Canvas is een interactie-component, net zoals Button en Scrollbar. Het kan daarom met add aan de applet worden toegevoegd. Een Canvas is een soort schilderslinnen, waarop je een tekening kunt maken. Een canvas heeft afmetingen en een achtergrondkleur, dus daar hoeven we ons niet meer druk om te maken. In de klasse Ruimte, die een extensie wordt van Canvas, hoeven we alleen maar extra variabelen toe te voegen die de gekleurde deeltjes beschrijven, en methoden die die deeltjes tekenen en laten bewegen. De klasse Deeltje Elk bewegend deeltje dat in dit programma een rol speelt, heeft een aantal eigenschappen: een kleur, een positie binnen de ruimte, en een bewegingsrichting. Voor elk van deze eigenschappen kunnen we variabelen declareren. De kleur is een Color-verwijzing, de positie bestaat uit twee int-waarden x en y, en de bewegingsrichting kunnen we beschrijven met de afstand dx en dy die het deeltje bij elke stap moet bewegen. Elk deeltje moet zijn eigen kleur, positie en richting krijgen. We maken daarom een aparte klasse Deeltje, waarin deze variabelen zijn gedeclareerd. In het programma kunnen we dan voor elk deeltje een Deeltje-object cre eren, waarin de benodigde variabelen gebundeld zijn. De klasse Deeltje is geen extensie van een al bestaande klasse; we maken hem puur vanuit het niets, en een Deeltje-object bestaat dan ook alleen uit de variabelen die we zelf in de klasse declareren. Opzet van de klassen Het programma zal dus gaan bestaan uit drie klassen: de extensie van Applet die we ditmaal Simulatie zullen noemen, en de extra klassen Ruimte en Deeltje. We bekijken nu eerst de opzet van de klassen voor wat betreft de declaraties van membervariabelen; de methoden volgen later.

9.2 Toepassing: Bewegende deeltjes

105

class Simulatie extends Applet { Button stap, auto; Ruimte r1, r2, r3; // methoden nog toe te voegen } class Ruimte extends Canvas { Deeltje d1, d2, d3; // methoden nog toe te voegen } class Deeltje { Color kleur; int x, y, dx, dy; // methoden nog toe te voegen }

Er moeten nu objecten gemaakt worden met deze klassen als type, zodat er een heel netwerk van verwijzingen ontstaat. De gewenste situatie kun je je als volgt voorstellen:
Simulatie Button
browsers current applet r1 stap auto r2

Button
r3

Ruimte

Ruimte

Ruimte

d1

d2

d3

d1

d2

d3

d1

d2

d3

x y dx dy kleur

x y dx dy kleur

x y dx dy kleur

x y dx dy kleur

x y dx dy kleur

x y dx dy kleur

x y dx dy kleur

x y dx dy kleur

x y dx dy kleur

Deeltje

Deeltje

Deeltje

Deeltje

Deeltje

Deeltje

Deeltje

Deeltje

Deeltje

De browser cre eert een Simulatie-object. Dit bestaat voor een deel uit de ge erfde variabelen van Applet, en voor een deel uit de vijf in de klasse Simulatie extra gedeclareerde variabelen: stap, auto, r1, r2 en r3. Het is de taak van de methode init om deze variabelen naar nieuw te maken objecten te laten wijzen. Dat gebeurt zoals gewoonlijk door middel van toekenningsopdrachten:
stap auto r1 = r2 = r3 = = new Button("Stap"); = new Button("Start"); new Ruimte(iets); new Ruimte(iets); new Ruimte(iets);

Hierdoor gaan stap en auto naar nieuw gecre eerde Button-objecten wijzen, en r1, r2 en r3 naar nieuw gecre eerde Ruimte-objecten. De Ruimte-objecten bestaan voor een deel uit de ge erfde variabelen van Canvas, en voor een deel uit de drie in de klasse Ruimte extra gedeclareerde variabelen: d1, d2 en d3. Deze wijzen echter nog niet naar objecten. De constructormethode van Ruimte Bij het evalueren van een new-expressie gebeuren er twee dingen: er wordt ruimte aangemaakt voor het nieuwe object; de constructormethode, zoals gedenieerd in de bijbehorende klasse, wordt aangeroepen. Alle opdrachten die in de constructormethode staan, worden dus automatisch uitgevoerd op het erd wordt. Dat maakt de constructormethode de ideale plaats om de moment dat het object gecre membervariabelen een beginwaarde te geven, en eventuele andere voorbereidingen te treen. De constructormethode onderscheidt zich op twee manieren van de andere methoden die in een klasse worden gedenieerd:

106

Objecten en klassen

de naam van de constructormethode is hetzelfde als de naam van de klasse, en begint dus volgens de stijl-conventies voor variabele-namen bij wijze van uitzondering met een hoofdletter; de constructormethode heeft geen resultaattype, zelfs niet void; de constructormethode kan immers alleen maar via de speciale new-expressie worden aangeroepen, en die heeft automatisch het nieuw gecre eerde object als resultaatwaarde. In onze nieuwe klasse Ruimte gaan we ook een constructormethode schrijven, waarin het Ruimteeerd, wordt klaargezet voor gebruikt. object, direct nadat het is gecre Eerste taak van de constructormethode is om de afmetingen van het Ruimte-object vast te stellen. Dat gebeurt door een aanroep van setSize, die is ge erfd van de klasse Canvas (de klasse Ruimte is een extensie van Canvas). Bij die aanroep moeten de gewenste breedte en hoogte worden opgegeven. Die willen we echter in de drie verschillende ruimtes verschillende waarden geven. Daarom geven we de constructormethode van Ruimte twee parameters, waarmee de breedte en hoogte kunnen worden gespeciceerd. Bij de creatie van de ruimte-objecten (in de init-methode van de klasse Simulatie) kan de gewenste grootte dan per object verschillend worden gekozen. De constructormethode van Ruimte begint dus als volgt (let op: er staat in constructormethodes geen resultaattype tussen public en de methode-naam):
public Ruimte(int breed, int hoog) { this.setSize(breed, hoog);

Volgende taak is het instellen van de achtergrondkleur. Ook dit kan via een van Canvas ge erfde methode: setBackground. We maken de achtergrond van elk ruimte-object hetzelfde, namelijk een zelfgemengde lichtgele kleur:
this.setBackground(new Color(255,255,128));

Nu wordt het tijd om de membervariabelen van Ruimte een waarde te geven: de verwijzingen naar Deeltje-objecten d1, d2 en d3. Deze verwijzingen wijzen uit zichzelf nog nergens naar, dus de Deeltje-objecten moeten nog worden gecre eerd:
d1 = new Deeltje(iets ); d2 = new Deeltje(iets ); d3 = new Deeltje(iets );

Direct bij de creatie van een Ruimte-object, worden dus ook de drie bijbehorende Deeltje-objecten gemaakt. De klasse Deeltje heeft ook een constructormethode, waarin de variabelen van het nieuwe Deeltje-object een waarde krijgen. We verplaatsen onze aandacht daarom nu eerst even naar de methoden in de klasse Deeltje. De methoden van Deeltje De klasse Deeltje is nergens een extensie van. Een Deeltje-object bestaat dus alleen maar uit de variabelen die in de klasse Deeltje zijn gedeclareerd: x, y, dx, dy en kleur. De vraag die zich nu opdringt is wat we met zon Deeltje-object eigenlijk willen doen; met andere woorden: welke methoden zijn er nodig in de klasse Deeltje? Het is niet zo moeilijk om een paar nuttige handelingen met Deeltje-objecten te bedenken: een constructormethode, die de variabelen een beginwaarde geeft een methode doeStap, die de plaats van het deeltje zodanig verandert, dat het deeltje een stap doet in de door de richtingsvector aangegeven richting een methode teken, die het deeltje intekent op een als parameter te speciceren Graphicsobject. De methode teken krijgt een Graphics-object als parameter, zodat hij grasche methoden kan aanroepen. We gaan deze methode later aanroepen op het moment dat een deeltje getekend moet worden, maar dat is van later zorg. Voor het eigenlijke tekenen van het deeltje zijn alleen zijn eigen kleur en positie van belang; die worden in de body van teken dan ook gebruikt.
public void teken(Graphics gr) { gr.setColor(kleur); gr.fillOval(x-3, y-3, 7, 7); }

9.2 Toepassing: Bewegende deeltjes

107

Die variabelen moeten dan natuurlijk wel een waarde hebben. De ideale plaats om dat te doen is de constructormethode. Door de gewenste kleur en startpositie als parameter van de constructormethode mee te geven, kan elk deeltje zijn eigen kleur en positie krijgen. Ook de snelheid van het deeltje wordt bij de constructie vastgelegd:
public Deeltje(Color k, int x0, { kleur = k; x = x0; y = y0; dx = dx0; dy = dy0; } int y0, int dx0, int dy0 )

De methode doeStap is het interessantste. Door aanroep van deze methode gaat het deeltje bewegen. Omdat zowel de huidige plaats als de bewegingsrichting in het Deeltje-object zijn opgeslagen, hoeft deze methode geen parameters te krijgen. De beweging vindt in principe plaats door de variabelen x en y te vermeerderen met de bewegings-waarden dx en dy:
public void doeStap(iets ) { x += dx; y += dy;

Het kan echter gebeuren (bij negatieve bewegingsrichting) dat de co ordinaten negatief worden. In dat geval is het gewenst dat het deeltje terugkaatst tegen de muur. De co ordinaat wordt dan juist zo positief als hij negatief geworden was. Maar door het kaatsen verandert ook de bewegingsrichting: bij het kaatsen tegen de zij-muren klapt het teken van de bewegingsrichting om: beweging naar links wordt beweging naar rechts, en omgekeerd. Het kaatsen tegen de linker- en bovenmuur wordt behandeld door de volgende opdrachten:
if (x<0) { x = -x; dx = -dx; } if (y<0) { y = -y; dy = -dy; }

Het deeltje kan echter ook bij de rechter- en ondermuur dreigen uit beeld te raken, en ook hier zouden we het deeltje willen laten terugkaatsen. Om dat te kunnen testen, moet het deeltje echter weten hoe groot de ruimte is waarin hij beweegt, en aan de membervariabelen x, y, dx, dy en kleur is dat niet te zien! Voor dat doel gaan we aan doeStap een parameter meegeven die de afmetingen van de ruimte waarin het deeltje beweegt aangeeft. Een Dimension-object is precies wat we nodig hebben, al moeten de zich daarin bevindende breedte en hoogte nog met een cast naar int geconverteerd worden:
public void doeStap(Dimension hok) { int maxX, maxY; maxX = (int)hok.getWidth(); maxY = (int)hok.getHeight();

De aldus verkregen waarde van maxX kunnen we nu gebruiken in een derde test in de methode doeStap:
if (x>=maxX) iets

Het is wel leuk om zelf even na te denken hoe deze situatie moet worden opgevangen (de oplossing staat in listing 18). De methoden van Simulatie De klasse Simulatie wordt alleen maar gebruikt voor de creatie van e en object, maar is evengoed belangrijk. Het object modelleert het totale applet, en heeft dezelfde opbouw als alle eerdere eren, en een event-listener programmas: een methode init om de interactie-componenten te cre om de acties van de gebruiker af te handelen. Een methode paint is ditmaal niet nodig, omdat er niets op de achtergrond van het applet wordt getekend. De twee buttons en de drie canvas-objecten

blz. 115

108

Objecten en klassen

tekenen zichzelf namelijk automatisch. Een deel van de methode init hebben we hierboven al besproken: creatie van de vijf objecten waar de membervariabelen naar toe moeten wijzen.
public void init() { stap = new Button("Stap"); auto = new Button("Start"); r1 = new Ruimte(100,196); r2 = new Ruimte(196,150); r3 = new Ruimte(60,75);

Bij de creatie van de Ruimte-objecten geven we de breedte en de hoogte van de ruimte mee, conform de denitie van de constructormethode van Ruimte zoals we die hebben gedenieerd. Zoals gewoonlijk moeten de objecten ook aan de applet worden toegevoegd
this.add(stap); this.add(auto); this.add(r1); this.add(r2); this.add(r3);

De twee buttons krijgen omgekeerd het totale applet-object als action-listener toegewezen:
stap.addActionListener(this); auto.addActionListener(this); }

De action-listener moet natuurlijk wel beloofd worden (met implements ActionListener in de klasse-header), en de belofte moet worden nagekomen. Bij de afhandeling van het indrukken van de button testen we welke van de twee buttons is ingedrukt.
public void actionPerformed(ActionEvent e) { if (e.getSource()==stap) this.doeStap(); else iets }

Voor het gemak zetten we de eigenlijke afhandeling van het indrukken van de stap-button in een aparte methode. Die methode noemen we doeStap (niet te verwarren met de gelijknamige methode in de klasse Deeltje). In de methode doeStap van Simulatie zou de methode doeStap van alle Deeltje-objecten kunnen worden aangeroepen. We gaan dat echter niet voor alle negen Deeltjeobjecten apart doen. In plaats daarvan delegeren we het werk aan de drie Ruimte-objecten, door daarvan een methode aan te roepen, die we (alweer!) doeStap noemen. Niet vergeten dat we die straks nog moeten schrijven. . .
private void doeStap() { r1.doeStap(); r2.doeStap(); r3.doeStap();

Deze methode is private gemaakt, omdat hij alleen maar door een van zijn collega-methoden wordt aangeroepen, en niet direct van buitenaf. Nadat de drie Ruimte-objecten aldus een stap hebben gedaan, moeten we ervoor zorgen dat het resultaat zichtbaar wordt. Daarom roepen we voor elk van de drie ruimtes de methode repaint aan, die de Ruimte-objecten hebben ge erfd van Canvas.
r1.repaint(); r2.repaint(); r3.repaint(); }

De methoden van Ruimte Het enige wat ons nu nog te doen staat is het schrijven van de methoden van de klasse Ruimte. Met de constructormethode van Ruimte hadden we al een begin gemaakt:

9.3 Animatie

109

public Ruimte(int breed, int hoog) { this.setSize(breed, hoog); this.setBackground(new Color(255,255,128));

Hier worden ook de drie Deeltje-objecten van het Ruimte-object gecre eerd. Inmiddels weten we nu ook wat daarbij als parameter meegegeven moet worden: een Color-object, en vier ints voor de startpositie en -snelheid.
d1 = new Deeltje(Color.RED , 30, 40, 10, 10); d2 = new Deeltje(Color.GREEN, 100, 80, 5,-10); d3 = new Deeltje(Color.BLUE , 200, 60, 8, 2);

We moesten niet vergeten om in de klasse Ruimte ook een methode doeStap klaar te zetten. die hadden we immers aangeroepen vanuit de methode doeStap van de klasse Simulatie. Het schrijven van deze methode is heel simpel: we zetten gewoon onze drie deeltjes aan het werk om een stap te doen. De deeltjes wilden de afmeting van de ruimte waarin ze zich bevinden als parameter meekrijgen. Die kunnen ze krijgen: de afmetingen van een Canvas (en dus ook van een Ruimte) zijn eenvoudig op te vragen met de methode getSize:
public void doeStap() { d1.doeStap(this.getSize()); d2.doeStap(this.getSize()); d3.doeStap(this.getSize()); }

De laatste methode van de klasse Ruimte is paint. Het gaat om een herdenitie van de gelijknamige methode van Canvas. Het doel hiervan is hetzelfde als we gewend zijn bij Applet: de methode paint wordt aangeroepen als het canvas getekend moet worden. Aanroep van paint kan bovendien worden afgedwongen door het aanroepen van repaint, iets wat we in Simulaties doeStap inderdaad hebben gedaan. Het tekenen van het schilderij op het canvas is ook al heel gemakkelijk: we laten onze drie deeltjes zichzelf tekenen, door aanroep van de methode teken die we daarvoor hadden klaargezet in de klasse Deeltje.
public void paint(Graphics gr) { d1.teken(gr); d2.teken(gr); d3.teken(gr); }

Hiermee is het programma bijna voltooid. Wat rest is de afhandeling van het indrukken van de tweede button. Maar dat is een onderwerp apart.

9.3

Animatie

Automatische actie Door steeds maar op de Stap-button te blijven drukken, kunnen we de deeltjes laten blijven bewegen. Maar dat wordt op den duur vervelend; leuker zou het zijn als we lui achterover kunnen leunen, en de deeltjes automatisch blijven bewegen. Met andere woorden: als het programma zich als een tekenlm, oftewel een animatie zou kunnen gedragen. Het leuke van Java is dat dat veel gemakkelijker kan dan in eerdere talen zoals Basic, Pascal, C en C++. Het kost eigenlijk maar een paar regels in het programma om het tot leven te wekken. Het mechanisme zit echter wel doortrapt in elkaar, dus let op dat je de draad niet kwijtraakt. De klasse Thread Kern van het animatie-mechanisme is de klasse Thread in package java.util. Als je een animatie wilt maken, moet je een object van deze klasse cre eren; een Thread-object dus. Als parameter moet je daarbij een object meegeven; this is meestal een goede keuze. Je gaat dus als volgt te werk:
Thread animatie; animatie = new Thread(this);

Nadat het Thread-object gecre eerd is, kun je de animatie starten door de methode start aan te roepen:

110

Objecten en klassen

animatie.start();

Het Thread-object reageert daarop door op zijn beurt de methode run aan te roepen van het object dat bij creatie als parameter was meegegeven. Hoe weet het object zo zeker dat het meegegeven object een methode run kent? Wel, de compiler controleert dat het meegegeven object als type een Runnable klasse heeft. Daar zorgen we dus voor door in de klasse-header te beloven: implements Runnable, en die belofte na te komen door inderdaad een methode run te deni eren. Dit lijkt allemaal maar omslachtig. Waarom al die moeilijkdoenerij om de methode run aan te roepen? Dat kan toch ook direct door
this.run();

te schrijven? Dat kan inderdaad, maar er is e en verschil: het Thread-object roept de methode run aan, maar wacht niet totdat dat afgelopen is. De methode start zet de methode run aan het werk, en keert direct daarna terug naar de aanroeper. Vanaf dat moment gebeuren er dus twee dingen tegelijk: de methode run begint, maar de rest van het programma draait ook weer verder! Daar gaan we gebruik van maken, door in de methode run een opdracht steeds opnieuw te laten uitvoeren. De opdracht staat daarom in de body van een while-opdracht, met als voorwaarde iets wat altijd waar blijft:
public void run() { while (1==1) this.doeStap(); }

(In plaats van de altijd-ware voorwaarde 1==1 hadden we ook de boolean constante true kunnen gebruiken). Hiermee wordt de methode die vroeger werd aangeroepen als reactie op het indrukken van Stap automatisch steeds opnieuw aangeroepen. De methode sleep Maar wacht even, dat gaat wel erg snel: nu is het stuiteren van de deeltjes bijna niet meer te volgen. Na elke stap moet eigenlijk een korte pauze worden ingelast. Dat kan, en wel door aanroep van de statische methode sleep uit de klasse Thread. Als parameter krijgt deze methode het aantal milliseconden dat de pauze moet duren. We herzien dus de methode run:
public void run() { while (true) { this.doeStap(); Thread.sleep(50); } }

De pauze van 50 milliseconden zorgt ervoor dat er 20 keer per seconde een stap wordt uitgevoerd, wat een mooie vloeiende animatie oplevert. Aanroep van sleep moet in een try-catch opdracht De aanroep van sleep, als die precies zo wordt gedaan als in de vorige paragraaf, geeft echter een foutmelding van de compiler: exception must be caught. Het is namelijk zo dat in uitzonderlijke omstandigheden het (haze-)slaapje ook onderbroken kan worden voordat de slaaptijd is verstreken. In dit programma zal dat niet gebeuren, omdat we die mogelijkheid helemaal niet gebruiken. Evengoed eist de compiler dat we toch rekening houden met de mogelijkheid. Het opvangen van dit soort uitzonderlijke omstandigheden (exceptions) moet gebeuren met een try-catch-opdracht, zoals besproken in sectie 8.4.
try { Thread.sleep(50); } catch (Exception e) { }

Controleren van de animatie Het Thread-mechanisme kunnen we gebruiken om het bewegen van de deeltjes automatisch te laten eerd en gestart als verlopen na het indrukken van de start-button. Het Thread-object wordt gecre reactie op het indrukken van de button:

9.3 Animatie

111

public void actionPerformed(ActionEvent e) { if (e.getSource()==auto) { animatie = new Thread(this); animatie.start();

Om de animatie ook weer te kunnen stoppen, veranderen we op dat moment het opschrift van de button:
auto.setLabel("Stop");

De afhandeling van de auto-button moet, nu hij het opschrift Stop heeft, natuurlijk anders verlopen dan voorheen. Dat betekent dat we de body van actionPerformed weer even moeten herzien. We gebruiken een boolean variabele beweging, en we zorgen ervoor dat die de waarde true heeft zolang de animatie loopt. Deze variabele wordt in de klasse gedeclareerd, en krijgt in init de waarde false. De afhandeling van het indrukken van de auto-button verloopt nu zo:
public void actionPerformed(ActionEvent e) { if (e.getSource()==auto) { if (beweging) { // stop de animatie beweging = false; auto.setLabel("Start"); } else { // start de animatie beweging = true; animatie = new Thread(this); animatie.start(); auto.setLabel("Stop"); } } else enzovoort }

De animatie stopt natuurlijk niet door alleen maar een variabele de waarde false te geven. Maar we kunnen wel de methode run aanpassen. In plaats van de while-opdracht eeuwig te laten duren, kunnen we in de voorwaarde van de while-opdracht de boolean variabele beweging nauwlettend in de gaten houden:
public void run() { while (beweging) { this.doeStap(); try {Thread.sleep(50);} catch (Exception e){} } }

Normaal gesproken zal de voorwaarde van een while-opdracht niet veranderen als er in de body geen toekenningen aan worden gedaan. Maar we zitten nu een een situatie met twee parallel verlopende processen, die elkaar be nvloeden: terwijl de animatie in run bezig is, kan de gebruiker op een knop drukken, en bij het afhandelen daarvan wordt de waarde van de variabele beweging gelijk aan false gemaakt. De waarde null Met een toekenningsopdracht kun je een objectverwijzing naar een object laten wijzen. Het komt soms voor dat je de verwijzing ongedaan wilt maken, dus dat de verwijzing juist niet naar een object moet wijzen. Voor dit doel is er een speciale constante: null. Dit is als het ware de nulwaarde voor verwijzingen; dit is de waarde die verwijzingen hebben als er nog nooit een toekenning aan is gedaan. Het aardige is dat je met een if-opdracht kunt testen of variabelen de waarde null hebben. Gebruikmakend daarvan, kunnen we het programma in de vorige paragraaf herschrijven, zo dat de boolean variabele beweging niet meer nodig is. In plaats daarvan gebruiken we de variabele animatie. Dat is een verwijzing naar het Thread-object. We gaan nu deze verwijzing naar null laten wijzen op het moment dat de gebruiker de Stop-knop indrukt. Om te weten of de animatie

112

Objecten en klassen

Figuur 19: De applet Simulatie in werking

nog beweegt, kunnen we testen of de variabele animatie een andere waarde dan null heeft (d.w.z. daadwerkelijk naar een Thread-object wijst). Daarom nog e en keer de button-afhandelingsmethode:
public void actionPerformed(ActionEvent e) { if (e.getSource()==auto) { if (animatie!=null) { animatie = null; auto.setLabel("Start"); } else { animatie = new Thread(this); animatie.start(); auto.setLabel("Stop"); } } else this.doeStap(); }
blz. 113

De methode run gebruikt nu als voorwaarde om door te gaan dat de waarde van deze verwijzing nog niet null gemaakt is; zie listing 16.

9.3 Animatie

113

import java.awt.*; import java.awt.event.*; import java.applet.Applet;


5

public class Simulatie extends Applet implements ActionListener, Runnable { Ruimte r1, r2, r3; Thread animatie; Button stap, auto; public void init() { r1 = new Ruimte(100,196); r2 = new Ruimte(196,150); r3 = new Ruimte( 60, 75); stap = new Button("Stap"); auto = new Button("Start"); animatie = null; this.add(r1); this.add(r2); this.add(r3); this.add(stap); this.add(auto); stap.addActionListener(this); auto.addActionListener(this); } private void doeStap() { r1.doeStap(); r2.doeStap(); r3.doeStap(); r1.repaint(); r2.repaint(); r3.repaint(); } public void actionPerformed(ActionEvent e) { if (e.getSource()==stap) this.doeStap(); else if (e.getSource()==auto) { if (animatie==null) { animatie = new Thread(this); animatie.start(); auto.setLabel("Stop"); } else { animatie = null; auto.setLabel("Start"); } } } public void run() { while (animatie!=null) { this.doeStap(); try{ Thread.sleep(50); } catch (Exception e) {} } } private static final long serialVersionUID = 1; } Listing 16: Simulatie/src/Simulatie.java

10

15

20

25

30

35

40

45

50

114

Objecten en klassen

import java.awt.*; class Ruimte extends Canvas { Deeltje d1, d2, d3; public Ruimte(int b0, int h0) { this.setSize(b0, h0); this.setBackground(new Color(255,255,128)); d1 = new Deeltje(Color.RED , 30, 40, 10, 10); d2 = new Deeltje(Color.GREEN, 100, 80, 5,-10); d3 = new Deeltje(Color.BLUE , 200, 60, 8, 2); } public void doeStap() { d1.doeStap(this.getSize()); d2.doeStap(this.getSize()); d3.doeStap(this.getSize()); } public void paint(Graphics gr) { d1.teken(gr); d2.teken(gr); d3.teken(gr); } private static final long serialVersionUID = 1; } Listing 17: Simulatie/src/Ruimte.java

10

15

20

25

9.3 Animatie

115

import java.awt.*; class Deeltje { int x, y, dx, dy; Color kleur; public Deeltje(Color k, int x0, int y0, int dx0, int dy0) { kleur = k; x = x0; y = y0; dx = dx0; dy = dy0; } public void doeStap(Dimension hok) { int maxX, maxY; maxX = (int)hok.getWidth(); maxY = (int)hok.getHeight(); x += dx; y += dy;
25

10

15

20

30

if (x >= maxX) { x = 2*maxX-x; dx = -dx; } else if (x<0) { x = -x; dx = -dx; } if (y >= maxY) { y = 2*maxY-y; dy = -dy; } else if (y<0) { y = -y; dy = -dy; } } public void teken(Graphics gr) { gr.setColor(kleur); gr.fillOval(x-4, y-4, 9, 9); } } Listing 18: Simulatie/src/Deeltje.java

35

40

45

116

Objecten en klassen

9.4

Klasse-ontwerp en -gebruik

Ontwerp: wat is en wat kan het object, en hoe? Als je een nieuwe klasse gaat schrijven, moet je je drie dingen afvragen: Wat is het object dat door de klasse wordt beschreven? (Het antwoord op deze vraag schrijf je in de variabele-declaraties bovenin de klasse.) Wat kun je doen met het object dat door de klasse wordt beschreven? (Het antwoord op deze vraag zijn de headers van de methoden in de klasse.) Hoe kun je dingen met het object doen? (Het antwoord op deze vraag zijn de bodies van de methoden in de klasse.) In dit hoofdstuk hebben we voor het eerst meerdere klassen geschreven. Het is nu dus voor het eerst dat al deze vragen aan de orde zijn gekomen. Gebruik: wat kan het object? Als je een klasse wilt gebruiken die iemand anders heeft geschreven, bijvoorbeeld een bibliotheekklasse, dan hoef je eigenlijk maar e en ding te weten: Wat kun je doen met het object dat door de klasse wordt beschreven? Voor het gebruik van de klasse is het niet nodig om precies te weten uit welke variabelen het object zoal bestaat. Ook de bodies van de methoden hoef je niet te kennen om de methoden te kunnen aanroepen. Het is natuurlijk wel handig als je een informeel begrip hebt van wat de methoden doen, maar daarvoor is het niet nodig om te weten hoe dat precies gebeurt. Top-down versus bottom-up ontwerp Bij het schrijven van klassen heb je een grote vrijheid. Je kunt zelf bedenken welke methoden je gaat schrijven, en welke parameters deze zullen krijgen. Dit natuurlijk wel op voorwaarde dat je ze ook op een overeenkomstige manier aanroept; als je in de header van een methode schrijft dat deze een int als parameter krijgt, dan moet je bij aanroep ook een getal als parameter meegeven. Bij het bedenken van al die methoden in al die klassen, kun je ruwweg op twee manieren te werk gaan: Top-down ontwerp. Je begint met de meest veelomvattende klasse, d.w.z. de klasse die een extensie is van Applet. Bij het schrijven van init, paint en de event-listeners merk je vanzelf dat je allerlei objecten nodig hebt. Voor alles wat maar een beetje ingewikkeld is, bedenk je een naam voor een methode die je graag zou willen hebben. Daarna ga je die methoden e en voor e en schrijven, waarbij je alvast weer methoden aanroept, ook al heb je die nog niet geschreven. Daar ga je mee door tot de methoden die je nog moet schrijven zo simpel zijn dat je ze direct kunt schrijven. Sleutelzinnetje: niet vergeten om deze methode straks nog te schrijven. Bottom-up ontwerp. Je begint met de simpelste klasse (in de casus in dit hoofdstuk is dat Deeltje), en schrijft zo veel mogelijk methoden waarvan je denkt dat ze nog wel eens nuttig zullen zijn (zoals doeStap). Daarna ga je verder met de wat complexere klassen (in het voorbeeld: Ruimte), waarbij je de zojuist geschreven klasse goed kunt gebruiken. Zo ga je verder, totdat je de allesomvattende extensie van Applet kunt schrijven. Sleutelzinnetje: wat zou je met dit soort objecten willen kunnen doen? Bij het behandelen van de casus hebben we beide strategie en door elkaar heen gebruikt (en zo gaat het in de praktijk ook vaak). We zijn top-down begonnen met de klasse Simulatie. Maar halverwege zijn we overgeschakeld op bottom-up ontwerp: de methoden van Deeltje hebben we geschreven voordat we ze nodig hadden in de methoden van Ruimte.

9.5

Klassen in de Java-libraries

Niet het wiel opnieuw uitvinden Er zijn in de Java-bibliotheken een heleboel klassen beschikbaar voor gebruik. Voordat je zelf een klasse begint te schrijven, is het handig om je eerst te ori enteren of iets dergelijks al bestaat; het kan je een boel werk schelen. We geven een overzicht van een aantal klassen in de bibliotheken, met steeds enkele methoden die in die klasse gedenieerd zijn. Het overzicht is verre van volledig, want dat zou een wel erg saaie opsomming worden. Voor een totaal-overzicht kun je beter de online help raadplegen.

9.5 Klassen in de Java-libraries

117

Klassen van echte objecten Om te beginnen zijn er een aantal klassen die concrete objecten in de echte wereld beschrijven. Bijvoorbeeld: String: een tekst, bestaande uit lettertekens en/of andere symbolen zoals die op het toetsenbord te vinden zijn. int length(): bepaalt de lengte van de string String substring(int x,int y): selecteert een deel van de string, aangegeven door twee posities, en levert die op als resultaat String concat(String s): plakt een tweede string erachter, en levert dat op als resultaat char charAt(int n): bepaalt welk symbool er op een bepaalde posititie staat boolean equals(String s): vergelijkt de string letter-voor-letter met een andere string GregorianCalendar: een tijdstip in geschiedenis of toekomst, tot op de seconde precies (de naam is een beetje misleidend, want een object met type GegorianCalendar is niet een kalender, maar een tijdstip volgens die kalender). void set(int j, int m, int d): zet de datum op de gespeciceerde jaar, maand en dag. int get(int x): vraag de waarde van het gespeciceerde item. Je kunt het gewenste item aanduiden met constanten als Calendar.YEAR, Calendar.DATE enzovoorts. Point: een punt in de twee-dimensionale ruimte void setLocation(int x, int y): verplaats het punt double getX(): kijk wat de x-co ordinaat is Klassen van computer-gerelateerde objecten Veel klassen beschrijven een object dat iets te maken heeft met de computer. Al naar gelang je smaak kun je dit natuurlijk ook objecten in de echte wereld noemen. BufferedImage: een plaatje void setRGB(int x, int y, int k): geef een punt van het plaatje een kleur int getRGB(int x, int y): kijk welke kleur een punt heeft Graphics getGraphics(): lever een Graphics-object, waarmee je in het plaatje kunt tekenen, gebruikmakend van de methoden die Graphics biedt AudioClip: een geluidfragment void play(): speel het geluid af void loop(): speel het geluid steeds opnieuw af void stop(): stop met afspelen File: een al of niet bestaande le of directory, aangeduid door een complete padnaam String getName(): levert alleen de naam, zonder directory File getParentFile(): levert de directory waar de le in zit boolean exists(): kijk of de le echt bestaat void delete(): zorg dat hij hierna zeker niet meer bestaat Klassen van interactiecomponenten Bij het opbouwen van een grasche userinterface kun je gebruik maken van een aantal klassen die interactie-componenten beschrijven. Objecten met deze klassen als type kun je toevoegen aan een applet door aanroep van add. Button: een knop die de gebruiker kan indrukken TextField: een veld waar de gebruiker tekst kan invullen TextArea: een meer-regelig invoerveld Scrollbar: een door de gebruiker verschuifbare regelaar Label: een door de gebruiker niet te wijzigen tekst Checkbox: een vakje dat de gebruiker kan aankruisen Sommige klassen zijn niet in de eerste plaats bedoeld om objecten van te cre eren, maar eerder eren. In de extensie is het om een extensie van te maken, en daarvan vervolgens objecten te cre de bedoeling dat je enkele methoden herdenieert, zodat het object zich naar jouw wensen gaat gedragen. Voorbeelden zijn:

118

Objecten en klassen

Applet: een compleet toepassingetje, voor gebruik in een web-browser. Herdenieer de methoden init en paint. Canvas: een schilderslinnen, te gebruiken als interactie-component. Herdenieer paint. Klassen van objecten die iets voor je doen Sommige klassen beschrijven objecten waar je je niet direct iets bij kunt voorstellen, maar die wel handige methoden kennen die je goed kunt gebruiken in programmas. Beschouw dit soort objecten maar als apparaatjes, die een kunstje voor je kunnen vertonen. Voorbeelden zijn: Graphics: een tekendoos met handige methoden zoals void drawLine(int x0, int y0, int x1, int y1): trek een lijn tussen twee punten void fillRect(int x, int y, int b, int h): vul de rechthoek met de gespeciceerde positie, breedte en hoogte Scanner: een snijapparaat om een String in stukjes te snijden op aangegeven punten (standaard bij de spaties) String next(): geef het eerstvolgende plakje int nextInt(): geef het eerstvolgende plakje, en converteert die naar een integer boolean hasNext(): kijk of er nog plakjes zijn Scanner useDelimiter(String s): splits voortaan op iets anders dan spaties

Opgaven
9.1 Vergeten add Bekijk de situatie-schetsen in sectie 9.1. Wat kun je opmerken over het Button-object als de aanroep this.add(reset) per ongeluk uit het programma is weggelaten? 9.2 Lichtkrant Schrijf een applet genaamd Lichtkrant. Deze laat een tekst langzaam door het beeld bewegen, van rechts naar links. Als de tekst helemaal uit beeld is verdwenen, begint hij weer opnieuw. De lengte van een String kun je bepalen door aanroep van length. Ga ervan uit dat elke letter gemiddeld 10 beeldpunten breed is. Gebruik een Thread-object voor de animatie! De tekst die getoond wordt, moet als parameter in de html-le kunnen worden gespeciceerd. (Je kunt de applet dan voor allerlei verschillende teksten gebruiken). 9.3 Simulatie-programma a. Bij het cre eren van een nieuw Thread-object kun je ook iets anders dan this meegeven. Welk object zou je in het Simulatie-programma nog meer kunnen gebruiken? Wat zijn daarbij de voorwaarden? In welke situatie zou dat handig zijn? b. De Ruimtes in de Simulatie zijn wrijvingsloos: eenmaal bewegende deeltjes blijven onbeperkt door bewegen. Wat zou je moeten veranderen om de aanwezigheid van wrijving te modelleren?

119

Hoofdstuk 10

Overerving
10.1 Subklassen
Subklasse: toevoegen van nieuwe variabelen/methoden Door gebruik te maken van klasse-bibliotheken kun je je veel werk besparen. Des te jammerder is het dan ook, als er in de bibliotheek een klasse zit die bijna doet wat je wilt, maar net niet helemaal. Of je hebt een vergelijkbare klasse al eens eerder geschreven, maar ditmaal wil je een kleine uitbreiding aan de klasse maken. Wat te doen? Vroeger, dat wil zeggen in de tijd van voor de object-geori enteerde talen (Pascal, C) maakte men in zon geval een kopie van het eerdere programma, om daarin vervolgens aan te passen wat er aangepast moest worden. Dat scheelde in ieder geval een boel tikwerk. Toch had deze knipen-plak-strategie een belangrijk nadeel: als het oorspronkelijke programma later verbeterd werd (fouten hersteld, of de snelheid verhoogd), dan moest die verbetering in de gewijzigde kopie apart doorgevoerd worden. En als die kopie ook alweer gebruikt was als uitgangspunt voor weer een andere versie, dan moest ook die versie weer aangepast worden. Er ontstaat, kortom, een versie-probleem: verschillende versies groeien uit elkaar en kunnen niet gemakkelijk met elkaar in overeenstemming worden gebracht. Een kleine verbetering in het oorspronkelijke programma (bijvoorbeeld: een jaartal opslaan met vier cijfers in plaats van twee) kan met zich mee brengen dat vele duizenden zoveelste-generatie gewijzigde kopie en apart ook gewijzigd moeten worden. Zoals bekend kunnen de kosten daarvan aardig oplopen. . . Hoe moet het dan wel? Als je een uitbreiding wilt maken van een eerder geschreven klasse, dan moet je er niet een kopie van maken en die veranderen, maar kun je beter de verandering beschrijven in een extensie van de klasse. Dat gebeurt door in de klasse-header het woord extends te schrijven, met daarachter de naam van de klasse waarvan je een uitbreiding wilt maken. We hebben daar al veel voorbeelden van gezien:
class Hallo extends Applet class Simulatie extends Applet class Ruimte extends Canvas {... } {... } {... }

Met extends maak je subklassen De uitgebreide klasse wordt ook wel een subklasse van de oorspronkelijke klasse genoemd. De oorspronkelijke klasse heet, omgekeerd, de superklasse van de extensie. Een klasse kan maar e en (directe) superklasse hebben, maar een klasse kan meerdere subklassen hebben. Bijvoorbeeld, de klasse Applet heeft zowel de klasse Hallo als de klasse Simulatie als subklassen, maar Hallo heeft maar e en superklasse (namelijk Applet). Het is wel zo, dat je van de subklasse opnieuw weer een uitbreiding kunt maken: een sub-subklasse dus van de oorspronkelijke klasse. Die sub-subklasse heeft e en directe superklasse, maar is indirect ook een subklasse van de super-superklasse. Er kan dus een hele stamboom ontstaan van klasseafstammelingen. De objecten van een subklasse kunnen tevens worden opgevat als object van de superklasse. Een Ruimte-object bijvoorbeeld (zoals gedenieerd in het vorige hoofdstuk) is ook een Canvas-object. Maar het is niet zomaar een Canvas-object, nee, het is een heel bijzonder Canvas-object; eentje namelijk met gekleurde bolletjes erop. Het wiskundige jargon voor dit wat overdreven geformuleerde zinnetje is: een Ruimte-object is een bijzonder geval van een Canvas-object.

120

Overerving

Overerving van methoden en variabelen Objecten van de subklasse mogen de methoden gebruiken van de superklasse, en van de eventuele erfd van de supersuperklasse daar weer van, enzovoorts. Die methoden worden, zoals dat heet, ge klasse. Hetzelfde geldt voor de variabelen die in de superklasse zijn gedeclareerd: die zijn ook in elk object van de subklasse aanwezig. Zo kan het gebeuren dat je in subklassen van Applet gewoon de methode add kunt aanroepen, ook al is die helemaal niet apart gedenieerd in de subklasse. De methode is namelijk wel bekend in de klasse Applet, en wordt daar door de subklassen dus van ge erfd. Bij het opzoeken van methoden in de handleiding is dit wel iets om op te letten: zie je een methode niet staan in een overzicht van methoden bij de klasse waar je hem had verwacht, dan betreft het waarschijnlijk een ge erfde methode van een superklasse. De methode add bijvoorbeeld, zul je in de klasse Applet tevergeefs zoeken: hij wordt gedenieerd in de super-superklasse van Applet, dat is Container. Een ander voorbeeld waar overerving een rol kan spelen, nu eens niet in verband met Applet en dergelijke, maar gewoon in je eigen klassen. Stel dat je een klasse Bolletje hebt gemaakt. Zon Bolletje heeft een positie en een doorsnede. Er is een methode zetPlaats, een methode groei en een methode teken gedenieerd in de klasse:
class Bolletje { int x, y, diam; void zetPlaats(int x0, int y0) { x = x0; y = y0; } void groei() { diam++; } void teken(Graphics gr) { gr.fillOval(x,y,diam,diam); } }

Later kun je een subklasse KleurBol maken, waarin de klasse Bolletje wordt uitgebreid met een kleur, en een methode zetKleur om de kleur vast te leggen:
class KleurBol extends Bolletje { Color kleur; void zetKleur(Color k) { kleur = k; }

Heb je nu een object van type KleurBol, dan kun je daar de nieuwe methode zetKleur van aanroepen, maar ook nog steeds de methode zetPlaats en groei. Een KleurBol-object is tenslotte nog steeds een Bolletje, en voor het groeien en plaatsen van een bolletje maakt de eventuele kleur niet uit. Omgekeerd kan het natuurlijk niet: je kunt niet met zomaar een bolletje de methode zetKleur aanroepen, want zon bolletje heeft geen kleur. (Tenzij het toevallig een heel bijzonder bolletje is, namelijk een gekleurd bolletje, maar daar kun je niet zomaar op rekenen). Herdenitie van methoden Bij het erven van de methode teken door de klasse KleurBol ontstaat er toch een probleem. Een gekleurd bolletje moet namelijk op een andere manier getekend worden dan een gewoon bolletje: bij het tekenen speelt de kleur immers een rol. Voor dit soort situaties is het toegestaan om in subklassen een methode een andere denitie te geven dan de oorspronkelijke, dus om de methode te herdeni eren. In de klasse KleurBol kunnen we de volgende herdenitie van teken zetten:
void teken(Graphics gr) { gr.setColor(kleur); gr.fillOval(x,y,diam,diam); } }

10.1 Subklassen

121

Het nog-niet-uitgebreide object super Het is wel jammer dat je, zodra je besluit om een methode te herdeni eren, je de hele body opnieuw moet schrijven. In de hergedenieerde versie van teken moest de aanroep van fillOval bijvoorbeeld opnieuw worden opgeschreven. Nou valt dat hier nog wel mee, maar als het tekenen ingewikkelder was geweest (bijvoorbeeld gekleurde huisjes in plaats van bolletjes) is dat toch zonde van het werk, en wat erger is: gevoelig voor versie-problematiek. Je zou eigenlijk bij de herdenitie van de methode teken de oorspronkelijke methode willen kunnen aanroepen. Dat kan echter niet met
this.teken(gr);

want dan roep je de eigen methode aan, die dan weer zichzelf aanroept, die dan weer zichzelf aanroept, die dan weer zichzelf aanroept. . . Nee, dat willen we niet. Als uitweg in deze situatie is er in Java een speciale constante beschikbaar, genaamd super. Dit is net zoiets als this: het is een verwijzing naar het huidige object. Maar met dit verschil: super wijst naar het huidige object, alsof hij het type van de superklasse heeft. De herdenitie van teken kan dus als volgt verlopen:
void teken(Graphics gr) { gr.setColor(kleur); super.teken(gr); }

Polymore Objecten van een subklasse zijn acceptabel op plaatsen waar een object van die superklasse wordt verwacht. Dat geldt bijvoorbeeld voor parameters. Stel dat er een methode bestaat die een Bolletje als parameter wil hebben misschien omdat hij hem wil laten groeien:
void laatGroeien(Bolletje bol) { bol.groei(); }

In een ander deel van het programma hebben we zowel een Bolletje als een KleurBol beschikbaar:
Bolletje saai; KleurBol mooi; saai = new Bolletje(); mooi = new KleurBol();

We kunnen nu laatGroeien aanroepen met elk van beide objecten:


laatGroeien(saai); laatGroeien(mooi);

Omgekeerd is het niet toegestaan. Een methode als


void maakRood(KleurBol kb) { kb.zetKleur(Color.RED); }

kan niet worden aangeroepen met zomaar een bolletje (die immers niet gekleurd kan worden), maar natuurlijk wel met een KleurBol (die dat wel kan):
maakRood(saai); maakRood(mooi); // fout // goed

De leukste situatie is, als er een hergedenieerde methode wordt aangeroepen. Bekijk de volgende variant op laatGroeien:
void tekenEenBolletje(Bolletje bol) { bol.teken(ergens); }

die we aanroepen met zowel het gewone bolletje als met de kleurbol:
tekenEenBolletje(saai); tekenEenBolletje(mooi);

Het gewone bolletje wordt gewoon getekend, maar de KleurBol wordt netjes gekleurd getekend! Daar is die methode tekenEenbolletje mooi ingetrapt. Hij denkt van zomaar een Bolletje de

122

Overerving

teken-methode aan te roepen, maar als die Bolletje-parameter een KleurBol blijkt te zijn (wat mag, want dat is een subklasse van Bolletje), dan wordt daar wel de hergedenieerde versie van teken voor aangeroepen. En dat komt in de meeste gevallen goed uit. Dit verschijnsel heet polymore. De parameter kan letterlijk vele vormen aannemen (poly=veel, morf=vorm), en in elk van de gevallen wordt precies de juiste versie van de methode teken aangeroepen. Final methoden Als je niet wilt dat een programmeur van een subklasse jouw functie kan herdeni ren, kun je een methode final deni eren. Final methodes kunnen niet in een subklasse een nieuwe invulling krijgen. Vergelijk dit met nal variabelen, die geen nieuwe waarde kunnen krijgen.

10.2

Klasse-hi erarchie en

Extends: is een Met subklassen kun je de objecten in de wereld proberen te ordenen. Als je het woord extends daarbij leest als is een, kun je controleren of je geen denkfouten hebt gemaakt. Hier volgt een mogelijkheid om vervoermiddel-objecten te modelleren met behulp van allerlei klassen, die elkaars subklassen zijn.
class class class class class class class class class class class Vervoermiddel Voertuig Vliegtuig Boot MotorVoertuig Fiets MotorBoot ZeilBoot StoomBoot Auto VrachtWagen extends extends extends extends extends extends extends extends extends extends Vervoermiddel Vervoermiddel Vervoermiddel Voertuig Voertuig Boot Boot MotorBoot MotorVoertuig Auto

Met dit soort klasse-denities ontstaat een hele hi erarchie van klassen. Bij elk van de subklassen kunnen attributen en/of methoden worden toegevoegd, die alleen maar relevant zijn voor de betreende klasse, en de subklassen daarvan. Bijvoorbeeld, de variabele aantalWielen hoort typisch thuis in Voertuig en niet in Vervoermiddel, want boten hebben geen wielen. De variabele vliegHoogte hoort thuis in Vliegtuig, en de boolean variabele belWerkt in de klasse Fiets. Zon hi erarchie van klassen kun je overzichtelijk in een schema weergeven:
Vervoer middel Voertuig Motor Voertuig Auto Motor Fiets Fiets Vliegtuig Boot Motor Boot Zeil Boot Stoom Boot Vracht Wagen

Bij het maken van een indeling in subklassen moet je als ontwerper allerlei beslissingen nemen. Er is niet e en goede onderverdeling, maar afhankelijk van de toepassing kan een bepaalde onderverdeling handiger uitpakken dan een andere. In het voorbeeld hebben we bijvoorbeeld gekozen om de klasse Vervoermiddel eerst onder te verdelen naar het medium waarover het zich voortbeweegt: land, lucht of water. Pas daarna worden de klassen in verdere subklassen onderverdeeld: gemotoriseerd of niet. Dat had ook andersom gekund. Voor sommige klassen is het ook niet altijd even duidelijk op welke plaats ze in de hi erarchie thuishoren: is een MotorFiets een bijzonder soort Fiets (namelijk met een motor), of een bijzonder soort MotorVoertuig (namelijk met weinig wielen)?

10.3 Klasse-hi erarchie en in de Java-libraries

123

De onderlinge ligging van de klassen is echter wel altijd duidelijk. Een VrachtWagen is een Auto, maar een Auto is (lang niet altijd) een VrachtWagen. Een Fiets is een Voertuig, maar niet elk Voertuig is een Fiets. Membervariabelen: heeft een Werkend met diagrammen moet je wel uitkijken waar het eigenlijk over gaat. Er is nog een ander soort diagram dat wel eens getekend wordt in verband met objecten. Het gaat dan om welke objecten onderdeel uitmaken van andere objecten. Bijvoorbeeld in het Simulatie-programma in het vorige hoofdstuk:

Dit diagram moet je heel anders interpreteren. De lijnen moet je ditmaal van boven naar beneden lezen, met de uitspraak heeft een. Het diagram is heel bruikbaar om een overzicht te krijgen van de samenhang tussen de membervariabelen in een bepaald programma, maar het is geen klassehi erarchie. Een Deeltje is namelijk helemaal geen Ruimte (maar het maakt er onderdeel van uit). In dit diagram stellen de vakjes dan ook objecten voor, en geen klassen. Dat maakt ook dat (objecten met dezelfde) klassen meermalen kunnen voorkomen in het diagram, iets wat in een klasse-hi erarchie ook al onmogelijk is.

10.3

Klasse-hi erarchie en in de Java-libraries

Interface-componenten Veel klassen uit de Java-library zijn geordend in hi erarchie en, vergelijkbaar met de hi erarchie van vervoermiddellen uit de vorige sectie. Alle interactie-componenten, waaruit een grasche userinterface is opgebouwd, zijn bijvoorbeeld direct of indirect subklasse van de klasse Component.
Component Container Button Canvas Label Scrollbar Text Component TextArea TextField Panel Window Applet Frame Dialog FileDialog

java.awt

Methoden die op alle interactie-componenten van toepassing zijn, worden gedenieerd in de klasse Container. Voorbeelden daarvan zijn setBackground (om de achtergrondkleur te veranderen) en getBounds (om de afmetingen op te vragen). Methoden die alleen van toepassing zijn op componenten waarin de gebruiker iets kan intikken, zoals getText, zijn gedenieerd in de klasse TextComponent. Methoden die te maken hebben met het onderverdelen van de ingetikte tekst in meerdere regels, zoals getRows, zijn geplaatst in TextArea, omdat ze niet van toepassing zijn op TextFields. Event-listeners Om te reageren op de acties van de gebruiker, kun je aan interactie-componenten een eventlistener koppelen. Verschillende soorten componenten hebben verschillende soorten event-listeners: een Button en een TextField kunnen een ActionListener hebben, maar een Scrollbar heeft een AdjustmentListener. Er zijn verschillen tussen een ActionListener en een AdjustmentListener, maar ze hebben ook dingen gemeenschappelijk. Dat maakt dat het een goed idee is om ze in een hi erarchie onder te

124

Overerving

brengen. Informeel hebben we dat al gedaan, door ze samenvattend event-listener te noemen. Dat is inderdaad de naam van de superklasse.
Event Listener Action Listener Adjustment Listener Component Listener Mouse Listener MouseMotion Listener

Strikt genomen zijn EventListeners geen klassen, maar interfaces: er zitten geen methoden in, maar slechts methode-headers, dat wil zeggen wensenlijstjes die door andere klassen ge mplementeerd erarchie geordend. (Om het onderscheid toch te moeten worden. Maar ook interfaces zijn in een hi honoreren hebben we ze in de schets van de hi erarchie een andere vorm en kleur gegeven). Events Een event-listener zet andere objecten aan het werk door het aanroepen van methoden als actionPerformed of adjustmentValueChanged. Als parameter wordt daarbij een object meegegeven waarin nadere details over de gebeurtenis die is opgetreden worden meegedeeld. Het type van dat object verschilt bij de diverse methoden, maar al die types hebben iets gemeenschappelijks: ze beschrijven een event. De klassen die de types beschrijven zijn dan ook ondergebracht in een klasse-hi erarchie:
Event Object AWT Event Action Event Component Event Adjustment Event Focus Event Input Event Mouse Event Key Event

java.awt.event

Alles in e en hi erarchie Hoe hoger je in de hi erarchie kijkt, des te algemener is een beschrijving van de klasse. Het is vaak ook lastig om het onder woorden te brengen zonder al te vaag te worden. Probeer maar eens uit te leggen wat een interactie-component is, zonder voorbeelden te geven (dingen zoals buttons en scrollbars enzo). Het is de sport om toch zoveel mogelijk overeenkomsten te zoeken tussen klassen die op het eerste gezicht weinig met elkaar te maken hebben. Wat is het gemeenschappelijke van een String en een Component? En wat van een Image en een ActionEvent? Niet zo heel erg veel, maar wel: het zijn allemaal beschrijvingen van een object. En dat is al reden genoeg om ze een gemeenschappelijke superklasse te geven, die dan ook, heel toepasselijk, Object heet.
Object String

Component Event Object

De methoden van de klasse Object zijn noodzakelijkerwijs erg algemeen. We zullen er hier twee noemen: clone: maakt een exacte kopie van het object toString: converteert het object naar een leesbare String

10.3 Klasse-hi erarchie en in de Java-libraries

125

De methode toString wordt automatisch aangeroepen als je een object met de +-operator aan een string probeert te plakken. In veel klassen wordt de methode toString daarom hergedenieerd, zodat objecten van die klasse wanneer dat nodig is, gemakkelijk in tekst-vorm op het scherm kunnen worden getoond.

Opgaven
10.1 Type van add Iemand schrijft:
b = new Button("druk hier"); t = new TextField(10); this.add(b); this.add(t);

Hoe kan het correct zijn dat add zowel een Button als een TextField als parameter accepteert? Wat is het type van de parameter van add? 10.2 Veel objecten met actionlisteners In de methode actionPerformed kun je te weten komen welke button of texteld de actie heeft veroorzaakt. (Hoe ging dat ook alweer?) In een programma zijn er tien tekstvelden en een button. De button is gedeclareerd als membervariabele met de declaratie Button b; . Als de gebruiker op de button drukt, moet het programma stoppen door aanroep van de statische methode exit uit de klasse System. Als de gebruiker op Enter drukt in e en van de tien tekstvelden, moet de inhoud van dat tekstveld worden veranderd: de tekst moet een hoofdletter-versie worden van de tekst die is ingetikt. Schrijf de methode actionPerformed die dit doet. 10.3 Klasse-hi erarchie en In sectie 10.2 staat een klassehi erarchie van vervoermiddelen. Maak een dergelijke hi erarchie voor e en (of meer) van onderstaande toepassingsgebieden naar keuze. Neem daarbij zo nodig ontwerpbeslissingen: welke eigenschap vind je het belangrijkste, om als eerste naar onder te verdelen? Scheikunde: chemische elementen Biologie: levende organismen Taalkunde: natuurlijke talen Informatica: computertalen Zijn er klassen die zich niet goed in de hi erarchie laten onderbrengen? Waarom niet? 10.4 Type-controle Worden de types van expressies over het algemeen gecontroleerd door de Java-compiler of door de bytecode-interpreter? Er is een uitzondering op deze regel. In welk geval is dat? 10.5 Repaint en update Tot nu toe hebben we altijd gezegd dat repaint de methode paint aanroept met een ergens vandaan gehaald Graphics-object als parameter. Dat is niet helemaal waar: eigenlijk roept repaint de methode update aan, met het Graphics-object als parameter. Die methode tekent eerst een grote witte rechthoek (zodat een eventueel nog aanwezige achtergrond uitgewist wordt), en roept vervolgens pas paint aan, waarbij het Graphics-object wordt doorgegeven. Als je snel achter elkaar repaint aanroept (bijvoorbeeld in een animatie) gaat het beeld hinderlijk knipperen. Hoe kun je dat verhelpen? 10.6 TextField als source van een ActionEvent Bekijk de volgende implementatie van actionPerformed:
public void actionPerformed(ActionEvent e) { TextField tf; tf = (TextField) e.getSource(); tf.setText("hallo"); }

Waarom staat er (TextField) in de toekenningsopdracht? Wat gaat er fout als je dit weglaat? Wat kan er toch nog fout gaan nu het er wel staat?

126

Hoofdstuk 11

Strings en Arrays
11.1 Strings en characters
De klasse TextArea In eerdere programmas gebruikten we TextField-objecten om de gebruiker waarden te laten invoeren. In zon TextField kan echter maar e en regel worden ingetikt. Zijn er meerdere regels nodig, dan kun je in plaats daarvan TextArea-objecten gebruiken. Je kunt deze objecten gebruiken voor invoer van de gebruiker, maar ook om resultaten aan de gebruiker te laten zien. In de klasse TextArea zitten onder andere de volgende methoden: TextArea(int r, int k): maakt een TextArea met het gespeciceerde aantal rijen en kolommen void setText(String s): zet een string in de TextArea String getText(): zet de tekst die door de gebruiker is ingetikt in een string void append(String s): voeg een tekst toe aan de tekst die al in de TextArea staat void setEditable(boolean b): speciceer of de gebruiker de tekst wel of niet mag veranderen. In tegenstelling tot een TextField kan een TextArea geen ActionListener hebben. Bij een TextField treedt de ActionListener in werking als de gebruiker op de Enter-toets drukt, maar in een TextArea is de Enter-toets nodig om naar de volgende regel te kunnen gaan. Om de gebruiker aan te laten geven dat de tekst volledig is ingetikt en verwerkt mag worden, is het daarom handig om naast een TextArea voor dit doel een Button te plaatsen. Toepassing: hoeveel letters ingetikt? Een eenvoudige toepassing biedt de gebruiker de gelegenheid om een tekst in te tikken in een TextArea. Zodra de gebruiker op een button drukt, wordt de lengte van de tekst getoond in een tweede TextArea. De klasse-header van een extensie Applet veronderstellen we bekend, dus we gaan direct kijken naar de methode init, waar de interactiecomponenten worden gemaakt:
public void init() { invoer = new TextArea(5, 40); uitvoer = new TextArea(2,40); knop = new Button("Tel"); this.add(invoer); this.add(uitvoer); this.add(knop); knop.addActionListener(this); }

In de methode die wordt aangeroepen als reactie op het indrukken van de Button pakken we de tekst uit de textarea, tellen hoeveel symbolen daar in zitten, en zetten het resultaat op in het tweede tekstveld:
public void actionPerformed(ActionEvent e) { String s; int n; s = invoer.getText(); n = s.length(); uitvoer.setText("je hebt " + n + " tekens getikt"); }

11.1 Strings en characters

127

Interessanter is het echter om behalve het totale aantal letters ook iets te zeggen over het aantal woorden dat is ingetikt. Om dat te kunnen doen, moeten we niet alleen de lengte van de string bekijken, maar ook de individuele symbolen daarvan, op zoek naar spaties. Daarvoor hebben we behalve length nog wat meer methoden uit de klasse String nodig. De klasse String In de klasse String zitten onder andere de volgende methoden: int length(): bepaalt de lengte van de string String substring(int x,int y): selecteert een deel van de string, aangegeven door twee posities, en levert die op als resultaat String concat(String s): plakt een tweede string erachter, en levert dat op als resultaat boolean equals(String s): vergelijkt de string letter-voor-letter met een andere string char charAt(int n): bepaalt welk symbool er op een bepaalde posititie staat Met de methode substring kun je een deel van de string selecteren, bijvoorbeeld de eerste vijf letters:
kop = s.substring(0,5); uitvoer.setText(kop + " waren de eerste vijf letters");

De telling van de posities in de string is een beetje merkwaardig: de eerste letter staat op positie 0, de tweede letter op positie 1, enzovoorts. Als parameters van de methode substring geef je de positie van de eerste letter die je wilt hebben, en de positie van de eerste letter die je net niet meer wilt hebben. Dus de aanroep s.substring(0,5) geeft de letters op positie 0, 1, 2, 3 en 4; met andere woorden de eerste 5 letters. Je kunt de eerste letter van een string te pakken krijgen met een aanroep als:
String voorletter; voorletter = s.substring(0,1);

Het resultaat is dan een string met lengte 1. Er is echter nog een andere manier om losse letters uit een string te pakken: de methode charAt. Het resultaat daarvan is niet een nieuw string-object, maar een losse waarde van het primitieve type char, die je dus direct in een variabele kunt opslaan:
char eerste; eerste = s.charAt(0);

Een van de voordelen van char boven string-objecten met lengte 1, is dat je char-waarden direct op gelijkheid kunt testen met de operator ==, terwijl dat bij strings enigzins onnatuurlijk met een aanroep van de methode equals moet gebeuren. Het primitieve type char Net als alle andere primitieve types kun je char-waarden opslaan in variabelen, meegeven als parameter aan een methode, opleveren als resultaatwaarde van een methode, onderdeel laten uitmaken van een object, enzovoorts. Er is een speciale notatie om constante char-waarden in het programma aan te duiden: je tikt gewoon het gewenste letterteken, en zet daar enkele aanhalingstekens omheen. Dit ter onderscheiding van String-constanten, waar dubbele aanhalingstekens omheen staan:
char sterretje; String hekjes; sterretje = *; hekjes = "####";

Tussen enkele aanhalingstekens mag maar e en symbool staan; tussen dubbele aanhalingstekens mogen meerdere symbolen staan, maar ook e en symbool, of zelfs helemaal geen symbolen. Geschiedenis van char Het aantal verschillende symbolen dat in een char-variabele kan worden opgeslagen is in de geschiedenis (en in verschillende programmeertalen) steeds groter geworden: In de jaren 70 van de vorige eeuwe dacht men aan 26 = 64 verschillende symbolen wel genoeg te hebben: 26 letters, 10 cijfers en 28 leestekens. Dat er op die manier geen ruimte was om hoofd- en kleine letters te onderscheiden nam men voor lief.

128

Strings en Arrays

In de jaren 80 werden meestal 27 = 128 verschillende symbolen gebruikt: 26 hoofdletters, 26 kleine letters, 10 cijfers, 33 leestekens en 33 speciale tekens (einde-regel, tabulatie, piep, enzovoorts). De volgorde van deze tekens stond bekend als ascii: de American Standard Code for Information Interchange. Dat was leuk voor Amerikanen, maar Fran caises, Deutsche Mitb urger, en inwoners van Espa na en de Fr-r denken daar anders over. In de jaren 90 kwam dan ook een codering met 28 = 256 symbolen in zwang, waarin ook de meest voorkomende land-specieke letters een plaats vonden. dos had een eigen codering, later kwam er een andere codering van ansi (American National Standards Institute), die werd overgenomen door de internationale standaarden-organisatie iso. Maar hoe moet dat met de Griekse en Cyrillische letters? En het Indiase Devangari-alfabet? En de Japanse Kanji-tekens? In de jaren 00 werd daarom de codering opnieuw uitgebreid tot een tabel met 216 = 65536 verschillende symbolen. Dat is voorlopig wel genoeg. Deze codering heet Unicode. De eerste 256 tekens van Unicode komen overeen met de iso-codering, dus die blijft ook gelden. In Java worden char-waarden opgeslagen via de Unicode-codering. Niet dat er veel computers zijn waarop je al deze tekens daadwerkelijk kunt weergeven, maar op deze manier hoeft de taal, en onze programmas tenminste niet te worden aangepast zodra dat wel het geval wordt. Aanhalingstekens Bij het gebruik van Strings en char-waarden is het belangrijk om de aanhalingstekens pijnlijk precies op te schrijven. Als je ze vergeet, is wat er tussen staat namelijk geen letterlijke tekst meer, maar een stukje Java-programma. En er is een groot verschil tussen de letterlijke string "hallo" en de variabele-naam hallo de letterlijke string "boolean" en de type-naam boolean de letterlijke string "123" en de int-waarde 123 de letterlijke char-waarde + en de optel-operator + de letterlijke char-waarde x en de variabele-naam x de letterlijke char-waarde 7 en de int-waarde 7 Informatici hebben iets met aanhalingstekens. Grapjes en woordspelingen die gebaseerd zijn op de verwarring die door het weglaten van aanhalingstekens ontstaat, zijn dan ook erg populair in kringen van informatici (en worden daarbuiten vaak helemaal niet begrepen). In de casus met de geheime sleutel in sectie 8.2 stond zon woordspeling. Had je die gezien? Speciale char-waarden Speciale lettertekens zijn, juist omdat ze speciaal zijn, niet op deze manier aan te duiden. Voor een aantal speciale tekens zijn daarom aparte notaties in gebruik, gebruik makend van het omgekeerdeschuine-streep-teken (backslash): \n voor het einde-regel-teken, \t voor het tabulatie-teken. Dat introduceert een nieuw probleem: hoe die schuine streep dan zelf weer aan te duiden. Dat gebeurt door twee schuine strepen achter elkaar te zetten (de eerste betekent: er volgt iets speciaals, de tweede betekent: het speciale symbool is nu eens niet speciaal). Ook het probleem hoe de aanhalingstekens zelf aan te duiden is op deze manier opgelost: \\ voor het backslash-teken, \ voor het enkele aanhalingsteken, \" voor het dubbele aanhalingsteken. Er staan in deze gevallen dus weliswaar twee symbolen tussen de aanhalingstekens, maar samen duiden die toch e en char-waarde aan. Rekenen met char De symbolen in de Unicode-tabel zijn geordend; elk symbool heeft zijn eigen rangnummer. Het volgnummer van de letter A is bijvoorbeeld 65, dat van de letter a is 97. Let op dat de code van het symbool 0 niet 0 is, maar 48. Ook de spatie heeft niet code 0, maar code 32. Het symbool dat wel code 0 heeft, is een speciaal teken dat geen zichtbare representatie heeft. Je kunt het code-nummer van een char te weten komen door de char-waarde toe te kennen aan een int-variabele:

11.1 Strings en characters

129

char c; int i; c = *; i = c;

of zelfs direct
i = *;

Dit kan altijd; er zijn tenslotte maar 65536 verschillende symbolen, terwijl de grootse int meer dan 2 miljard is. Andersom kan de toekenning ook, maar dan moet je voor de int-waarde nog eens extra garanderen dat je accoord gaat met onverwachte conversies, mocht de int-waarde te groot zijn. Die garantie geef je door voor de int-waarde tussen haakjes te schrijven dat je er een char van wilt maken:
c = (char) i;

Je kunt op deze manier rekenen met symbolen: het symbool na de z is (char)(z+1), en de hoodletter c is de c-A+1-de letter van het alfabet. Deze garantie-notatie heet een cast. We hebben hem ook gebruikt om bij conversie van doublenaar int-waarde te garanderen dat we accoord gaan met afronding van het gedeelte achter de komma:
double d; int i; d = 3.14159; i = (int) d;

Toepassing: tellen van woordaantal We kunnen nu het voorbeeldprogramma aanpassen, zodat niet alleen het totaal aantal lettertekens wordt getoond, maar ook het aantal woorden. Om te beginnen plukken we daartoe weer de ingetikte tekst van het scherm:
public void actionPerformed(ActionEvent e) { String s; s = invoer.getText();

Het tellen is nu wat lastiger, omdat we alle indiviuele lettertekens van de string moeten inspecteren. Daarbij gaan we het totaal aantal spaties en regelovergangen tellen; daarmee worden woorden immers gescheiden. We declareren dus twee variabelen waarin we de telling gaan bijhouden, en een derde variabele waarmee we de positie in de string kunnen aangeven:
int spaties, regels, positie;

Met een for-opdracht kunnen we de variabele positie achtereenvolgens alle mogelijke posities in de string laten langslopen. Let op dat de telling begint bij 0, en doorloopt tot (en dus niet tot en met) de lengte van de string
spaties = 0; regels = 0; for (positie=0; positie<s.length(); positie++)

In de body van de for-opdracht pakken we door aanroep van charAt het symbool dat op de positie staat zoals aangeduid door positie, zodat we kunnen controleren of het een spatie of een regelovergang is:
{ } if (s.charAt(positie)== ) spaties++; if (s.charAt(positie)==\n) regels++;

Na aoop van de telling kunnen we de resultaten presenteren aan de gebruiker:


uitvoer.setText( spaties+regels + " woorden\n" + "op " + regels + " regels" ); }

Merk op dat we in de string het einde-regel-teken \n gebruiken om de tekst in het uitvoerveld over twee regels te spreiden.

130

Strings en Arrays

11.2

Arrays

Array: rij variabelen van hetzelfde type In sectie 11.4 gaan we een programma schrijven dat niet alleen het aantal spaties telt, maar dat de frequentie van ` elke letter bepaalt. De uitvoer wordt dus zoiets als 23 As, 7 Bs, 3 Cs, 8 Ds .... Zoals we in het vorige voorbeeld twee int-waarden gebruikten als teller voor het aantal spaties en het aantal regelovergangen, zo zouden we nu zesentwintig tellers kunnen maken: voor elke letter e en. Dat wordt een omvangrijke declaratie:
int as, bs, cs, ds, es, fs, gs, hs, is, js, ks, ls, enzovoort

en wat nog erger is: in de body van de for-opdracht zijn zesentwintig if-opdrachten nodig om al die letters te testen:
if if if if (s.charAt(positie)==a (s.charAt(positie)==b (s.charAt(positie)==c (s.charAt(positie)==d ) ) ) ) as++; bs++; cs++; ds++; // enzovoort

Gelukkig kan dat handiger. Er is een manier om in e en klap een heleboel genummerde variabelen te declareren. Als je zon variabele wilt gebruiken kun je die aanduiden met zijn volgnummer. Zon rij genummerde variabelen heet een array. Letterlijk dus: een rij. Creatie van arrays Een array heeft veel gemeenschappelijk met een object. Als je een array wilt gebruiken moet je, net als bij een object, een variabele declareren die een verwijzing naar de array kan bevatten. Voor een array met int-waarden kan de declaratie er zo uitzien:
int [] tabel;

De vierkante haken geven aan dat we niet een losse int-variabele declareren, maar een array van int-variabelen. De variabele tabel zelf echter is niet de array: het is een verwijzing die ooit nog eens naar de array zal gaan wijzen, maar dat op dit moment nog niet doet:
tabel

Om de verwijzing daadwerkelijk naar een array te laten wijzen, hebben we een toekenningsopdracht nodig. Net als bij een object gebruiken we een new-expressie, maar die ziet er ditmaal iets anders uit: achter new staat het type van de elementen van de array, en tussen vierkante haken het gewenste aantal:
tabel = new int[5];

Het array-object dat is gecre eerd bestaat uit een int-variabele waarin de lengte van de array is opgeslagen, en uit een aantal genummerde variabelen van het gevraagde type (in dit voorbeeld ook int). De nummering begint bij 0, en daarom is het laatste nummer e en minder dan de lengte. De variabelen in de array krijgen (zoals gebruikelijk bij membervariabelen) automatisch een neutrale waarde: 0 voor getallen, false voor booleans, en null voor objectverwijzingen. De situatie die hiermee in het geheugen ontstaat is:
tabel
5 0 0 0 0 0 length 0 1 2 3 4

Gebruik van array-waarden Je kunt de genummerde variabelen aanspreken door de naam van de verwijzing te noemen, met tussen vierkante haken het nummer van de gewenste variabele. Je kunt die de genummerde variabelen op deze manier een waarde geven:
tabel[2] = 37;

en je kunt de variabelen gebruiken in een expressie:

11.3 Toepassing: CirkelKlikker

131

Figuur 20: De applet CirkelKlikker in werking na 1, 3 en 16 kliks

x = tabel[2] + 5;

Het zijn, kortom, echte variabelen. De variabele length, die ook deel uitmaakt van de array, kun je wel gebruiken in een expressie, bijvoorbeeld
if (tabel.length < 10) iets

Je kunt deze variabele echter niet veranderen. De lengte van een eenmaal gecre eerde array ligt dus vast; je kunt de array niet meer langer of korter maken. De echte kracht van arrays ligt in het feit dat je het nummer van de gewenste variabele met een expressie kunt aanduiden. Neem bijvoorbeeld het geval waarin alle array-elementen dezelfde waarde moeten krijgen. Dat kan met een hele serie toekenningsopdrachten:
tabel[0] tabel[1] tabel[2] tabel[3] tabel[4] = = = = = 0; 0; 0; 0; 0;

maar dat is, zeker bij lange arrays, natuurlijk erg omslachtig. Je kunt de regelmaat in deze serie opdrachten echter uitbuiten, door op de plaats waar het volgnummer (0, 1, 2, 3 en 4) staat, een variabele te schrijven. In een for-opdracht kun je deze variabele dan alle gewenste volgnummers laten langslopen. De length-variabele kan mooi dienen als bovengrens in deze for-opdracht:
int nummer; for (nummer=0; nummer<tabel.length; nummer++) tabel[nummer] = 0;

11.3

Toepassing: CirkelKlikker

Geschiedenis bewaren We gaan een applet schrijven dat een cirkeltje tekent op alle punten die de gebruiker met de muis aanklikt. Bovendien tekent de applet de bounding box van deze cirkels: de kleinste rechthoek die alle cirkels bevat. Zie listing 19 voor de tekst van het programma, en guur 20 voor drie snapshots. Een eerste idee voor de aanpak van dit programma is om twee membervariabelen te declareren:
int x, y;

blz. 132

De methode mouseClicked geeft deze variabelen een waarde, en roept repaint aan om het scherm opnieuw te tekenen:

132 import java.awt.*; import java.awt.event.*; import java.applet.Applet;


5

Strings en Arrays

10

public class CirkelKlikker extends Applet implements MouseListener { final int MAXAANTAL = 100; final int DIAMETER = 15; final int STRAAL = DIAMETER/2; int aantal = 0; int [] xs, ys; private static int kleinste(int[]a, int n) { int res = a[0]; for (int t=1; t<n; t++) if (a[t] < res) res = a[t]; return res; } private static int grootste(int[]a, int n) { int res = a[0]; for (int t=1; t<n; t++) if (a[t] > res) res = a[t]; return res; } public void init() { xs = new int[MAXAANTAL]; ys = new int[MAXAANTAL]; this.addMouseListener(this); } public void paint(Graphics g) { for (int t=0; t<aantal; t++) { g.fillOval(xs[t]-STRAAL, ys[t]-STRAAL, DIAMETER, DIAMETER); } if (aantal>0) { int minX = CirkelKlikker.kleinste(xs,aantal)-STRAAL; int maxX = CirkelKlikker.grootste(xs,aantal)+STRAAL; int minY = CirkelKlikker.kleinste(ys,aantal)-STRAAL; int maxY = CirkelKlikker.grootste(ys,aantal)+STRAAL; g.drawRect( minX, minY, maxX-minX, maxY-minY ); } } public void mouseClicked(MouseEvent e) { if (aantal<MAXAANTAL) { xs[aantal] = e.getX(); ys[aantal] = e.getY(); aantal++; this.repaint(); } } public void mousePressed( MouseEvent e) {} public void mouseReleased(MouseEvent e) {} public void mouseEntered (MouseEvent e) {} public void mouseExited (MouseEvent e) {} private static final long serialVersionUID = 1; } Listing 19: CirkelKlikker/src/CirkelKlikker.java

15

20

25

30

35

40

45

50

55

11.3 Toepassing: CirkelKlikker

133

public void mouseClicked(MouseEvent e) { x = e.getX(); y = e.getY(); this.repaint(); }

De methode paint kan de cirkel dan op het scherm tekenen:


public void paint(Graphics g) { g.fillOval(x-5, y-5, 10, 10); }

Helaas, op deze manier werkt het programma niet goed. De methode paint begint namelijk elke keer met een schoon scherm, en daardoor zien we alleen maar het laatst aangeklikte punt. De eerder aangeklikte punten zijn verdwenen. Om de hele klik-geschiedenis te bewaren hebben we niet genoeg aan een losse x en y waarde: we moeten de co ordinaten van alle punten opslaan. Daarvoor gaan we twee arrays gebruiken. Een array vullen In plaats van de twee losse variabelen declareren we twee arrays:
int[] xs, ys

In de methode init worden de array-objecten gecre eerd. We moeten daarbij de lengte van de array vastleggen. Dat zet dus een bovengrens op het maximaal aanklikbare punten. Wat zullen we daarvoor nemen: 100? of 1000? We moeten een keus maken, maar om deze keus in een latere versie van het programma (CirkelKlikker Pro) gemakkelijk te kunnen herzien, maken we een final variabele die de grens vastlegt:
static final int MAXAANTAL = 100;

Met gebruikmaking van deze constante wordten de arrays gemaakt in init:


public void init() { xs = new int[MAXAANTAL]; ys = new int[MAXAANTAL]; }

Nu hebben we nog een variabele nodig die aangeeft tot hoe ver de arrays al in gebruik zijn. Aan het begin is er nog geen enkele waarde in gebruik, dus declararen en initialiseren we:
int aantal = 0;

Maar daar komt verandering in als er een punt wordt aangeklikt: op de eerste vrije plaats worden de co ordinaten van het aangeklikte punt bewaard, en daarna wordt het aantal opgehoogd.
public void mouseClicked(MouseEvent e) { xs[aantal] = e.getX(); ys[aantal] = e.getY(); aantal = aantal+1; this.repaint(); }

De methode paint kan nu alle waarden van de array langlopen, althans zo ver als nodig is volgens de variabel aantal. Daartoe gebruik paint een eigen tellertje t, die loopt van 0 tot aantal. In paint mag het aantal natuurlijk niet worden veranderd, want dan zou de administratie van aangeklikte punten niet meer kloppen.
public void paint(Graphics g) { for (int t=0; t<aantal; t++) { g.fillOval(xs[t]-5, ys[t]-5, 10, 10); } }

Zoeken van de grootste en kleinste waarde Nu moeten we de bounding box nog tekenen. Daarvoor hebben we de kleinste en grootste waarde nodig uit de twee arrays. Om het werk in paint niet te ingewikkeld te maken gaan we een methode schrijven die de kleinste waarde van een array zoekt. Die kunnen we dan twee keer aanroepen: een keer voor de array xs, en een keer voor ys.

134

Strings en Arrays

De methode kleinste krijgt een array als parameter. Dat gebeurt gewoon zoals je zou verwachten: een declaratie van een array in de header van de methode. Het is eigenlijk niet zozeer de array als geheel die wordt meegegeven, als wel de verwijzing ernaartoe. De array zelf blijft in het geheugen eerd. staan waar hij stond toen hij werd gecre De parameter krijgt bij aanroep van de methode een waarde; in de body methode wordt niet een new array gemaakt, want we willen juist werken met de array die de aanroeper heeft meegegeven. We mogen hopen dat de persoon die de methode aanroept inderdaad een verwijzing naar een bestaande array meegeeft (en niet de waarde null), en dat bovendien de elementen van de array al een waarde hebben. De situatie zou bijvoorbeeld als volgt kunnen zijn:
tabel
5 12 95 11 23 15 length 0 1 2 3 4

De tweede parameter van kleinste geeft aan tot hoe ver we de array willen doorzoeken.
static int kleinste(int[] tabel, int n)

Zolang we de waarde nog niet ge nspecteerd hebben, gaan we er voorlopig van uit dat de waarde met volgnummer 0 de kleinste is.
{ int resultaat; resultaat = tabel[0];

Met een for-opdracht lopen we nu alle elementen langs. Bij elk element controleren we of die misschien kleiner is dan wat we tot nu toe dachten dat de kleinste was. Als dat zo is, passen we de waarde van de resultaatwaarde aan.
int t; for (t=0; t<n; t++) if (tabel[t] < resultaat) resultaat = tabel[t];

Na aoop van de for-opdracht kunnen we de resultaatwaarde veilig opleveren, want die is getoetst aan alle waarden in de array.
return resultaat; }

Deze methode kunnen we nu twee keer aanroepen om de kleinste waarde van de twee arrays te bepalen. Omdat het een statische methode is, roepen we hem aan met de naam van ded klasse voor de punt:
int minX = CirkelKlikker.kleinste(xs, aantal); int minY = CirkelKlikker.kleinste(ys, aantal);

Op soortgelijke manier kunnen we een methode grootste schrijven, die de maximale arraywaarde bepaalt. Met behulp van de gevonden minima en maxima kunnen we de bounding box tenslotte gemakkelijk tekenen:
g.drawRect( minX, minY, maxX-minX, maxY-minY );

11.4

Toepassing: Tekst-analyse met letterfrequentie

Tellen van aparte letter-frequenties Nu kunnen we het in sectie 11.2 beloofde programma schrijven dat van elke letter apart telt hoe vaak hij in een tekst voorkomt. We gaan nog eens de methode actionPerformed herzien, voortbordurend op het voorbeeld eerder in dit hoofdstuk.
public void actionPerformed(ActionEvent e) { String s; int n; char c; s = invoer.getText();

11.4 Toepassing: Tekst-analyse met letterfrequentie

135

import java.applet.Applet; import java.awt.*; import java.awt.event.*;


5

public class Tekst extends Applet implements ActionListener { TextArea invoer, uitvoer; Button knop; public void init() { invoer = new TextArea(5,30); uitvoer = new TextArea(28,20); knop = new Button("Tel letters"); uitvoer.setEditable(false); this.add(invoer); this.add(knop ); this.add(uitvoer); knop.addActionListener(this); } public void actionPerformed(ActionEvent e) { TurfTab tabel; tabel = new TurfTab(); tabel.turf( invoer.getText() ); uitvoer.setText( tabel.toString() ); } private static final long serialVersionUID = 1; } Listing 20: Tekst/src/Tekst.java

10

15

20

25

Figuur 21: De applet Tekst in werking

136

Strings en Arrays

Voor de zestentwintig afzonderlijke tellertjes kunnen we nu een array gebruiken. Die moet worden gedeclareerd en met new worden gecre eerd:
int [] tabel; tabel = new int[26];

Nu kunnen we de string langslopen. Voor elk symbool dat we tegenkomen, verhogen we de bijbehorende teller in de array. Het volgnummer van de gewenste teller bepalen we door van de Unicode-code van het aangetroen symbool c, de Unicode-code van de constante A af te trekken. Daardoor wordt het aantal letters A bijgehouden in teller nummer 0, het aantal letters B in teller nummer 1, en het aantal letters Z in teller nummer 25. Voor hoofdletter en kleine letters treen we aparte maatregelen, andere symbolen worden genegeerd.
for (n=0; n<s.length(); n++) { c = s.charAt(n); if (c>=A && c<=Z) tabel[c-A]++; else if (c>=a && c<=z) tabel[c-a]++; }

Nadat de hele telling is uitgevoerd, kunnen we de resultaten op het scherm weergeven. Daartoe lopen we de hele array langs, ditmaal op volgorde met behulp van een for-opdracht. Voor elke teller voegen we een toepasselijke tekst op het scherm toe.
uitvoer.setText(""); for (n=0; n<26; n++) { c = (char)(n+A); uitvoer.append (c + ": " + tabel[n] + " keer\n" ); } }

Scheiden van inhoud en userinterface Het is al met al een behoorlijk ingewikkelde methode geworden. Wat het vooral zo onoverzichtelijk maakt, is dat er een aantal zaken door elkaar lopen: het van het scherm plukken van de string, creatie van de array, het eigenlijke telwerk, en de lay-out van het resultaat. Veel overzichtelijker zou het worden als we een deel van deze taken konden uitbesteden aan de methoden van het object. Het zou bijvoorbeeld erg handig zijn als er een klasse TurfTabel bestond, die we met de aanroep van een methode turf konden vragen om alle letters in een bepaalde string te turven. Als we daarna ook zouden kunnen vragen om het resultaat als string weer te geven, dan hadden we de hele methode in vier regels kunnen schrijven:
public void actionPerformed(ActionEvent e) { TurfTab telling; telling = new TurfTab(); telling.turf( invoer.getText() ); uitvoer.setText( telling.toString() ); }

blz. 135 blz. 137

Zon klasse TurfTabel bestaat natuurlijk niet. We kunnen hem voor de gelegenheid echter wel zelf maken. De verschillende deel-taken kunnen worden ondergebracht in aparte methoden: in de constructormethode: creatie en op-nul-zetten van de array in de methode turf: het eigenlijke telwerk in de methode toString: de layout van het resultaat We kunnen zelfs nog een vierde methode maken, die het turven van e en symbool voor zijn rekening neemt, rekening houdend met hoofdletters en dergelijk. In de methode turf kunnen we dan die methode aanroepen voor elk symbool dat in de string wordt aangetroen. In de andere klasse blijft, naast bovenstaande korte versie van actionPerformed, dan alleen nog het opbouwen van de userinterface over. Op deze manier is de communicatie met de gebruiker in e en klasse ondergebracht, en alle inhoudelijke zaken in de andere klasse. Zon scheiding van aandachtspunten is altijd een goede gewoonte. In listing 20 en listing 21 is de complete uitwerking van het programma weergegeven.

11.4 Toepassing: Tekst-analyse met letterfrequentie

137

class TurfTab { int [] tellers; int totaal;


5

public TurfTab() { tellers = new int[26]; }


10

15

20

private void turf(char c) { if (c>=A && c<=Z) { tellers[c-A]++; totaal++; } else if (c>=a && c<=z) { tellers[c-a]++; totaal++; } } public void turf(String s) { for (int i=0; i<s.length(); i++) turf( s.charAt(i) ); } public String toString() { String s = ""; for (int i=0; i<26; i++) s += (char)(i+A) + ": " + tellers[i] + " keer\n"; s += "totaal: " + totaal; return s; } } Listing 21: Tekst/src/TurfTab.java

25

30

35

138

Strings en Arrays

11.5

Syntax van arrays

Arrays van objecten De elementen van de array kunnen van elk gewenst type zijn. Dat kan een primitief type zijn zoals int, double, boolean of char, maar ook een object-type. Zo kun je bijvoorbeeld een array maken van Strings, van Buttons, van TextFields, of van objecten van een zelfgemaakte klasse, zoals Deeltje. Hier is een array met Deeltje-objecten, die hoofdstuk 9 gebruikt had kunnen worden in plaats van losse Deeltje-variabelen d1, d2 en d3:
Deeltje [ ]
deeltjes length 5 0 1 2

x y dx dy kleur

x y dx dy kleur

x y dx dy kleur

Deeltje

Deeltje

Deeltje

De verwijzings-variabele kan worden gedeclareerd met:


Deeltje[] deeltjes;

De eigenlijke array kan worden gecre eerd met


deeltjes = new Deeltje[3];

Maar pas op: hiermee is weliswaar het array-object gecre eerd, maar nog niet de individuele deeltjes! Dat moet apart gebeuren in een for-opdracht, die elk deeltje apart cre eert:
int t; for (t=0; t<deeltjes.length; t++) deeltjes[t] = new Deeltje();

Syntax van arrays De diverse notaties die arrays mogelijk maken zijn specieke bijzonderheden in de Java-syntax. De aanwezigheid van arrays is dan ook veel fundamenteler dan zomaar een extra klasse in de library. De mogelijkheid van het lege paar haken in een declaratie zoals
int [] tabel

wordt geschapen in het syntax-diagram van type. Na het eigenlijke type (een van de acht standaardtypen of een klasse-naam) kan er nog een paar vierkante haken volgen:

type
klasse

naam
boolean int double char
Hieruit blijkt ook dat het mogelijk is om meer dan e en paar haken te schrijven, wat de mogelijkheid biedt voor twee- of meerdimensionale tabellen. Het aanmaken van een array met new, en het opzoeken van een bepaalde waarde in de array wordt mogelijk gemaakt door de syntax van expressie:

byte short long float

[]

11.5 Syntax van arrays

139

expressie constante
prefix-operator

expressie expressie
? ( ( type )

infix-operator postfix-operator

expressie
: )

expressie expressie expressie


.

expressie

expressie
this super new

naam
(

expressie
, [

type

expressie

expressie ]

Let op het verschil tussen new-type gevolgd door iets tussen ronde haken (de aanroep van de constructormethode van een klasse) en new-type gevolgd door iets tussen vierkante haken (de aanmaak van een array, maar ingeval het een array van objecten is nog niet van de objecten in die array). Als je goed kijkt naar het syntax-diagam van declaratie, zie je dat ook achter de variabele-naam in een declaratie nog een leeg paar vierkante haken kan staan:

declaratie type naam


[] ,
Daardoor is het mogelijk om een array als volgt te declareren:
int tabel [];

; =

initialisatie

Dat is echter minder logisch dan dat de haken bij het type horen. In Java zijn beide notaties mogelijk; deze extra mogelijkheid is vooral bedoeld voor verstokte C-programmeurs die dit van oudsher zo gewend waren. Arrays versus strings Als de elementen van een array van het type char zijn, begint zon array verdacht veel op een Stringobject te lijken: immers, zowel in een array-van-char als in een String kan een tekst, bestaande uit meerdere symbolen, worden opgeslagen. Toch zijn er verschillen tussen een array-van-char en een String. De belangrijkste verschillen zijn: In een array kun je de individuele elementen nog veranderen met a[x]=. . . ; in een String kan dat niet. Van een String kun je daarentegen allerlei methoden aanroepen, zoals equals en substring. Bovendien kun je strings optellen met de +-operator. Met arrays kan dat allemaal niet. Er zijn ook een aantal overeenkomsten, hoewel de notatie voor sommige zaken voor een array en een string verschillend is: Zowel van een array als van een String kun je het zoveelste element opvragen, met respectievelijk de expressie a[x] en s.charAt(x) Zowel van een array als van een String kun je de lengte bepalen. Bij een array door de membervariabele length te bekijken: a.length, bij een String door de methode length aan te roepen: s.length(). Let op: bij een array zijn dus geen haakjes nodig, bij een String wel!

140

Strings en Arrays

Initialisatie van arrays We wijzen er nog eens op dat, als je een array declareert, je een verwijzing naar een array-object hebt, en nog niet de array zelf. Bijvoorbeeld bij de volgende array van strings:
String [] dagnamen;

onstaat de eigenlijke array pas door de toekenning:


dagnamen = new String[7];

Nu bestaat de array wel, maar die zeven strings hebben ieder nog geen waarde gekregen. Dat kan nog een heel gedoe worden:
dagnamen[0] dagnamen[1] dagnamen[2] dagnamen[3] dagnamen[4] dagnamen[5] dagnamen[6] = = = = = = = "maandag"; "dinsdag"; "woensdag"; "donderdag"; "vrijdag"; "zaterdag"; "zondag";

Gelukkig is er speciale notatie om arrays met constanten te initialiseren. Zelfs de new is dan niet meer nodig. De initialisatie moet meteen bij de declaratie gebeuren, door de elementen op te sommen tussen accolades:
String [] dagnamen = {"maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag", "zondag" };

Deze notatie hebben we te danken aan de syntax van initialisaties:

initialisatie expressie
{

initialisatie
,

De opsomming tusen accolades is dus niet een vorm van expressies, en kan dus ook niet gebruikt worden in een toekennings-opdracht. De opsomming is een vorm van initialisatie, en kan dus alleen maar gebruikt worden direct bij de declaratie. Handig is het wel. De notatie kan ook gebruikt worden voor de inititialisatie van arays met getallen:
int [] maandlengtes = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

en zelfs voor de initialisatie van arrays van arrays:


int [][] blok = { {1,2,3}, {4,5,6} };

Opgaven
11.1 Character escapes Hoeveel en welke tekens worden in het tekstveld uitvoer zichtbaar bij het uitvoeren van de opdracht:
uitvoer.setText( "\"/\\//\"/" );

Schrijf bovendien een opdracht die de broncode van de hierboven geciteerde opdracht toont op het tekstveld bron . 11.2 Strings vergelijken Twee strings s en t kunnen vergeleken worden met s==t en met s.equals(t). Wat is het verschil? Vul bovendien het juiste woord in in de volgende uitspraken, en licht het antwoord toe: als s==t dan geldt (altijd / soms / nooit) s.equals(t) als s.equals(t) dan geldt (altijd / soms / nooit) s==t 11.3 Uppercase In de klasse String zit de methode toUpperCase, die een hoofdletterversie van de string oplevert. Die kun je bijvoorbeeld aanroepen met
h = s.toUpperCase( );

11.5 Syntax van arrays

141

eren Als je zelf de klasse String zou moeten schrijven, hoe zou je deze methode dan kunnen deni (gebruikmakend van andere methoden in de klasse String)? 11.4 Zoek de grootste a. Schrijf een statische methode grootste met als parameter een array van doubles, met als resultaat de hoogste waarde die in de array voorkomt. b. Schrijf een andere statische methode plaatsGrootste, met als resultaat het volgnummer (de index in de array) van de grootste waarde. 11.5 Frequentie van de kleinste Schrijf een methode met als parameter een array van doubles. De methode moet opleveren hoe vaak de kleinste waarde van de array voorkomt. Bijvoorbeeld: als de array de waarden 9,12,9,7,12,7,8,25,7 bevat, dan is het resultaat 3, omdat de kleinste waarde (7) drie maal voorkomt. 11.6 Sorteren Maak een variant van de methode plaatsGrootste, waarbij niet de hele array doorzocht wordt, maar alleen de eerste n elementen, waarbij n een aparte parameter is. Gebruik deze methode in de volgende methode: schrijf een statische methode sorteer, met als parameter een array van doubles. Het resultaattype is void. Na aoop van het uitvoeren van de methode moeten de elementen in de array in opklimmende volgorde van grootte staan. Hint hierbij: zoek eerst de plaats van de grootste waarde in de hele array. Verwissel deze waarde met de waarde op de laatste plaats. Dan staat de grootste waarde alvast achteraan, waar hij hoort. Zoek nu de grootste waarde van het resterende deel van de array. Die waarde wordt verwisseld met de op e en na laatste waarde in de array. Dit gaat zo verder: zoek weer het grootste element van de array minus de laatste twee elementen, verwissel die met op het twee na laatste element, enzovoorts. 11.7 Zoek positie van een waarde Schrijf een statische methode met als parameter een array van integers en een losse integer, die oplevert op welke plaats de losse integer als eerste in de array voorkomt. Als het getal nergens voorkomt, moet de methode 1 opleveren. 11.8 Slim zoeken (moeilijker) Als je weet dat de array op volgorde gesorteerd is, kun je op een slimmere manier zoeken: kijk in het midden van de array of je daar de gezochte waarde aantreft. Zo ja, mooi. Zo nee, dan weet je of je in de helft links ervan of in de helft rechts ervan moet verder zoeken. Op deze manier wordt het te doorzoeken stuk van de array bij elke stap twee keer zo klein. Gebruik twee integers die de grenzen van het te doorzoeken stuk array aanduiden. 11.9 Histogram Schrijf een statische methode met een array van ints als parameter. De methode levert een lange string op, met daarin de regels van een staafdiagram. De array-elementen bevatten getallen van 1 tot en met 10. Deze string moet een staafdiagram bevatten van de waarden in de array. Bijvoorbeeld, als de array de waarden 6, 10, 8, 6, 6, 4, 8, 5, 5, 7, 6, 2, 4, 10, 9, 5, 6, 8, 7, 6 bevat moet het staafdiagram 10 regels bevatten:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: * ** *** ****** ** *** * ***

142

Hoofdstuk 12

Ontwerp van programmas


12.1 Layout van de userinterface
Layout managers De interactie-componenten die je door aanroep van add aan een applet toevoegt, komen allemaal naast elkaar op het scherm te staan. Past het er niet meer naast, dan gaat het verder op de rij daaronder. De ruimte die er voor het totale applet beschikbaar is, kan vari eren. Als je het programma uitvoert met appletviewer, kun je dat zelfs doen terwijl het programma loopt; als je het programma uitvoert met een web-browser, kun je de maten die in de html-le staan gespeciceerd veranderen. Verandert de afmeting, dan worden de interactie-componenten opnieuw gerangschikt: het zou kunnen zijn dat er nu meer of juist minder naast elkaar passen. Door de afmetingen slim te kiezen, kun je een nette layout afdwingen (zie bijvoorbeeld het programma Simulatie, waar de drie ruimtes met deeltjes net op de eerste rij passen, zodat de twee buttons op de rij daaronder komen te staan). De indeling gebeurt geheel automatisch, en wordt verzorgd door een zogenaamde layout manager, die aan de applet is gekoppeld. Ben je niet tevreden met de verzorging van de layout, dan kun je een andere layout manager uitkiezen. Dat gebeurt door aanroep van de methode setLayout van de klasse Container. Dat is een superklasse van Applet, dus deze methode is ook in applets beschikbaar. Als parameter geef je de gewenste layout manager mee, wat hoe kan het ook anders een object is. Bijvoorbeeld:
this.setLayout( new BorderLayout() );

Aangezien de layout manager verder in het programma niet expliciet nodig is, kan het gecre eerde object direct worden meegegeven aan setLayout, zonder het eerst in een variabele op te slaan. Beschikbare Layout managers Er zijn een viertal verschillende layout managers beschikbaar in de Java-bibliotheek. Ze zijn alle vier een implementatie van de interface LayoutManager, en dat is mooi, want dat verwacht de methode setLayout van zijn parameter. Het implementeren van een interface met implements is iets anders dan het maken van een subklasse met extends. In de klasse-diagrammen zullen we voor implements een stippellijn gebruiken, om het te onderscheiden van de doorgetrokken lijn die extends symboliseert:
Layout Manager Border Layout Flow Layout Grid Layout

De standaard layout manager is FlowLayout. Met een FlowLayout object als layout managers worden alle componenten van links naar rechts en van boven naar beneden naast/onder elkaar gezet; zie guur 22a. Met een GridLayout kun je componenten in nette rijen en kolommen weergeven. Als parameter van de constructor moet je opgeven hoeveel rijen en kolommen er zijn, en hoeveel pixels tussenruimte je tussen de rijen en kolommen wilt openhouden:
this.setLayout( new GridLayout(2,3,5,5) );

12.1 Layout van de userinterface

143

Figuur 22: Een panel met respectievelijk FlowLayout, GridLayout, BorderLayout en null layout.

Om de componenten in rijen en kolommen te kunnen indelen, verandert de layout manager de vorm van de componenten. Alle beschikbare ruimte wordt eerlijk verdeeld over de rijen en kolommen. Als er niet genoeg ruimte is voor een object, dan heb je pech (zie de knop met het opschrift kortschildkever in guur 22b). Een BorderLayout ordent de componenten langs de vier randen van het scherm, met een vijfde component in het midden. Er kunnen in dit geval maximaal vijf componenten worden gerangschikt. Bij elke aanroep van add moet worden gespeciceerd waar de component terecht moet komen. Dat gebeurt met een extra parameter bij add, die e en van de vijf daarvoor bestemde constanten uit de klasse BorderLayout moet zijn:
this.setLayout( new BorderLayout() ); this.add(new Button("koe"), this.add(new Button("varken"), this.add(new Button("kortschildkever"), this.add(new Button("kip"), this.add(new Button("olifant"), BorderLayout.NORTH BorderLayout.WEST BorderLayout.CENTER BorderLayout.EAST BorderLayout.SOUTH ); ); ); ); );

Bij deze layoutmanager krijgen Noord en Zuid zoveel hoogte als ze nodig hebben, maar worden ze in de breedte opgerekt. West en Oost krijgen zoveel breedte als ze nodig hebben, en worden in de lengte opgerekt. Alle overblijvende ruimte gaat naar het centrum; zie guur 22c. Een laatste alternatief is om helemaal geen layout manager te gebruiken. Om de default FlowLayout kwijt te raken, moet je dan null opgeven als layout manager. De consequentie is dat je van elke component door middel van een aanroep van setBounds moet aangeven waar hij op het scherm geplaatst moet worden. Dat geeft je complete vrijheid in de layout, zoals in guur 22d. Maar pas op: als de gebruiker nu de afmetingen van het window verandert, wordt de layout niet aangepast!

144 De init-methode bevat opdrachten als

Ontwerp van programmas

this.setLayout(null); b =new Button("koe"); b.setBounds(10,10,70,20); this.add(b); b =new Button("varken"); b.setBounds(25,50,50,30); this.add(b); b =new Button("kortschildkever"); b.setBounds(40,30,100,15); this.add(b);

De klasse Panel Als je wel de layout managers wilt gebruiken, maar je wilt toch een complexere layout bereiken, dan kun je gebruik maken van de klasse Panel. Dit is een component zoals alle andere, maar met een bijzondere eigenschap: je kunt er met add deel-componenten aan toevoegen. Bovendien kan een Panel-object een eigen layout-manager krijgen. Je kunt dus bijvoorbeeld het applet een BorderLayout geven, en in het centrum een Panel-object plaatsen die voor zijn inhoud een GridLayout gebruikt. In de casus in de volgende sectie gebruiken we dit mechanisme. Je kunt zelfs aan het Panel weer deel-Panels toevoegen, die er weer een andere layout op nahouden. Op deze manier kun je ingewikkelde dashboards opbouwen.

12.2

Toepassing: Rekenmachine

blz. 145 blz. 146

Beschrijving van de casus We gaan een eenvoudige 4-functie calculator maken. Voorlopig werkt de calculator alleen met integer getallen (maar dat kan eenvoudig aangepast worden). De gebruiker kan de calculator bedienen met buttons op het scherm, zoals in guur 23. Net als in het vorige hoofdstuk maken we weer twee klassen: een klasse Calc waar de userinterface wordt opgebouwd, en het indrukken van de knoppen wordt afgehandeld (zie listing 22); een klasse Proc waar al het inhoudelijke werk gebeurt, maar die juist niets met de userinterface te maken wil hebben (zie listing 23). De processor van de rekenmachine Voor het schrijven van de klasse Proc moeten we ons eerst afvragen welke variabele er in deze klasse nodig zijn. Met andere woorden: in welke toestand de processor zich kan bevinden. Om daar achter te komen, bekijken we in detail wat er gebeurt als de gebruiker de rekenmachine bedient. De gebruiker begint met een getal in te toetsen. Alle ingedrukte cijfertoetsen worden geaccumuleerd in een steeds groter wordend getal. Is het getal 12 al ingevoerd, en drukt de gebruiker op de 3-toets, dan wordt de nieuwe waarde 123; dat is 10 maal de oude waarde plus het ingedrukte cijfer. Daarna drukt de gebruiker een operator-toets in. Op het scherm verandert ogenschijnlijk niet (er blijft bijvoorbeeld 123 staan), maar de huidige waarde is blijkbaar anders geworden. Na het indrukken van de 4-toets verschijnt namelijk niet 1234, maar 4 op het scherm. De gebruiker gaat door met het intoetsen van het tweede getal, bijvoorbeeld 456, en drukt tenslotte op de =-toets. Op dat moment is de oude waarde (123) ineens weer van belang, want die moet worden opgeteld/vermenigvuldigd/enz. bij de nieuwe waarde. Bovendien is de eerder gekozen operator van belang, want na het indrukken van de =-toets moet de machine weten of er opgeteld of vermenigvuldigd moet worden. Voor de toestand van de rekenmachine zijn dus van belang: de huidige waarde de vorige waarde de waarde op het scherm (niet altijd gelijk aan de huidige waarde!) de laatst gekozen operator De klasse Proc krijgt dus vier membervariabelen: drie getallen en een char. Voor de getallen gebruiken we long-waarden in plaats van int-waarden, zodat de calculator getallen tot 18 cijfers aankan. Verder maken we methoden voor de diverse acties die op de calculator kunnen worden toegepast: De methode cijfer accumuleert een cijfer bij het huidige getal. De methode reken voert de berekening die de variabele operator aangeeft uit op het vorige getal en de huidige waarde. Het resultaat komt in de variabele scherm, en wordt ook de nieuwe vorige waarde, zodat het resultaat meteen als linker operand van de volgende operatie gebruikt kan worden.

12.2 Toepassing: Rekenmachine

145

import java.applet.Applet; import java.awt.*; import java.awt.event.*;


5

public class Calc extends Applet implements ActionListener { Label result; Panel knoppen; Proc proc; public void init() { Button knop; String opschrift;

10

15

proc = new Proc(); result = new Label( "0", Label.RIGHT ); knoppen = new Panel(); result.setFont( new Font("Arial", Font.BOLD, 20) ); this. setLayout( new BorderLayout() ); knoppen.setLayout( new GridLayout(4,4,6,6) ); for (int n=0; n<16; n++) { opschrift = "789/456*123+0C=-".substring(n,n+1); knop = new Button(opschrift); knop.addActionListener(this); knoppen.add( knop ); } this.add(result , BorderLayout.NORTH ); this.add(knoppen, BorderLayout.CENTER); } public void actionPerformed(ActionEvent e) { Button b; char c; b = (Button) (e.getSource()); c = b.getLabel().charAt(0); if (c==C) proc.schoon(); else if (c===) proc.reken(); else if (c>=0&&c<=9) proc.cijfer( c-0 ); else proc.operatie(c); result.setText( ""+proc.scherm ); } private static final long serialVersionUID = 1;

20

25

30

35

40

45

} Listing 22: Calculator/src/Calc.java

146

Ontwerp van programmas

class Proc { long waarde, vorige, scherm; char operator;


5

Proc() { schoon(); }
10

15

void schoon() { waarde = 0; vorige = 0; operator = +; scherm = 0; } void reken() { switch(operator) { case +: vorige case -: vorige case *: vorige case /: vorige } scherm = vorige; waarde = 0; }

20

25

+= -= *= /=

waarde; waarde; waarde; waarde;

break; break; break; break;

30

35

void cijfer(int n) { waarde = 10*waarde+n; scherm = waarde; } void operatie(char c) { reken(); operator = c; } } Listing 23: Calculator/src/Proc.java

40

12.2 Toepassing: Rekenmachine

147

Figuur 23: De applet Calc in werking.

De methode operatie correspondeert met het indrukken van een operator-toets. Eerst wordt de eventueel nog hangende berekening uitgevoerd; vervolgens wordt de nieuwe operator opgeslagen. De methode schoon initialiseert de toestand. Door het toekennen van + aan de variabele operator, kan de =-toets ook veilig ingedrukt worden als er alleen maar cijfers zijn ingevoerd. De schoon-methode correspondeert met de C-toets, maar wordt ook in de constructormethode aangeroepen. De userinterface van de rekenmachine In de methode init bouwen we de userinterface op. In een for-opdracht worden 16 buttons gecre eerd. Met een aanroep van substring kiezen we voor elke button het juiste opschrift uit een string met alle opschriften. De buttons worden met add toegevoegd aan een voor dat doel aangemaakt Panel-object, die een GridLayout heeft. Daardoor komen de knoppen netjes in een 4 4 raster te staan. Het Panel op zijn beurt wordt, samen met een Label voor het resultaat, aan de totale applet toegevoegd, die een BorderLayout heeft gekregen. In de methode actionPerformed reageren we op een actie van de gebruiker. Omdat in dit programma alleen maar Buttons een action-event kunnen genereren, weten we zeker dat getSource een Button-object zal opleveren. Met een cast kunnen we die dus veilig naar Button converteren, waarna we het opschrift van de button kunnen achterhalen. Afhankelijk van dit opschrift wordt e en van de methodes van Proc aangeroepen. Het resultaat wordt vervolgens op de display van de calculator getoond. De switch-opdracht In de klasse Calc wordt een nieuw soort opdracht gebruikt: de switch-opdracht. De switch-opdracht kan gebruikt worden als vereenvoudiging van een veel voorkomend soort ifopdracht. Vaak wordt een if-opdracht gebruikt om een variabele met een aantal alternatieven te vergelijken:
if (x==1) een(); else if (x==2) twee(); else if (x==3) drie(); else meer();

Met een switch-opdracht kan dit worden geschreven als:


switch(x) { case 1: case 2: case 3: default: een(); break; twee(); break; drie(); break; meer();

148

Ontwerp van programmas

Bij uitvoering van een switch-opdracht wordt de waarde tussen de haakjes uitgerekend. Vervolgens wordt de verwerking van opdrachten voortgezet achter het woord case met de betreende waarde. Is dit er niet, dan worden de opdracht(en) achter default uitgevoerd. De waarden achter de diverse cases moeten constanten zijn. De break-opdracht Als we niet uitkijken, worden in een switch-opdracht niet alleen de opdrachten achter betreende case uitgevoerd, maar ook de opdrachten achter de volgende cases. Alleen de plaatsing van de speciale break-opdracht verhindert dit. De betekenis van de break-opdracht is: stop de verwerking van de body van de switch-, while- of for-opdracht waarmee je bezig bent. Zonder de plaatsing van deze opdrachten zou in bovenstaand voorbeeld in het geval dat x de waarde 2 heeft niet alleen methode twee worden aangeroepen, maar ook drie en meer.

12.3

Applications

Opbouw van een application Tot nu toe hadden alle voorbeeldprogrammas de vorm van een applet, bedoeld om hun resultaat te tonen in het window van een web-browser. Je maakt een applet door een subklasse te maken van de klasse Applet, en daarin init en/of paint te herdeni eren. Als reactie op een <applet>-tag in de html-le maakt de browser dan een object met die klasse als type, en bewerkt dat object vervolgens met de methoden init en paint. In plaats van met een browser kun je het applet ook runnen met het programma appletviewer. Die toont het applet in een apart window, maar dat window is eigendom van appletviewer: de titel van het window luidt altijd Applet Viewer, er is e en menu genaamd Applet waarmee je het programma kunt starten en stoppen, en onderin beeld verschijnen meldingen als Applet started. Je kunt echter ook Java-programmas maken die hun resultaat tonen in een zelfstandig window, compleet met een eigen titel en eigen menu-keuzes. Zon programma heet een application. Applications kunnen, net als applets, gebruik maken van de Java-bibliotheken, inclusief de AWTlibrary om buttons, scrollbars en dergelijke te maken. Alleen de klasse Applet mag niet gebruikt worden in een application. In feite mag er in applications zelfs meer dan in applets: zo kan een application bijvoorbeeld les op de disk lezen en schrijven. Ook kan een application extra windows aanmaken en aparte dialoog-windows opstarten om bijvoorbeeld om een lenaam te vragen. Applets mogen dat soort faciliteiten niet gebruiken, omdat dat een veiligheids-risico zou betekenen: applets worden immers gestart als je een web-pagina bezoekt, en het surft niet lekker als je ieder moment een applet zou kunnen tegenkomen dat ongevraagd allerlei les op je harddisk zou weggooien. Applications worden door de gebruiker expliciet opgestart, en hebben daarmee dezelfde rechten die andere programmas hebben; de gebruiker dient een application dus te vertrouwen. De methode main Voor het uitvoeren van een application heb je geen web-browser nodig, en ook niet het programma appletviewer. Je hebt echter wel een Java-bytecode-interpreter nodig. Die is aanwezig in de vorm van het programma java. Je kunt deze interpreter starten vanaf een commandoregel met bijvoorbeeld het commando
java Hallo

of vanuit de JCreator-omgeving met menukeuze BuildExecute Project (of simpelweg F5). In beide gevallen reageert de interpreter met het aanroepen van een methode. Ditmaal is dat niet (zoals bij applets) de methode init, maar in plaats daarvan de methode main. Er is nog een verschil met applets: bij applets maakt de browser eerst een object aan, om dat onder handen te geven aan de methode init; bij applications maakt de interpreter geen object aan. De methode main is dan ook een statische methode, dat wil zeggen een methode zonder object onder handen. Deze methode mag dan ook niet eventuele membervariabelen, die boven in de klasse zijn gedeclareerd, gebruiken. De methode main moet exact de volgende header hebben:
public static void main(String[] p)

12.3 Applications

149

De methode levert dus void op, is public omdat hij van buitenaf gebruikt moet kunnen worden, is static omdat hij geen object onder handen krijgt, en heeft een array met strings als parameter. Alleen de naam van de parameter mag je nog vari eren. Hier is de naam p gekozen. Creatie van een window Omdat er in applications nog geen window aanwezig is om de resultaten in te kunnen tonen, zul je die zelf moeten maken (tenzij je een niet-window-programma schrijft, dat zijn uitvoer toont in een dos-sessie of iets vergelijkbaars; zie sectie 13.6). Je kunt zon window maken door een object van het type Frame te cre eren. Dan krijg je echter een leeg window; in de praktijk zul je daarom een eren, en van die klasse een object aanmaken. In de constructor-methode subklasse van Frame deni van deze klasse gebeurt wat in een applet in de methode init zou gebeuren. De opbouw van een application is daarom vaak als volgt:
class Hallo extends Frame { Hallo() { // de noodzakelijke initialisaties } public static void main(String[] p) { // aanmaak van een Hallo-object } }

De methode main hoeft strikt genomen niet in diezelfde klasse te staan. Het is misschien zelfs wel een beetje raar dat de methode main een object van zijn eigen klasse aanmaakt. Maar kwaad kan dit ook niet, en zeker in kleine programmas zou het wat overdreven zijn om speciaal voor de methode main een andere klasse aan te maken. Een initialisatie die in ieder geval nodig is, is het vaststellen van de afmetingen van het window. Er is immers geen html-le meer, zoals die bij applets werd gebruikt om de afmetingen op te geven. Om het window ook zichtbaar te maken, kun je in main van het kersverse object vervolgens de methode setVisible aanroepen. Resultaten kun je tonen in het window door, net als in het geval van applets, de methode paint te herdeni eren. Een complete Hallo-application luidt bijvoorbeeld:
import java.awt.*; class Hallo extends Frame { Hallo() { this.setSize(200,100); } public void paint(Graphics gr) { gr.drawString("Hallo", 50, 50); } public static void main(String[] p) { Hallo h; h = new Hallo(); h.setVisible(true); } }

Let op dat de methode paint niet static is: deze methode heeft immers het Frame-object, of liever gezegd het object van de subklasse daarvan, onder handen. Een alternatief voor het directe tekenen door herdenitie van paint, is het gebruikmaken van interactie-componenten zoals TextField en Button. Die kunnen zoals gebruikelijk van ActionListeners worden voorzien, zodat het programma kan reageren op het gebruik ervan. Interactie-componenten die nodig zijn in de methode actionPerformed, moeten als membervariabele worden gedeclareerd. Een application heeft standaard een BorderLayout als LayoutManager. Dat betekent dat je bij aanroep van add een windrichting moet opgeven. Bijvoorbeeld:
import java.awt.*; import java.awt.event.*; class Hallo extends Frame implements ActionListener

150

Ontwerp van programmas

TextField t; Button b; Hallo() { t = new TextField(20); b = new Button("druk hier"); this.setSize(200,100); this.add(t, BorderLayout.CENTER); this.add(b, BorderLayout.NORTH ); b.addActionListener(this); } public void actionPerformed(ActionEvent e) { t.setText("Hallo"); } public static void main(String[] p) { Hallo h; h = new Hallo(); h.setVisible(true); }

12.4

Menus en WindowEvents

Creatie van een menu-balk Om een menu-balk te cre eren zijn er een drietal standaard-klassen beschikbaar: MenuBar, Menu en MenuItem. Door aanroep van de methode setMenuBar kan de menubar boven in het Frame-object worden neergezet. De MenuItems kunnen een ActionListener krijgen, die een seintje krijgt als het item wordt gekozen. Zoals we ook steeds bij applets hebben gedaan, kunnen we het totale (Frame-subklasse-)object gebruiken als listener. Hoe de creatie van menus precies verloopt kun je eigenlijk raden. De objecten worden geconstrueerd, en met add aan elkaar toegevoegd. Als parameter van de constructor dient de String die als menu-titel moet worden getoond. Een typisch menu kun je opbouwen met:
MenuBar bar; Menu menu; MenuItem item; bar = new MenuBar(); menu = new Menu("File"); item = new MenuItem("Open"); menu.add(item); item.addActionListener(this); item = new MenuItem("Save"); menu.add(item); item.addActionListener(this); menu.addSeparator(); item = new MenuItem("Quit"); menu.add(item); item.addActionListener(this); bar.add(menu);

Aanroep van de methode addSeparator heeft als eect dat er een horizontale scheidingslijn verschijnt tussen de menuitems Save en Quit. De variabele item wordt steeds opnieuw gebruikt. Dit natuurlijk pas nadat de nieuwe objecten zijn toegevoegd aan het File-menu. Nadat het menu in zijn geheel aan de menubar is toegevoegd, kan ook de variabele menu opnieuw worden gebruikt voor het volgende menu:
menu = new Menu("Edit"); item = new MenuItem("Find"); menu.add(item); item.addActionListener(this); item = new MenuItem("Copy"); menu.add(item); item.addActionListener(this); item = new MenuItem("Paste"); menu.add(item); item.addActionListener(this); bar.add(menu);

Tot slot wordt de aldus opgebouwde menubar aan het frame gehangen:
this.setMenuBar(bar);

Het window-sluitmenu In de rand van het window zijn de gebruikelijke knopjes aanwezig om het window te sluiten, te maximaliseren, en te iconiceren. Het sluit-knopje werkt echter nog niet. Het indrukken ervan genereert wel een Event, maar we moeten zelf in het programma schrijven dat hierop gereageerd moet worden door het programma te be eindigen.

12.5 Toepassing: een bitmap-editor

151

Het event is van een nieuw type: WindowEvent. Daarnaar kan worden geluisterd door implementaties van WindowListener. We schrijven in de klasse daarom
class Programma extends Frame implements ActionListener, WindowListener

In de constructor komt te staan dat het window luistert naar zijn eigen window-events:
private Programma() { this.addWindowListener(this);

De belofte om WindowListener te zijn wordt ingelost door het schrijven van de methode windowClosing. In de body roepen we de methode exit aan (een statische methode uit de klasse System), die onmiddelijke be eindiging van het programma tot gevolg heeft.
public void windowClosing (WindowEvent e) { System.exit(0); }

Dat is echter nog niet voldoende. Door de interface WindowListener worden namelijk niet e en, maar zeven methodes gevraagd. Hoewel die in dit programma allemaal niet nodig zijn, moeten ze wel gedenieerd worden. De body kan daarbij dan leeg blijven. Aldus deni eren we ook:
public public public public public public void void void void void void windowClosed (WindowEvent windowOpened (WindowEvent windowIconified (WindowEvent windowDeiconified(WindowEvent windowActivated (WindowEvent windowDeactivated(WindowEvent e) e) e) e) e) e) {} {} {} {} {} {}

12.5

Toepassing: een bitmap-editor

Bitmaps Een bitmap is een tekening die uit kleine puntjes is opgebouwd. Bitmaps worden veel gebruikt, omdat beeldschermen (bijna) altijd uit losse beeldpunten bestaan, waarop een bitmap goed kan worden weergegeven. De naam bitmap geeft aan dat elk punt door een bit (0 of 1), of boolean waarde (false of true) kan worden afgebeeld. Een 0 of false duidt een wit punt aan, een 1 of true een zwart punt. Eigenlijk is het een woord uit het zwartwit-tijdperk, want plaatjes zijn tegenwoordig natuurlijk over het algemeen in kleur. De correcte term voor een kleurenplaatje dat uit losse punten is opgebouwd is eigenlijk pixmap: een plaatje dat uit pixels oftewel beeldpunten is opgebouwd. Functies van de bitmap-editor We gaan in deze sectie een bitmap-editor ontwikkelen. Het is geen pixmap-editor: met dit programma kun je alleen zwartwit-plaatjes maken. Het programma toont een vergrote weergave van een bitmap, die de gebruiker met de muis kan veranderen: met de linker muisknop worden punten ingekleurd, met de rechter muisknop weer blanco. We maken twee versies van het programma: een applet en een application. De gebruiker kan een aantal operaties op het plaatje uitvoeren. In de application gaat dat met behulp van menukeuzes, in de applet met buttons. Deze operaties zijn: schoonmaken van het hele plaatje inverteren van het plaatje (wit wordt zwart en zwart wordt wit) het plaatje e en beeldpunt opschuiven naar links, rechts, boven of beneden het plaatje massiever maken, of juist hol het zogenaamde game of life spelen, waarbij een nieuwe generatie wordt berekend, of een continue animatie wordt getoond Als het programma gewoon start, dan wordt als afmetingen van de bitmap 20 20 beeldpunten gebruikt. Wil de gebruiker een bitmap met andere afmetingen editten, dan moet die het programma parametriseren: in de application via de commandoregel, in de applet met <param>-tags in de html-le. Opdeling in klassen We verdelen het programma over een aantal verschillende klassen: De hoofdklasse van de application wordt zoals altijd een subklasse van Frame, die we BitEdit zullen noemen (zie listing 30 en verder).

blz. 161

152

Ontwerp van programmas

blz. 155

blz. 157 blz. 142

De hoofdklasse van de applet wordt zoals altijd een subklasse van Applet, die we BitApplet zullen noemen. De applet-versie van het programma heeft vrijwel dezelfde opbouw als de applicatie: de methode main is niet nodig, en in plaats van de constructormethode hebben we een methode init, maar de rest is hetzelfde. Alleen enkele details bij het maken van de menus verschillen nog, omdat de menus in de applet de vorm van buttons hebben in plaats van drop-down menus. De listing is daarom niet apart afgedrukt. De belangrijkste interface-component die in het programma gebruikt worden is de weergave van de tekening. De tekening is een subklasse van Canvas, waarin de paint-methode opnieuw is gedenieerd. Onze subklasse van Canvas zullen we BitCanv noemen (zie listing 24 en verder). Voor de eigenlijke bitmap gaan we een klasse maken waarin de inhoud van een bitmap wordt gemodelleerd, zonder daarbij de precieze visuele representatie vast te leggen. Een bitmap, compleet met alle benodigde operaties die erop uitgevoerd kunnen worden, zullen we modelleren in de klasse BitMap (zie listing 26 en verder). Dit is eenzelfde aanpak als in het rekenmachine-programma in hoofdstuk 12, die een aparte klasse Proc had waarin het model ligt opgeslagen. Methoden van BitEdit In de klasse BitEdit wordt de grasche userinterface (GUI) gemodelleerd. Zoals in eerdere programmas ook het geval was, wordt de statische methode main in deze klasse ondergebracht. Het eren en visible maken van e en BitEdit-object. belangrijkste wat daar gebeurt is het cre In de constructormethode van BitEdit worden de voor een application gebruikelijke administratieve details afgehandeld. Verder wordt de opbouw van de interface en het menu gedaan, al wordt dat werk voor de overzichtelijkheid uitbesteed aan twee daarvoor te schrijven methoden initInterface en initMenu. Om te kunnen reageren op commandos die de gebruiker via de drop-down menus geeft, wordt de methode actionPerformed gedenieerd, en krijgen alle menu-items this als actionlistener. Verder is er nog een methode run om de in de Game of Life benodigde animatie te laten lopen. Muiskliks worden niet afgehandeld: dat doet de BitCanv, die deeluitmaakt van de GUI, zelf. Methoden van BitCanv In de klasse BitCanv wordt de grasche afbeelding van de tekening gemodelleerd. De tekening zelf wordt in in deze klasse niet gemodelleerd, uitsluitend het weergeven ervan op het scherm. Daartoe wordt de methode paint hergedenieerd. Hierin wordt een lijnenstelsel getekend, waarmee de individuele beeldpunten afgebakend worden. Verder worden de gekleurde beeldpunten als vierkantje ingetekend. Hiertoe wordt aan een BitMap-object gevraagd welke punten gekleurd zijn. Het indrukken van de muis wordt afgevangen door het implementeren van de methoden uit de interface MouseListener en MouseMotionListener. Als reactie op het indrukken van de muis, of het draggen (bewegen met ingedrukte muisknop) wordt het betreende punt in het BitMapobject gekleurd (of juist blanco) gemaakt, en het hele plaatje opnieuw getekend. Bij het tekenen wordt zoveel mogelijk het hele scherm gevuld. Om te bepalen hoe groot de beeldpunten kunnen worden afgebeeld, wordt de beschikbare ruimte wordt gedeeld door het aantal beeldpunten. Om de beeldpunten vierkant te houden, wordt het minimum van de uitkomsten van deze delingen voor de breedte en de hoogte gebruikt. Methoden van BitMap In een object van de klasse BitMap worden de bits van het plaatje opgeslagen. Er komen methoden isZwart en maakZwart om de status van een beeldpunt op te vragen, respectievelijk te veranderen. (Die kunnen vanuit BitCanv worden aangeroepen als de bitmap getekend moet worden, of met de muis veranderd wordt). Daarnaast komen er de volgende methoden in de klasse BitMap: Twee constructor-methoden: de ene maakt een blanco bitmap met gegeven breedte en hoogte, een andere maakt een kopie van een bestaande bitmap. Zes methoden die het hele plaatje tegelijk veranderen: clear om te wissen, invert om het te inverteren, en left, right, up en down om het beeld te verschuiven. Deze operaties werken op de bitmap die onderhanden is; na aanroep van deze methoden is de bitmap dus veranderd. Drie methoden die het plaatje veranderen vogens bepaalde regels: bold maakt het plaatje massiever, outline bepaalt de omtrek van het plaatje, en life bepaalt een nieuwe generatie

12.5 Toepassing: een bitmap-editor

153

Figuur 24: Vier stappen in het bold maken van een plaatje

volgens de regels van het game of life. Drie hulpmethoden die van pas komen om bold en outline te implementeren: een methode and die alleen die punten gekleurd laat die in beide plaatjes gekleurd zijn; een methode or die de punten gekleurd maakt die in een van beide plaatjes (of in allebei) zwart zijn; en een methode xor die de punten kleurt die in een van beide plaatjes, maar niet in allebei gekleurd zijn. Twee methoden die het plaatje van vorm veranderen: rotate draait het plaatje een kwartslag, en scale maakt het dubbel zo groot. Anders dan de overige methoden kunnen deze methoden niet de bitmap die onder handen is aanpassen: daarin is immers niet genoeg ruimte om alle nieuwe beeldpunten op te slaan. In plaats daarvan zullen deze methode dus een nieuwe bitmap maken, en die als resultaat opleveren. De bitmap die onder handen is wordt niet veranderd. Beeldbewerkingsoperaties In de klasse BitMap komt een methode bold die een plaatje massiever maakt. Een makkelijke manier om dat te doen is de volgende. We maken eerste een kopie van het plaatje. De kopie schuiven we e en beeldpunt naar links. Daarna kleuren we de beeldpunten die in het oorspronkelijke plaatje of in het verschoven plaatje zwart zijn. Het plaatje krijgt er daardoor een extra rand bij. Vervolgens herhalen we het proced e in verticale richting. Om de outline van een plaatje te bepalen gaan we als volgt te werk. We maken weer een kopie, en verschuiven die e en beeldpunt naar links en naar beneden. Vervolgens laten we alleen die

154

Ontwerp van programmas

Figuur 25: Tien stappen tijdens het Game of Life

beeldpunten zwart, die in het oorspronkelijke plaatje zwart zijn, maar in het verschoven plaatje wit; die liggen immers aan de rand. Ook de beeldpunten die wit zijn, maar in het verschoven plaatje zwart liggen aan de rand, en moeten dus zwart gemaakt worden. Het Game of Life Nee, dit is geen actiespelletje met high score en verborgen extra levels. Het Game of Life is een simulatieproces, in de jaren zestig van de vorige eeuw bedacht door John Conway. Het bijzondere ervan is dat door hele simpele regels toch hele complexe patronen kunnen ontstaan. Het wordt daarom soms gebruikt als metafoor voor biologische processen: op grond van eenvoudige biochemische processen kunnen ingewikkelde organismen, en zelfs intelligentie en bewustzijn ontstaan. De wereld in het Game of Life bestaat uit een twee-dimensionaal raster, waarin elke cel bewoond kan worden door een beestje. Je kunt deze wereld dus weergeven met een bitmap: een zwart vakje duidt op de aanwezigheid van een beestje, een wit vakje is onbewoond. Het bewoningspatroon in de volgende generatie wordt bepaald door de acht buur-vakjes van elk vakje: Een vakje blijft bewoond als twee of drie buurvakjes bewoond zijn (bij minder buren sterft het beestje van eenzaamheid, en bij meer buren door verstikking). In een leeg vakje wordt een nieuw beestje geboren als er precies drie buurvakjes bewoond zijn. De vakjes aan de randen hebben minder buurvakjes dan de rest. Om deze vakjes ook eerlijk te behandelen, doen we alsof de linker-en de rechterrand aan elkaar grenzen, en de onder- en bovenrand ook. In guur 25 is een aantal opeenvolgende generaties uit het Game of Life getekend. Opvallend is dat bepaalde clusters van beestjes stabiel zijn, of zich zelfs over de wereld kunnen verplaatsen! De individuele beestjes sterven en worden geboren, maar een soort hogere organismen blijven leven en kunnen bewegen. . .

12.6

Details van de bitmap-editor

Implementatie van BitMap met een array Alle genoemde beeldbewerkingsmethoden kunnen de individuele beeldpunten van het plaatje aanspreken door de methoden isZwart en maakZwart aan te roepen. Maar om die twee methoden te schrijven, zullen we een beslissing moeten nemen over de variabelen waarmee een bitmap-object wordt gemodelleerd. De meest voor de hand liggende implementatie is een array van boolean waarden: voor elk beeldpunt eentje. Twee extra integers bewaren de breedte en hoogte van het plaatje. De array wordt ge nitialiseerd in de constructor, en methoden isZwart en maakZwart kiezen het juiste element er uit:

12.6 Details van de bitmap-editor

155

import java.awt.*; import java.awt.event.*; class BitCanv extends Canvas implements MouseListener, MouseMotionListener { BitMap model; BitCanv(int w, int h) { model = new BitMap(w,h); this.addMouseListener(this); this.addMouseMotionListener(this); } public void setModel(BitMap bm) { model = bm; this.repaint(); } private int diameter() { Dimension dim = getSize(); return Math.min( dim.width/model.getWidth(), dim.height/model.getHeight() ); } public void update(Graphics g) { paint(g); } private static boolean isLinkerKnop(MouseEvent e) { return (e.getModifiers()&MouseEvent.BUTTON1_MASK)!=0; } public void mousePressed (MouseEvent e) { int x, y, w, h, d;
35

10

15

20

25

30

d x y w h

= = = = =

diameter(); e.getX()/d; e.getY()/d; model.getWidth(); model.getHeight();

40

if (x>=0 && x<w && y>=0 && y<h) model.maakZwart(x, y, isLinkerKnop(e) ); this.repaint(); }
45

Listing 24: BitEdit/src/BitCanv.java, deel 1 van 2

156

Ontwerp van programmas

50

public void paint(Graphics g) { int x, y, w, h, d; w = model.getWidth(); h = model.getHeight(); d = this.diameter(); g.setColor(Color.BLUE); for (y=0; y<=h; y++) g.drawLine(0, y*d, w*d, y*d ); for (x=0; x<=w; x++) g.drawLine(x*d, 0, x*d, h*d ); for (y=0; y<h; y++) { for (x=0; x<w; x++) { if (model.isZwart(x,y)) g.setColor(Color.RED); else g.setColor(Color.WHITE); g.fillRect( x*d+1, y*d+1, d-1, d-1 ); } } } public void doeOperatie(String aktie) { if (aktie.equals("Step")) else if (aktie.equals("Left")) else if (aktie.equals("Right")) else if (aktie.equals("Up")) else if (aktie.equals("Down")) else if (aktie.equals("Clear")) else if (aktie.equals("Invert")) else if (aktie.equals("Bold")) else if (aktie.equals("Outline")) else if (aktie.equals("Rotate")) else return; this.repaint(); }

55

60

65

70

75

model.life(); model.left(); model.right(); model.up(); model.down(); model.clear(); model.invert(); model.bold(); model.outline(); this.setModel(model.rotate());

80

85

90

public void mouseEntered (MouseEvent e) {} public void mouseExited (MouseEvent e) {} public void mouseReleased(MouseEvent e) {} public void mouseClicked (MouseEvent e) {} public void mouseMoved (MouseEvent e) {} public void mouseDragged (MouseEvent e) { this.mousePressed(e); } private static final long serialVersionUID = 1; } Listing 25: BitEdit/src/BitCanv.java, deel 2 van 2

12.6 Details van de bitmap-editor

157

import java.awt.image.*; class BitMap extends BufferedImage { static final int WIT = 0xFFFFFFFF; static final int ZWART = 0xFF000000; BitMap(int w, int h) { super(w, h, BufferedImage.TYPE_INT_RGB ); this.clear(); } BitMap(BufferedImage ander) { this(ander.getWidth(), ander.getHeight()); int w = ander.getWidth(); int h = ander.getHeight(); this.setRGB(0,0,w,h, ander.getRGB(0,0,w,h,null,0,w), 0, w); }

10

15

20

void maakZwart(int x, int y, boolean b) { if (b) this.setRGB(x, y, ZWART); else this.setRGB(x, y, WIT); }
25

boolean isZwart(int x, int y) { return this.getRGB(x, y)==ZWART; }


30

35

void clear() { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); for (y=0; y<h; y++) for (x=0; x<w; x++) this.maakZwart(x, y, false); } void invert() { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); for (y=0; y<h; y++) for (x=0; x<w; x++) this.maakZwart(x, y, ! this.isZwart(x,y) ); }

40

45

Listing 26: BitEdit/src/BitMap.java, deel 1 van 4

158

Ontwerp van programmas

50

55

void left() { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); for (y=0; y<h; y++) { for (x=1; x<w; x++) this.maakZwart(x-1, y, this.isZwart(x,y) ); this.maakZwart(w-1, y, false); } } void right() { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); for (y=0; y<h; y++) { for (x=w-1; x>0; x--) this.maakZwart(x,y, this.isZwart(x-1,y)); this.maakZwart(0, y, false); } } void up() { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); for (x=0; x<w; x++) { for (y=1; y<h; y++) this.maakZwart(x,y-1, this.isZwart(x,y)); this.maakZwart(x, h-1, false); } } void down() { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); for (x=0; x<w; x++) { for (y=h-1; y>0; y--) this.maakZwart(x, y, this.isZwart(x,y-1)); this.maakZwart(x, 0, false); } }

60

65

70

75

80

85

90

Listing 27: BitEdit/src/BitMap.java, deel 2 van 4

12.6 Details van de bitmap-editor

159

95

void or { int w = h = for

(BitMap ander) x, y, w, h; this.getWidth(); this.getHeight(); (y=0; y<h; y++) for (x=0; x<w; x++) this.maakZwart(x, y, this.isZwart(x,y) || ander.isZwart(x,y) );

}
100

105

void xor (BitMap ander) { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); for (y=0; y<h; y++) for (x=0; x<w; x++) this.maakZwart(x, y, this.isZwart(x,y) ^ ander.isZwart(x,y) ); } void and (BitMap ander) { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); for (y=0; y<h; y++) for (x=0; x<w; x++) this.maakZwart(x, y, this.isZwart(x,y) && ander.isZwart(x,y) ); } void bold() { BitMap ander; ander = new BitMap(this); ander.left(); this.or(ander); ander = new BitMap(this); ander.down(); this.or(ander); } void outline() { BitMap ander; ander = new BitMap(this); ander.left(); ander.down(); this.xor(ander); }

110

115

120

125

130

135

Listing 28: BitEdit/src/BitMap.java, deel 3 van 4

160

Ontwerp van programmas

140

145

150

void life() { int x, y, n; BitMap oud; oud = new BitMap(this); int w, h; w = this.getWidth(); h = this.getHeight(); for (y=0; y<h; y++) for (x=0; x<w; x++) { n = oud.buren(x,y); this.maakZwart(x,y, n==3 || (oud.isZwart(x,y)&&n==2) ); } } int buren(int x, int y) { int w, h; w = this.getWidth(); h = this.getHeight(); int xl = x-1; if (xl<0) int yl = y-1; if (yl<0) int xr = x+1; if (xr>=w) int yr = y+1; if (yr>=h) int n=0; if (this.isZwart(xl,yl)) if (this.isZwart(x ,yl)) if (this.isZwart(xr,yl)) if (this.isZwart(xl,y )) if (this.isZwart(xr,y )) if (this.isZwart(xl,yr)) if (this.isZwart(x ,yr)) if (this.isZwart(xr,yr)) return n; } BitMap rotate() { int x, y, w, h; w = this.getWidth(); h = this.getHeight(); BitMap result = new BitMap( h, w ); for (y=0; y<h; y++) for (x=0; x<w; x++) result.maakZwart(h-1-y, x, this.isZwart(x,y) ); return result; } } Listing 29: BitEdit/src/BitMap.java, deel 4 van 4 xl+=w; yl+=h; xr-=w; yr-=h; n++; n++; n++; n++; n++; n++; n++; n++;

155

160

165

170

175

180

185

12.6 Details van de bitmap-editor import java.awt.*; import java.awt.event.*;

161

class BitEdit extends Frame implements ActionListener, WindowListener, Runnable { BitCanv uitvoer; Thread animatie; final int animatieVertraging = 100; final String items [][] = { {"File", "Open", "Save", "Quit"} , {"Move", "Left", "Right", "Up", "Down"} , {"Edit", "Clear","Invert","Bold", "Outline"} , {"Transform", "Rotate","Scale"} , {"Life", "Step", "Start", "Stop"} }; private BitEdit (int w, int h) { this.setSize(800,600); this.setTitle("Bitmap editor"); this.addWindowListener(this); initInterface(w, h); initMenu(); } private void initMenu() { MenuBar bar; bar = new MenuBar(); for (int i=0; i<items.length; i++) bar.add( maakMenu(items[i], this) ); this.setMenuBar(bar); } private Menu maakMenu(String [] keuzes, ActionListener listener) { Menu menu; MenuItem item; menu = new Menu(keuzes[0]); for (int i=1; i<keuzes.length; i++) { item = new MenuItem(keuzes[i]); menu.add(item); item.addActionListener(listener); } return menu; } private void initInterface(int w, int h) { uitvoer = new BitCanv(w, h); this.setLayout(new BorderLayout() ); this.add(uitvoer, BorderLayout.CENTER); } public void actionPerformed(ActionEvent ev) { String aktie; aktie = ((MenuItem) ev.getSource()).getLabel(); this.doeOperatie(aktie); }

10

15

20

25

30

35

40

45

50

55

Listing 30: BitEdit/src/BitEdit.java, deel 1 van 2

162

Ontwerp van programmas

60

65

public void doeOperatie(String aktie) { if (aktie.equals("Quit")) { System.exit(0); } else if (aktie.equals("Start") && animatie==null) { animatie = new Thread(this); animatie.start(); } else if (aktie.equals("Stop")) { animatie=null; } else uitvoer.doeOperatie(aktie); } public void run() { while (animatie!=null) { uitvoer.doeOperatie("Step"); try {Thread.sleep(animatieVertraging);} catch (InterruptedException e) {} } } public static void main(String [] ps) { int w=30, h=20; if (ps.length>0) w = h = Integer.parseInt(ps[0]); if (ps.length>1) h = Integer.parseInt(ps[1]); BitEdit b; b = new BitEdit(w, h); b.setVisible(true); }

70

75

80

85

90

95

public void windowClosing (WindowEvent e) public void windowClosed (WindowEvent e) public void windowOpened (WindowEvent e) public void windowIconified (WindowEvent e) public void windowDeiconified(WindowEvent e) public void windowActivated (WindowEvent e) public void windowDeactivated(WindowEvent e) private static final long serialVersionUID = }

{System.exit(0);} {} {} {} {} {} {} 1;

Listing 31: BitEdit/src/BitEdit.java, deel 2 van 2

12.6 Details van de bitmap-editor

163

class BitMap { int breed, hoog; boolean [] punten; BitMap(Int b, int h) { breed=b; hoog=h; punten = new boolean[breed*hoog]; } void maakZwart(int x, int y, boolean b) { punten[y*breed+x] = b; } boolean isZwart(int x, int y) { return punten[y*breed+x]; }

Als een bitmap bijvoorbeeld 10 beeldpunten breed is, worden array-elementen 0 t/m 9 gebruikt voor de eerste rij, 10 t/m 19 voor de tweede rij, enzovoort. Daarom wordt in de methoden isZwart en maakZwart het nummer van de rij, y, vermenigvuldigd met de breedte van de bitmap. Implementatie van BitMap met een tweedimensionale array Bij het modelleren van tweedimensionale structuren, zoals plaatjes, is het eigenlijk gemakkelijker om een tweedimensionale array te gebruiken. Berekeningen zoals y*breed+x zijn dan niet meer nodig: je kunt de twee coordinaten in een tweedimensionale array apart speciceren. Tweedimensionale arrays worden op dezelfde manier gebruikt als eendimensionale arrays, alleen staan er nu (bij declaratie, initialisatie, en gebruik) twee paar vierkante haken achter de naam in plaats van e en. De implementatie van Bitmap komt er dan als volgt uit te zien:
class BitMap { int breed, hoog; boolean [][] punten; BitMap(int b, int h) { breed=b; hoog=h; punten = new boolean[breed][hoog]; } void maakZwart(int x, int y, boolean b) { punten[x][y] = b; } boolean isZwart(int x, int y) { return punten[x][y]; }

Implementatie van BitMap als subklasse van BueredImage Maar waarom zouden we al die moeite doen? Er zijn bij Java zo veel standaardklassen meegeleverd, dat er voor een heleboel situaties al een kant-en-klare klasse beschikbaar is, compleet met meer methoden dan je ooit zou willen gebruiken. De creativiteit van het programmeren gaat daarmee wel een beetje verloren, want in plaats van zelf zon klasse in elkaar te knutselen zit je alleen maar in dikke manuals te bladeren om op te zoeken welke methoden er allemaal beschikbaar zijn. Maar het is natuurlijk wel gemakkelijk. . . Ook in dit geval is er een klasse die vrijwel dat modelleert wat we nodig hebben: de klasse BufferedImage modelleert een complete bitmap, zij het eentje met kleuren-pixels: eigenlijk dus een pixmap. Methoden getRGB en setRGB zijn aanwezig om de kleur van een beplaad beeldpunt op te vragen, respectievelijk te verandern. Gebruikmakend daarvan kunnen we onze eigen isZwart en maakZwart schrijven, die dan alleen nog maar de vertaling van kleur naar zwartwit hoeven te doen. Er zijn geen eigen variabelen meer nodig om de bitmap op te slaan: die worden ge erfd van BufferedImage. Wel maken we twee constanten om gemakkelijk de kleuren zwart en wit te kunnen aanduiden.

164

Ontwerp van programmas

class BitMap extends BufferedImage { static final int WIT = 0xFFFFFFFF; static final int ZWART = 0xFF000000; BitMap(int b, int h) { super(b,h, BufferedImage.TYPE_INT_ARGB ); this.clear(); } void maakZwart(int x, int y, boolean b) { if (b) this.setRGB(x, y, ZWART); else this.setRGB(x, y, WIT); } boolean isZwart(int x, int y) { return this.getRGB(x,y)==ZWART; }

Deze implementatie van BitMap wordt in het uiteindelijke programma gebruikt. Codering van kleuren in integers Het gebruik van de klasse BufferedImage roept wel de vraag op hoe de kleur van een beeldpunt eigenlijk met e en int gecodeerd kan worden (het resultaat van getRGB en de parameter van setRGB). Voor de beschrijving van een kleur zijn immers drie getallen nodig: de hoeveelheid rood, groen en blauw. De mogelijke waarden voor rood, groen en blauw zijn echter beperkt tot het bereik van 0 tot en met 255 dat is voor elke kleurcomponent precies e en byte. In een int zijn vier bytes beschikbaar, dus dat moet passen; er is zelfs nog een byte over die gebruikt kan worden om de ondoorschijnendheid (die meestal alpha wordt genoemd) van de kleur te representeren. Wil je de losse kleurcomponenten gebruiken, dan zou je met de operatoren / en % met enige moeite de int is losse bytes splitsen.
kleur blauw groen rood alpha = = = = = plaatje.getRGB(); kleur (kleur / 256 ) (kleur /(256*256) ) (kleur /(256*256*256)) % % % % 256; 256; 256; 256;

Omgekeerd kun je met de operator * de losse componenten inpakken in een int:


kleur = alpha*256*256*256 + rood*256*256 + groen*256 + blauw;

In plaats van de delingen kunnen we ook gebruik maken van de operator >>. Om de uitkomst van a>>b te berekenen worden de bits die het getal a voorstellen b posities naar rechts geschoven. De uitkomst van 53>>2 bijvoorbeeld is 13. De binaire codering van 53 is namelijk 110101. Als je dat twee posities naar rechts schuift, krijg je 1101 (de 0 en de 1 aan de rechterkant vallen buitenboord), en dat is de binaire codering van het getal 13. Om een groepje bits te isoleren kunnen we vervolgens de operator & gebruiken. Deze werkt op twee getallen. De bits van die twee getallen worden stuk voor stuk bewerkt met de logische en-operator: een bit van het resultaat is alleen 1, als op de overeenkomstige plaats in beide getallen een 1 staat. De uitkomst van 172&15 bijvoorbeeld is 12. De binaire codering van 172 is namelijk 10101100, die van 15 is 00001111. Bitsgewijs gecombineerd geeft dat 00001100, en dat is 12. Let er op dat de operator & slechts uit een enkel &-teken bestaat, niet te verwarren met het dubbele &&-teken, dat gebruikt wordt om de en-operatie op twee boolean waarden uit te voeren. Gebruikmakend van deze nieuwe operatoren >> en & kan het opsplitsen van de samengestelde kleur nu als volgt gebeuren:
kleur blauw groen rood alpha = = = = = plaatje.getRGB(); kleur & 0xFF; (kleur >> 8) & 0xFF; (kleur >> 16) & 0xFF; (kleur >> 24) & 0xFF;

In plaats van het hexadecimale getal 0xFF hadden we ook het decimale getal 255 kunnen schrijven. De hexadecimale notatie benadrukt echter onze bedoeling, namelijk dat we de laatste acht bits van het getal willen pakken.

12.6 Details van de bitmap-editor

165

Omgekeerd kunnen we de kleurcomponenten inpakken met behulp van de operatoren << (schuif naar links) en | (combineer de bits van twee getallen met de logische or-operatie):
kleur = (alpha<<24) | (rood<<16) | (groen<<8) | blauw; plaatje.setRGB(kleur);

Het uit- en inpakken van kleurcomponenten met behulp van de schuif-operatoren is te prefereren boven de versie met de delings-operatoren, om twee redenen: het is voor de lezer van het programma duidelijker wat de bedoeling van de programmeur is het werkt ook als alpha groter is dan 127. De delings-versie gaat dan de mist in omdat er overow optreedt. Geparametriseerde opbouw van de menus In sectie 12.4 is besproken hoe je een menu kunt maken:
Menu menu item item item menu; = new = new = new = new MenuItem item; Menu("File"); MenuItem("Open"); menu.add(item); item.addActionListener(this); MenuItem("Save"); menu.add(item); item.addActionListener(this); MenuItem("Quit"); menu.add(item); item.addActionListener(this);

Dat wordt wel erg langdradig als er, zoals in de bitmap-editor, erg veel menu-keuzes zijn. Weliswaar kun je tijdens het programmeren de copy-paste functie van de editor gebruiken om het tikwerk te beperken, maar toch is dat niet zon goed idee: het maakt het programma lastiger te overzien en te onderhouden. Beter is het om de regelmaat te proberen te vangen in een while-opdracht. We krijgen dan:
Menu menu; MenuItem item; menu = new Menu("File"); for (int i=iets ; i<iets ; i++) { item = new MenuItem(iets ); menu.add(item); item.addActionListener(this); }

Als parameter van de MenuItem-constructor moet elke keer een andere string worden meegegeven. Om het nog even exibel te houden welke strings dat zijn, plaatsen we het hele stuk code in een methode, die als parameter een array met de te gebruiken strings meekrijgt. Het eerste element daarvan geven we mee aan de constructor van Menu, de overige aan de respectievelijke MenuItems. Het opgebouwde Menu wordt als resultaat van de methode opgelevert:
private Menu maakMenu(String [] keuzes) { Menu menu; MenuItem item; menu = new Menu(keuzes[0]); for (int i=1; i<keuzes.length; i++) { item = new MenuItem(keuzes[i]); menu.add(item); item.addActionListener(this); } return menu; }

Deze methode kunnen we nu meerdere keren aanroepen, een keer voor elk menu, waarbij we steeds een andere array met strings meegeven:
final String [] items1 = {"File", "Open", "Save", "Quit" }; final String [] items2 = {"Move", "Left", "Down", "Up" }; MenuBar bar; bar = new MenuBar(); bar.add( maakMenu(items1) ); bar.add( maakMenu(items2) );

Maar nog mooier is het om ook deze herhaling weer in een for-opdracht te vangen. We maken daarom de twee arrays van strings op hun beurt onderdeel van een array: een array van arrays van strings, met andere woorden een tweedimensionale array van strings. Van die tweedimensionale array wordt er in de body van de for-opdracht steeds e en rij uitgekozen. Door de tweedimensionale array e en index te geven, blijft er een eendimensionale array over, en dat is precies wat de methode maakMenu verwacht.

166

Ontwerp van programmas

final String [][] items = { {"File", "Open", "Save", "Quit" } , {"Move", "Left", "Down", "Up" } MenuBar bar; bar = new MenuBar(); for (int i=0; i<items.length; i++) bar.add( maakMenu(items[i]) );

};

Opgaven
12.1 Grasch histogram Een groothandel in eieren kan per maand 120.000 eieren verkopen. In sommige maanden is de vraag naar eieren echter groter dan in andere maanden. Om de productie te plannen maakt men gebruik van het volgende applet. Op het scherm zijn twee dingen zichtbaar: Op de linkerhelft staat een tekening van 300 bij 300 pixels. De tekening is een staaf-diagram: bij elke maand geeft een rechthoek aan hoeveel eieren er die maand verkocht zijn: een staaf van 0 pixels hoog betekent geen eieren verkocht, de totale hoogte van de tekening betekent de maximale omzet van 120.000 eieren, een staaf van de onderrand tot de halve hoogte is 60.000 eieren, enzovoorts. Op de rechterhelft staan onder elkaar 12 tekstvelden, waarin de gebruiker aantallen eieren kan intikken. Het programma moet op twee manieren reageren op de gebruiker: De gebruiker tikt een getal in in een van de tekstvelden, afgesloten met de Enter-toets. Het staafdiagram wordt dan opnieuw getekend, gebruikmakend van de nieuwe aantallen. De gebruiker klikt met de muis in de tekening op e en van de 12 maanden in het diagram. De staaf moet dan zo hoog worden als er geklikt is; het bijbehorende getal in een van de tekstvelden moet automatisch worden aangepast. Aan het begin zijn alle getallen nul, en is de tekening dus nog blanco. Hints: Maak een array-variabele waarin de toestand (dat is, de aantallen eieren in elke maand) wordt bijgehouden. Gebruik ook een array om de 12 tekstvelden in te maken. De 12 tekstvelden kun je stapelen in een Panel met een Gridlayout. Maak methodes die de toestand in het staafdiagram, respectievelijk de tekstvelden zichtbaar maken. Maak methodes die de toestand veranderen als gevolg van handelingen van de gebruiker. Je kunt reageren op muiskliks op de achtergrond van de applet (dat is: de tekening van het staafdiagram) met gebruikmaking van een MouseListener. Je kunt de Applet zijn eigen MouseListener laten zijn. Zoek op welke methoden er nodig zijn in een MouseListener, en hoe je aan de parameter van die methoden kunt zien op welke positie er geklikt is.

12.6 Details van de bitmap-editor

167

12.2 Halters Schrijf een applet met de volgende specicaties. Boven in beeld (200 bij 200 beeldpunten) is een drietal knoppen zichtbaar, met als opschrift respectievelijk clear, grid en move. De hele rest van het window kan door de gebruiker overal worden aangeklikt. Iedere keer als de gebruiker een punt van het window aanklikt, verschijnt gecentreerd op die plaats een stip met een doorsnede van 10 beeldpunten. Maximaal mogen honderd punten worden aangeklikt. Als de gebruiker toch meer punten probeert aan te klikken, gebeurt er niets. Elk tweede punt wordt met het vorige punt verbonden door een lijn (dus de tweede met de eerste, de vierde met de derde, de zesde met de vijfde enz.). Als de gebruiker op de knop grid drukt, wordt een raster van lijnen met tussenafstand van 20 beeldpunten zichtbaar (zodat de gebruiker de punten beter kan mikken). Als de gebruiker nogmaals op de knop grid drukt, verdwijnt het raster weer, bij een derde druk op de knop verschijnt het weer, enzovoorts. Als de gebruiker op de knop clear drukt, verdwijnen alle punten en lijnen, en kan de gebruiker weer beginnen met het aanklikken van nieuwe punten. Als de gebruiker op de knop move drukt, begint elk tweetal door een lijn verbonden punten naar elkaar toe te kruipen: elke seconde wordt de afstand ertussen 10% kleiner (theoretisch gesproken zullen ze elkaar dus nooit raken, maar in de praktijk zullen ze op een gegeven moment samenvallen). Als de gebruiker nogmaals op move drukt, stopt de beweging; bij een derde druk op de knop gaan de punten weer bewegen, enzovoorts.

168

Hoofdstuk 13

Objectgeori enteerd ontwerp


13.1 Abstracte klassen en interfaces
Het Once and only once principe Een goede programmeur is altijd op zoek naar een mogelijkheid om het programma compact te houden. In plaats van een stuk code met de tekstverwerker te knippen, elders nog eens in het programma toe te voegen, en er dan kleine wijzigingen aan te brengen, zal deze liever proberen om er een toepasselijk geparametriseerde methode van te maken. Zo wordt het once and only once principe hoog gehouden: programmeer iets e en keer, en niet meer dan dat. Objectgeori enteerde talen zijn bij hier uitstek geschikt voor. Niet alleen kun je groepjes opdrachten elders nog eens gebruiken door ze in een methode te zetten, die je daarna vele malen kunt aanroepen; je kunt ook groepjes variabele-declaraties opnieuw gebruiken door ze in een klasse te zetten, waarvan je daarna vele objecten kunt maken. Dankzij het mechanisme van subklassen kun je ook nog uitbreidingen en/of wijzigingen plegen aan eerder gemaakte klassen. Toch ligt zelfs dan het gevaar van copy/paste programmeren nog op de loer. Waarom dat zo erg is, en welke desastreuse gevolgen het heeft voor de onderhoudbaarheid van het programma zouden we hier uitgebreid kunnen bespreken. Maar dat doen we niet, want dat is elders al gedaan. Geheel in stijl van once and only once zullen we er hier naar verwijzen. Kijk op www.c2.com/cgi/wiki?OnceAndOnlyOnce voor een overtuigende beschrijving, en klik ook eens door naar aanverwante programmeerprincipes. Gezien? Dan kunnen we nu op zoek naar nog subtielere manieren om once and only once idee en in een programma op te schrijven. Klassen en interfaces We hebben het begrip klasse op twee manieren ingevoerd: een klasse is een groepje methoden dat bij elkaar hoort een klasse is het type van een object Later bleek dat het in feite om hetzelfde idee gaat. Methoden hebben immers een object onder handen; dat moet echter wel een object zijn van het juiste type: je kan wel de length uitrekenen van een String-object, maar niet van bijvoorbeeld een Color-object. Anderszijds kun je een Color-object onder handen nemen met de methode darken, en dat kan weer niet met een Stringobject. De methoden die je bij een bepaald object kunt gebruiken zijn precies de methoden die in de klasse zitten die het type is van dat object. Bij het schrijven van een klasse kunnen drie dingen van belang zijn: wat is een object van dit type? (dit leg je vast met de declaraties van variabelen in de klasse: elk object met deze klasse als type heeft beschikking over die variabelen); wat kun je doen met een object van dit type? (dit leg je vast met de methode-headers in de klasse: daarin staat hoe je het object onder handen neemt, en welke parameters daarbij nodig zijn); hoe doen de methoden hun werk? (dit leg je vast in de body van de methoden). Niet altijd zijn ze alle drie even interessant. Van een Color-object is het nog wel handig om te weten dat het is opgebouwd uit drie getallen (de hoeveelheid rood, groen en blauw), maar wat er allemaal nodig is voor de interne administratie van een Graphics-object hoef je eigenlijk niet te weten. Wel is het van belang om te weten dat er in de klasse Graphics een methode drawLine is, maar hoe dat precies gebeurt is weer minder van belang. Tenzij je toevallig de programmeur van zon klasse bent, dan is dat juist weer wel belangrijk.

13.1 Abstracte klassen en interfaces

169

Vaak hebben methoden een object als parameter. Zij zullen in hun body over het algemeen gebruik maken van dat object (anders was de parameter helemaal niet nodig. . . ). Dat gebruik kan zijn dat het object weer als parameter wordt meegegeven aan een andere methode, of dat het object onder handen wordt genomen door een van de toepasbare methoden. Het is bij dat laatste alleen maar van belang wat je met het parameter-object kunt doen, niet hoe dat precies gebeurt, of hoe het object intern is opgebouwd. Dit is precies de reden dat er in Java naast het begip klasse ook het begrip interface gehanteerd wordt. Een klasse is een groepje declaraties en methoden, en interface is alleen maar een groepje methode-headers. Een interface is daarmee een verlanglijstje voor wat er met een object moet kunnen. Voorbeeld: interface ActionListener Een voorbeeld van een interface in de Java-bibliotheek is ActionListener. Die is (ongeveer) als volgt gedenieerd:
interface ActionListener { public void actionPerformed(ActionEvent e); }

Omdat een interface alleen maar uit methode-headers bestaat is zon denitie meestal erg kort (tenzij er heel veel methodes op het verlanglijstje staan). Er zijn methodes die als parameter een object met het type ActionListener verwachten. Een voorbeeld daarvan is de methode addActionListener in de klasse Button, die in zon beetje elk interactief programma wordt gebruikt. De bedoeling is dat de button, als hij wordt ingedrukt, een seintje kan geven aan wie het maar horen wil. Dat gebeurt door het aanroepen van actionPerformed, en elk object dat die methode kent is welkom. Het aardige van dit mechanisme is dat de methode addActionListener in feite parameters kan accepteren met allerlei verschillende klassen als type; het enige wat van belang is, is dat die klasse de methode actionPerformed bevat, zodat die te zijner tijd kan worden aangeroepen. Op deze manier is er maar e en denitie van addActionListener nodig, die simpelweg een ActionListener parameter verlangt, in plaats van een heleboel denities voor elke denkbare klasse die zon actionPerformed methode kent. Bovendien kan de programmeur van Button (waarin de methode addActionListener staat) nog helemaal niet weten welke klassen er in de toekomst nog geschreven zullen gaan worden met een methode actionPerformed. Dankzij het interface-mechanisme blijft het once and only once principe dus overeind: er is maar e en methode addActionListener, en niet meer dan dat. Implementatie van interfaces In de header van een klasse kun je aankondigen dat zon klasse aan het verlanglijstje van een bepaalde interface tegemoet komt. We hebben dat in programmas al vaak gedaan voor de interface ActionListener, met een constructie als:
class Luisteraar implements ActionListener { public void actionPerformed(ActionEvent e) { doe iets nuttigs in reactie op een actie } }

Een interactief programma kan dan een Luisteraar-object laten reageren op het indrukken van een button:
class ProgrammaMetKnop extends Applet { public void init() { Luisteraar oor = new Luisteraar(); Button b = new Button("klik hier!"); this.add(b); b.addActionListener(oor); } }

In de voorbeeldprogrammas in eerdere hoofdstukken zijn we nogal zuinig geweest met het aantal klassen, en hebben we deze twee elementen gecombineerd in een enkele klasse. Daarmee waren het

170

Objectgeori enteerd ontwerp

programmas die naar hun eigen buttons luisterden, met de volgende opbouw:
class ProgrammaMetKnopDatZelfLuistert extends Applet implements ActionListener { public void init() { Button b = new Button("klik hier!"); this.add(b); b.addActionListener(this); } public void actionPerformed(ActionEvent e) { doe iets nuttigs in reactie op een actie } }

Omdat het programma, door de aanwezigheid van een methode actionPerformed, zelf naar zn buttons kan luisteren (en dat in de klasse-header ook belooft met implements ActionListener), is er nu ook geen apart object oor meer nodig: deze rol kan nu immers worden vervuld door this. Zo zuinig met klassen zijn programmeurs van wat grotere programmas meestal niet. Het is dan ook heel gebruikelijk dat er in een programma meerdere klassen zijn die allemaal op hun eigen manier ActionListener implementeren, en die gebruikt worden als luisteraar voor verschillende buttons. In het extreme geval heeft elke button zijn eigen luisteraar-object, al dan niet met een verschillend type. In het voorbeeldprogramma dat we in sectie 13.4 zullen bespreken zijn er zeven buttons, ieder met een eigen luisteraar-object, die bovendien twee verschillende implementaties van de interface ActionListener als type hebben. Once and only once implementatie van interfaces Het gebruik van interfaces maakt het mogelijk om een methode te schrijven met een parameter die van een heleboel verschillende typen kan zijn (mits die typen allemaal de interface implementeren). Dit stimuleert once and only once programmeren: de methode hoeft maar e en keer te worden geschreven, in plaats van voor elk type parameter apart. Toch is er ook een aspect van interfaces dat uitnodigt tot copy/paste programmeren, het tegenovergestelde van once and only once programmeren. Je merkt dat vooral aan interfaces die niet e en maar meerdere methode-headers bevatten, zoals MouseListener en MouseMotionListener. Als je een programma wilt schrijven dat reageert op het indrukken van de muis dan maak je een klasse die MouseListener implementeert. Je kunt dan een invulling geven aan de methode mousePressed, maar je bent tevens verplicht om de andere vier methoden van de interface te implementeren: mouseReleased, mouseClicked, mouseEntered en mouseExited, ook als je in die gebeurtenissen helemaal niet ge nteresseerd bent. In programmas waar alleen het indrukken van de muis van belang is, kom je dan ook vaak iets tegen als:
class MuisReactie implements MouseListener { public void mousePressed (MouseEvent e) { doe iets nuttigs in reactie het indrukken van de muis } public void mouseReleased(MouseEvent e) { } public void mouseClicked (MouseEvent e) { } public void mouseEntered (MouseEvent e) { } public void mouseExited (MouseEvent e) { } }

Schrijf je later nog eens een programma dat alleen mousePressed nodig heeft, dan is het verleidelijk om die andere vier methodes met hun lege body te kopi eren en in het nieuwe programma in te plakken. Maar dat is eigenlijk copy/paste programmeren, en dat is niet netjes! Een ander voorbeeld is de interface MouseMotionListener. Deze eist implementatie van twee methodes: mouseMoved en mouseDragged. De eerste reageert op beweging van de muis zonder ingedrukte muisknop, de tweede op beweging van de muis met ingedrukte muisknop. Is een programma ge nteresseerd in het bewegen van de muis ongeacht het indrukken van de knop, dan kun je iets schrijven als:

13.1 Abstracte klassen en interfaces

171

class BewegingReactie implements MouseMotionListener { public void mouseMoved(MouseEvent e) { doe iets nuttigs in reactie het bewegen van de muis } public void mouseDragged(MouseEvent e) { mouseMoved(e); } }

De methode mouseDragged roept de methode mouseMoved aan, zodat in reactie op het bewegenmet-ingedrukte-knop hetzelfde gebeurt als in reactie op het bewegen-zonder-ingedrukte-knop. Op deze manier wordt duplicatie van het stuk programma doe iets nuttigs voorkomen. Toch ligt copy/paste programmeren op de loer: schrijf je een ander programma dat moet reageren op het bewegen van de muis ongeacht het indrukken van de muisknop, dan is het verleidelijk om een kopie van de methode mouseDragged in te plakken. Abstracte klassen Om aan de verleiding van copy/paste weerstand te bieden, en aldus toch het once and only once principe te kunnen volhouden, is het in Java mogelijk om een interface gedeeltelijk te implementeren. Je kunt bijvoorbeeld een klasse maken die wel de methode mouseDragged implementeert, maar nog niet de methode mouseMoved. Zon klasse is een gedeeltelijke implementatie van de interface mouseMotionListener. Om geen ruzie te krijgen met de compiler, moet je in de header van zon klasse aangeven dat het een zogeheten abstracte klasse betreft:
abstract class MuisBewegingOngeachtKnop implements MouseMotionListener { public void mouseDragged(MouseEvent e) { mouseMoved(e); } }

Bij het programmeren heb je misschien onbedoeld al kennisgemaakt met abstracte klassen. Als je bij de implementatie van een interface namelijk een van de methoden vergeet geeft de compiler niet de foutmelding je bent een methode vergeten!, maar als je een methode wilt weglaten, moet je de klasse abstract maken. Het heeft wel zijn prijs om een klasse abstract te maken: van een abstracte klasse kun je niet met new een object maken. Ook dit ben je misschien al onbedoeld tegengekomen: als je een van de benodigde methoden van een interface vergeten bent te implementeren, en je volgt de suggestie van de compiler om de klasse dan maar abstract te maken klakkeloos op, dan krijg je bij de volgende compilatie meteen weer een nieuwe foutmelding: cannot instantiate abstract classes, oftewel: van een abstracte klasse kun je geen nieuwe objecten maken. Wat heeft zon abstracte klasse dan voor zin, als je er niet eens objecten van kunt maken? Als losstaande klasse is een abstracte klasse inderdaad zinloos. Het is dan ook de bedoeling dat een abstracte klasse verder wordt uitgewerkt in subklassen, die de ontbrekende methoden alsnog aanvullen. Die subklassen zijn niet abstract (je zou kunnen zeggen: ze zijn concreet, maar dat hoeft je in de header niet op te schrijven), en daar kun je dus wel objecten van maken. In een programma kun je bijvoorbeeld twee subklassen maken, die op verschillende manieren kunnen reageren op muisbeweging:
class MuisBeweging1 extends MuisBewegingOngeachtKnop { public void mouseMoved(MouseEvent e) { doe iets in reactie het bewegen van de muis } } class MuisBeweging2 extends MuisBewegingOngeachtKnop { public void mouseMoved(MouseEvent e) { doe iets anders in reactie het bewegen van de muis } }

Beide subklassen proteren van het feit dat in de abstracte superklasse wordt vastgelegd dat mouseDragged hetzelfde doet als mouseMoved. En dat zonder copy/paste van mouseDragged!

172

Objectgeori enteerd ontwerp

Adapters Je kunt abstracte klassen gebruiken, zoals in het voorbeeld hierboven, om een soort default-gedrag vast te leggen, die dan in subklassen nader wordt verjnd. Een wel heel simpele vorm van defaultgedrag is doe niets. Veel listener-interfaces, zoals MouseListener en WindowListener speciceren hele rijen methodes, waarvan er in de praktijk veel niets hoeven te doen. Je zou dus voor elke interface een bijbehorende abstracte klasse kunnen maken die elke methode een lege body geeft. Omdat dit een handig idee is, dat door elke Java-programmeur kan worden gebruikt, zijn die abstracte klassen in de Java-bibliotheek alvast neergezet. Zo is er bijvoorbeeld voor de interface WindowListener
interface WindowListener { void windowClosing (WindowEvent void windowClosed (WindowEvent void windowOpened (WindowEvent void windowIconified (WindowEvent void windowDeiconified(WindowEvent void windowActivated (WindowEvent void windowDeactivated(WindowEvent } e); e); e); e); e); e); e);

een bijbehorende abstracte klasse, die alle gespeciceerde methoden van een lege body voorziet:
abstract class WindowAdapter implements WindowListener { public void windowClosing (WindowEvent e) {} public void windowClosed (WindowEvent e) {} public void windowOpened (WindowEvent e) {} public void windowIconified (WindowEvent e) {} public void windowDeiconified(WindowEvent e) {} public void windowActivated (WindowEvent e) {} public void windowDeactivated(WindowEvent e) {} }

Hoewel alle methoden zijn ge mplementeerd, is de klasse toch abstract gemaakt, omdat het niet de bedoeling is om nieuwe objecten van het type WindowAdapter te maken. Maar wel kun je in je eigen programma een subklasse maken van WindowAdapter, waarin je een of meer methoden van een zinvolle inhoud voorziet. Bijvoorbeeld:
class WindowSluiter extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(); } }

Van deze klasse kun je wel nieuwe objecten maken, die je kunt gebruiken op de plaats waar je een WindowListener nodig hebt:
hoofdwindow.addWindowListener( new WindowSluiter() );

Naast WindowAdapter als triviale implementatie van WindowListener voorziet de Java-bibliotheek ook in MouseAdapter als triviale implementatie van MouseListener, en MouseMotionAdapter als triviale implementatie van MouseMotionListener. Hoe de triviale implementatie van KeyListener heet is dan niet moeilijk meer te raden.

13.2 Collections

173

13.2

Collections

De beperkingen van arrays Als je grote hoeveelheden gegevens van hetzelfde type in een programma wilt verwerken, kun je die opslaan in een array. Met een tellertje in een for-opdracht kun je de elementen van een array langslopen, maar je kunt ook naar believen de elementen van een array kris-kras door elkaar benaderen, om hun waarde te bekijken of eventueel te veranderen. Dit is een zeer krachtig hulpmiddel, en de array is dan ook al zo oud als de geschiedenis van enteerd perspectief een array eigenlijk een programmeertalen. Toch is vanuit modern object-geori onding. Dit vanwege het feit dat de mogelijkheden van een array precies vastliggen, en ingebouwd zijn in de taal: je kunt m declareren: String [] a; je kunt m cre eren: a = new String[100]; je kunt een waarde op een bepaalde plaats veranderen: a[n] = s; je kunt de waarde op een bepaalde plaats bekijken: s = a[n]; je kunt de bij creatie vastgelegde lengte later nog eens opvragen: x = a.length; Soms is dit precies wat je nodig hebt, en dan is er geen probleem. Maar soms zou je nog andere dingen met een array willen kunnen doen, bijvoorbeeld een extra element tussenvoegen, of de array langer maken dan bij zn oorspronkelijke creatie is vastgelegd. Je zou voor dat soort dingen eigenlijk methoden willen kunnen toevoegen in een subklasse. Maar hoewel een array een object is, geschiedt de toegang niet via methodes, maar via de speciale vierkante-haakjesnotaties. Ook heeft een array-object geen bijbehorende klasse, waarvan je een subklasse zou kunnen maken. In andere gevallen biedt een array juist meer dan je nodig hebt. Soms wil je bijvoorbeeld een gegevensverzameling opbouwen (strings die een gebruiker intikt ofzo), en die later nog eens in dezelfde volgorde langlopen (om ze op het scherm te tekenen ofzo). Je kunt daarvoor natuurlijk een array gebruiken, maar van de faciliteit dat je zon array kriskras kunt benaderen maak je helemaal geen gebruik: je wilt hem immers alleen maar op volgorde langslopen. Voor de faciliteit die je niet eens nodig hebt betaal je wel een dure prijs, namelijk dat je de lengte van de array vooraf moet vastleggen. Sommige beperkingen van een array zijn wel te omzeilen. De vaste lengte bijvoorbeeld: als je die dreigt te overschreiden, kun je snel een nieuwe array cre eren met de dubbele lengte van de vorige, en de oude elementen daarnaartoe kopi eren. Of de mogelijkheid om elementen tussen te voegen: als je eerste alle overige elementen een plaatsje opschuift, kun je dat in een array wel voor elkaar krijgen. Het wordt al snel een heel gedoe met indexen en hulptellertjes. Maar als je dat netjes opbergt in methoden, kun je een klasse maken waar achter de schermen weliswaar een array wordt gebruikt, maar waar je bij het gebruik van die klasse geen last van hebt, omdat dat door de diverse methoden wordt afgeschermd. De klasse ArrayList Die klasse bestaat natuurlijk al: hij heet ArrayList, en bevindt zich in het package java.util. Er kan hetzelfde soort dingen mee als met een gewone array: je kunt m declareren: ArrayList<String> a; je kunt m cre eren: a = new ArrayList<String>(); je kunt een waarde op een bepaalde plaats veranderen: a.set(n, s); je kunt de waarde op een bepaalde plaats bekijken: s = a.get(n); je kunt de huidige lengte opvragen: x = a.size(); je kunt een element op een bepaalde plaats invoegen: a.add(n, s); je kunt een element aan het eind achtervoegen: a.add(s); Het veranderen en opvragen van elementen is nu mogelijk via gewone methoden: set en get; er is dus geen bijzondere notatie nodig met vierkante haken. Ook bij de declaratie treen we die niet aan. Het opvragen van de lengte gaat via een gewone aanroep van de methode size, en dus niet via zon rare pseudo-membervariabele als length. Anders dan bij een array hoeven we bij een ArrayList niet de maximale lengte op te geven bij creatie: een arraylist kan groeien op het moment dat dat nodig is. Bij de declaratie en de creatie is echter wel iets anders aan de hand: we moeten opgeven wat het type is van de elementen die in de arraylist zullen worden opgeslagen (in het voorbeeld is dat String). Dit vormt een uitbreiding van de syntax van type:

174

Objectgeori enteerd ontwerp

type
klasse

, <

type

>

naam
boolean int double char
Achter een klassenaam kunnen dus tussen punthaken nog een of meer types vermeld worden. Let op het nieuwe soort haakjes: geen ronde haakjes, geen accolades, geen vierkante haakjes, maar punthaakjes. Tussen de punthaakjes staat de type-parameter van de klasse ArrayList. De typeparameter maakt ArrayList tot een zogeheten generiek type: het is een type dat in meerdere situaties gebruikt kan worden (zoals ArrayList<String> en ArrayList<Color>). Een voorbeeld van het gebruik van ArrayList staat in listing 32. Methode init maakt een eenvoudige userinterface waar de gebruiker een tekstveld kan intikken. Bovendien cre eert deze methode een ArrayList waarin strings kunnen worden opgeslagen. Methode actionPerformed pakt de ingetikte string en zet die aan het eind van een ArrayList. In methode paint worden alle tot nu toe ingetikte strings op het scherm gezet. De klasse LinkedList In package java.util zijn nog andere generieke klassen te vinden, zoals bijvoorbeeld LinkedList. Deze klasse heeft precies dezelfde methodes als ArrayList: onder andere get, set, size en tweemaal add. Deze methodes hebben ook precies dezelfde werking. Waarom zijn er twee klasses met precies dezelfde functionaliteit? Omdat de tijd die nodig is voor de aanroep van methodes verschillend is: wat de ene klasse snel kan kost de andere veel tijd, en omgekeerd. Zo kun je de klasse kiezen die de methode die je vaak nodig hebt eci ent uitvoert. De namen van de klasses suggereren al wat het verschil is: een ArrayList houdt achter de schermen een array bij, terwijl een LinkedList een aan keten van naar elkaar verwijzende objecten bijhoudt. Dit maakt dat een ArrayList snel is in dingen die gemakkelijk kunnen in een array: opzoeken met get, en vervangen met set. Invoegen met add is echter steeds langzamer naarmate het verder van het einde gebeurt: in zon geval moeten namelijk alle overige elementen worden opgeschoven. In een LinkedList is nou juist het tussenvoegen van een element snel te doen: kwestie van het openbreken van een schakeltje en twee verwijzingen veranderen. Maar hier is het opzoeken van een element met get weer lastig, omdat je alle schakels moet langslopen tot je de gewenste index hebt gevonden. Ook het geheugengebruik van de twee klassen verschilt. Een ArrayList kan goed omgaan met structuren waarbij een de index-nummers compact liggen: bijvoorbeeld 80 elementen in gebruik die allemaal een index tussen 0 en 100 hebben. Een LinkedList is juist goed in ijle structuren, zoals een structuur met drie elementen op de plaatsen 1, 170, en 1057823. De interface List Het is geen toeval dat ArrayList en LinkedList precies dezelfde methodes aanbieden. Ze zijn allebei een implementatie van de interface List die speciceert dat deze methodes moeten bestaan. Die interface zou er dus als volgt uit kunnen zien (in het echt is hij uitgebreider):
interface List<E> { boolean isEmpty int size E get E set boolean add void add }

byte short long float

[]

blz. 175

() (); (int index); (int index, E waarde); (E waarde); (int index, E waarde);

13.2 Collections

175

import import import import


5

java.awt.*; java.awt.event.*; java.applet.Applet; java.util.ArrayList;

public class ArrayListDemo extends Applet implements ActionListener { TextField invoer; ArrayList<String> alles;
10

15

public void init() { invoer = new TextField(20); this.add(invoer); invoer.addActionListener(this); alles = new ArrayList<String>(); } public void actionPerformed(ActionEvent e) { alles.add( invoer.getText() ); invoer.setText(""); this.repaint(); } public void paint(Graphics g) { int y = 50; for (int i=0; i<alles.size(); i++) { g.drawString(alles.get(i), 10, y); y += 20; } } private static final long serialVersionUID = 1; } Listing 32: ArrayListDemo/src/ArrayListDemo.java

20

25

30

176

Objectgeori enteerd ontwerp

blz. 175

Bij de declaratie van een generieke klasse of interface staat er tussen de punthaken een typevariabele, zoals E in het voorbeeld. Die type-variabele speelt in de body de rol van een type, zoals in het resultaat van get en de parameter van add. Verrassend is het resultaat van de methode set: die is niet void, wat je zou verwachten als je de aanroep in listing 32 ziet, maar E. Het gaat hier om de oorspronkelijke waarde die in de lijst stond voordat hij door set is veranderd. In bepaalde situaties kan dat van pas komen, en als je het niet nodig hebt, dan kun je het resultaat gewoon negeren. Merkwaardig is dat e en van de twee add-methodes een boolean resultaat heeft. Die resultaatwaarde is altijd true, dus erg nuttig is het niet. Waarom het resultaattype niet gewoon void is zal verderop blijken. De abstracte klasse AbstractList In de interface List wordt ook een methode isEmpty gespeciceerd. De betekenis is duidelijk: deze methode levert op of de lijst leeg is. Erg essentieel is deze methode niet, want daar kun je ook achter komen door te kijken of de size nul is. Maar het maakt een programma overzichtelijker als daarin een test if (a.isEmpty()) staat, in plaats van het meer omslachtige if (a.size()==0). Het zou niet erg once and only once zijn als ArrayList en LinkedList (en wie weet hoeveel andere implementaties van List) allemaal apart de methode isEmpty zouden moeten implementeren. Hier komt een abstracte klasse dus goed van pas: deze kan de methode isEmpty deni eren met behulp van een aanroep van size, zonder nog te hoeven weten hoe die methode size ge mplementeerd is. Dat is dan ook het abstracte er aan. In dezelfde stijl kan een van de methodes add (die met 1 parameter) uitgedrukt worden in de andere:
abstract class AbstractList<E> implements List<E> { boolean isEmpty() { return this.size()==0; } boolean add(E x) { this.add( this.size(), x); return true; } }

De klassen ArrayList en LinkedList zijn subklassen van deze abstracte klasse. Ze hoeven dus isEmpty() en add(E x) niet meer te deni eren. Het staat ze overigens vrij om dat t` och te doen, bijvoorbeeld omdat ze het sneller kunnen dan de generieke implementatie in AbstractList. Vooral LinkedList heeft een goede reden om isEmpty zelf opnieuw te deni eren. (Kun je bedenken waarom?) In een diagram kunnen we de klasse-hi erarchie als volgt weergeven: ArrayList en LinkedList zijn subklassen van AbstractList, en AbstractList is een implementatie van List. Let op de verschillende symbolen voor interface, abstract klasse en klasse, en de verschillende lijnen voor extends en implements.
List Abstract List Linked List Array List

De interface Collection In werkelijkheid zit de package java.util nog iets subtieler in elkaar. In guur 26 staat een overzicht van de interfaces en klasses die hieronder worden besproken. Er wordt onderscheid gemaakt tussen twee interfaces: Collection, voor gegevens waarbij de volgorde er niet toe doet; List, voor gegevens die op een lineaire volgorde staan.

13.2 Collections

177

Object

Abstract Collection

Abstract List

Linked List Array List

Collection

List

Abstract Set Sorted Set Abstract Map Sorted Map

Hash Set Tree Set Hash Map Tree Map

Set

Map List Iterator

Iterator

java.util extends

interface

implements

abstract class

class

Figuur 26: Classes en interfaces voor collections

Dit zijn een aantal (niet alle) methodes die gespeciceerd worden in de interface Collection:
interface Collection<E> { boolean add (E x); boolean remove (E x); boolean contains (E x); int size (); boolean isEmpty (); void clear (); }

Alle methodes waarin een index-nummer een rol speelt, staan in de sub-interface List:
interface List<E> extends Collection<E> { E get (int index); E set (int index, E x); boolean add (int index, E x); E remove (int index); }

Er zijn klassen, zoals HashSet, die wel een implementatie zijn van Collection, maar niet van List. Met een object van HashSet kun je dus elementen toevoegen, elementen verwijderen en kijken of een bepaald element aanwezig is. Maar je kunt bijvoorbeeld niet elementen aan het eind toevoegen, want er is niet een aanwijsbaar eind als de spullen op een grote hoop liggen in plaats van op een rijtje. Het langslopen van een Collection Kun je de elementen van een Collection langslopen, zoals dat in listing 32 met de elementen van een List gebeurde? Op het eerste gezicht niet, want je kunt niet met een tellertje via aanroep van get de achtereenvolgende elementen opvragen: er is immers geen methode get, en ook geen volgorde. Toch is dit wel mogelijk. Het is zelfs zo vaak nodig (en in concurrerende programmeertalen zo eenvoudig), dat de Java-ontwerpers de verleiding niet konden weerstaan om hier een speciale notatie voor te introduceren:
blz. 175

178

Objectgeori enteerd ontwerp

opdracht
for (

expr
,

expr

expr
,

opdracht

declaratie type naam : expr


Het gaat om een bijzonder soort for-opdracht. In plaats van de vertrouwde twee puntkommas in de header van de for-opdracht, ziet de header er nu anders uit, bijvoorbeeld:
for ( String s : coll )

blz. 179 blz. 175

Dit moeten we lezen als voor alle strings s in de collectie coll. In de body kunnen we vervolgens direct gebruikmaken van de waarde van s. Het mooie is dat variabele s, die hier ter plaatse ook wordt gedeclareerd, bij elke herhaling automatisch een andere waarde heeft. Geen gedoe met tellertjes, en aanroepen van get en size zijn niet nodig. In listing 33 staat een voorbeeld van het gebruik. Het programma heeft dezelfde opzet als listing 32. Nu declareren we echter in plaats van een ArrayList een Collection. Er hoeft maar op e en plaats een keuze gemaakt te worden voor een concrete implementatie (hier in de methode init). Voor de rest van het programma maakt de keuze niet uit: die kan met elke Collection werken. In de methode paint wordt de bijzondere for-syntax gebruikt om de elementen langs te lopen. Itereren met een Iterator Voor wie morele bezwaren heeft tegen speciale syntax voor een bijzondere situatie, is er nog een andere manier om de elementen van een Collection langs te lopen. Voor dit doel is er namelijk nog een methode beschikbaar in Collection:
interface Collection<E> { ... Iterator iterator (); }

deze methode iterator levert een Iterator-object op. Van dat object kun je vervolgens twee methoden aanroepen: hasNext en next. Oftewel: dat object is zelf weer een implementatie van de interface Iterator:
interface Iterator<E> { boolean hasNext (); E next (); }

Deze twee methoden kennen we trouwens wel van de klasse Scanner, en ze worden dan ook op een vertrouwde manier gebruikt in een while-opdracht:
Iterator<String> iter; iter = coll.iterator(); while (iter.hasNext()) doeIetsMet( iter.next() );

of nog compacter met een for-opdracht:


for (Iterator<String> iter=coll.iterator(); iter.hasNext(); doeIetsMet( iter.next() ); )

In een Collection staan de gegevens niet in een bepaalde volgorde. Het is dus niet zeker dat de Iterator die door iterator wordt teruggegeven de elementen in dezelfde volgorde oplepelt als waarin ze zijn toegevoegd, maar je krijgt ze wel gegarandeerd allemaal te zien. Dit is overigens precies wat er achter de schermen gebeurt als je de speciale for-syntax gebruikt. Het programma wordt dus niet sneller of langzamer van het gebruik van Iterators in plaats van de for-syntax. De interface Set Een andere sub-interface van Collection is Set. Net als bij List gedraagt methode add zich hierin bijzonder. In een Set garandeert add dat elk element hoogstens e en keer in de collectie

13.2 Collections

179

import import import import


5

java.awt.*; java.awt.event.*; java.applet.Applet; java.util.*; ActionListener hoeven we nog niet welke implementatie we kiezen

10

public class CollectionDemo extends Applet implements { TextField invoer; Collection<String> alles; // Bij declaratie // vast te leggen // van Collection public void init() { invoer = new TextField(20); this.add(invoer); invoer.addActionListener(this); alles = new TreeSet<String>(); // // // // }

15

Dit is de enige plaats waar de keuze vermeld wordt. In plaats van TreeSet zijn ook mogelijk: HashSet, ArrayList, LinkedList.

20

25

public void actionPerformed(ActionEvent e) { alles.add( invoer.getText() ); invoer.setText(""); this.repaint(); } public void paint(Graphics g) { int y = 50; for (String s : alles) { g.drawString(s, 10, y); y += 20;

30

// De speciale for-syntax, die geen // lineaire volgorde nodig heeft.

35

} } private static final long serialVersionUID = 1; } Listing 33: CollectionDemo/src/CollectionDemo.java

180

Objectgeori enteerd ontwerp

wordt opgenomen. Gelijkheid wordt hierbij getest met behulp van equals. Zit het element er al in, dan wordt het niet nogmaals toegevoegd, en levert add het resultaat false op. (Dit is dus de reden dat de add-methode een boolean oplevert, en dat deze methode in het geval van een List altijd true oplevert). Behalve dit veranderde gedrag van add zitten er in Set geen nieuwe methoden ten opzichte van Collection. Wel nieuwe methodes zijn er te vinden in de sub-sub-interface SortedSet. Bij implementaties van deze interface zorgt methode add er voor dat de toegevoegde elementen op opklimmende volgorde komen te staan. De iterator zal ze ook in die volgorde opsommen. Nieuw zijn twee methoden die de kleinste en de grootste waarde opleveren:
interface SortedSet<E> extends Set<E> { E first (); E last (); }

Om de volgorde te kunnen bepalen moet de elementen die in een SortedSet worden opgeslagen de interface Comparable implementeren. Voor een aantal standaardklassen, zoals String, Integer en Date, is dat al het geval, en voor je eigen klassen kun je daar ook voor zorgen. Als alternatief kun je aan de constructormethode van de SortedSet een Comparator-object meegeven, die de vergelijkingen uitvoert. Raadpleeg de online documentatie voor de details hiervan. De interface Map Stel je eens voor dat in een array de elementen niet aangeduid zouden worden met een nummer, maar met een String. Dat zou handig zijn: je zou dan bijvoorbeeld een vertaal-tabel kunnen maken tussen het Nederlands en het Engels, door als index de Nederlandse woorden te gebruiken, en als bijbehorende waarde de Engelse vertaling. Om een woord te vertalen hoef je alleen nog maar op de goede plaats de tabel te raadplegen. Zon vertaaltabel is precies wat er wordt gespeciceerd door de interface Map. Het lijkt op een List, maar waar een List de elementen nummert met int-waarden, mag als plaatsbepaling in een Map een willekeurig objecttype gebruikt worden. In de praktijk zijn dat vaak String-waarden. De interface Map is niet alleen generiek in het element-type E, maar ook in het key-type K. De interface speciceert (onder andere) de volgende methoden:
interface Map<K,E> { int size boolean isEmpty void clear E put E remove E get Set<K> keySet Collection<E> values } (); (); (); (K key, E value); (K key); (K key); (); ();

Met put kun je nieuwe vertalingen aan de tabel toevoegen, met remove kun je ze weer verwijderen, en met get kun je de vertaling opzoeken. Van een Map kun je geen iterator krijgen, maar wel kun je alle keys of alle waarden opvragen. Je krijgt dan respectievelijk een Set of een Collection, en daarvan kun je wel een iterator krijgen. Zo kun je, om bij het voorbeeld van de Nederlands-Engelse vertaaltabel te blijven, naar keuze alle Nederlandse of alle Engelse woorden langslopen. De sub-interface SortedMap zorgt er voor dat de vertaaltabel geordend wordt op volgorde van de keys, zodat er twee nieuwe methodes beschikbaar zijn:
interface SortedMap<K,E> extends Map<K,E> { K firstKey (); K lastKey (); }

Automatische boxing/unboxing In arrays kun je naar keuze objectverwijzingen of waarden van e en van de acht primitieve typen (int, char enzovoort) opslaan. In collections kun je uitsluitend objectverwijzingen opslaan. Zou je toch een collectie van bijvoorbeeld ints willen maken, dan moet je elk getal apart inpakken in een objectje, met precies e en int membervariabele.

13.3 Uitbreidingen van AWT

181

Voor dit doel is de klasse Integer beschikbaar. Bij de constructor kun je een int-waarde meegeven, die in het object verpakt wordt. Wil je de waarde van het getal weer gebruiken, dan kun je die opvragen met de methode getValue. Zo kun je dus je collectie opbouwen:
Collection<Integer> coll = new LinkedList<Integer>; coll.add( new Integer(37) ); coll.add( new Integer(42) );

en later weer langslopen:


for (Integer n : coll) verwerk( n.getValue() );

Het blijft desalniettemin gedoe, dat in- en uitpakken. Daarom kan sinds Java (1.)5.0 de compiler dat automatisch doen. We kunnen dus schrijven:
Collection<Integer> coll = new LinkedList<Integer>; coll.add( 37 ); // auto-boxing coll.add( 42 ); // auto-boxing for (int n : coll) verwerk( n ); // auto-unboxing

Achter de schermen wordt de int echter nog wel degelijk in- en uitgepakt in een Integer-object; daarom moeten we toch een Collection<Integer> declareren, en niet een Collection<int>.

13.3

Uitbreidingen van AWT

De beperkingen van AWT In vrijwel alle programmas tot nu toe hebben we het package java.awt gebruikt om een grasche gebruikersinterface te maken. Het opzetten van een interactief programma met AWT is relatief eenvoudig althans als je de werking van ActionListeners eenmaal doorhebt. . . Bijkomend voordeel is dat je proteert van het uitgangspunt van Java: het programma is niet speciek voor een bepaald operating systeem; je kunt een applet bekijken met (vrijwel) elke browser onder (vrijwel) elk operating systeem. Dat verklaart ook de A in de naam AWT: het is een abstracte window toolkit, die je kunt aanspreken zonder te weten welk concreet windowsysteem er aan ten grondslag ligt. Toch, zodra je probeert wat serieuzere programmas te schrijven die aan professionele eisen moeten voldoen, loop je aan tegen de beperkingen van AWT. De twee belangrijkste beperkingen zijn: met de klasse Graphics kun je wel lijntjes, cirkels, letters en zelfs bitmaps tekenen, maar sommige dingen zijn gewoonweg niet mogelijk. De beperking waar de meeste programmeurs het eerst tegenaan lopen is dat de dikte van de door drawLine getekende lijnen niet kan worden ingesteld. in het package java.awt zitten weliswaar klassen voor buttons, tekstvelden, keuzelijsten en checkboxen, maar daarmee heb je het wel zon beetje gehad. Een mooie combobox, zoals je in MS-Windowsprogrammas vaak ziet is niet mogelijk, laat staan zon hippe treeview erarchische structuren kunt visualiseren. waarmee je hi De beperkingen van Graphics zijn het gevolg van ondoordacht ontwerp. De beperkingen zijn inmiddels verholpen in een nieuwe klasse Graphics2D, waarover zo dadelijk meer. De beperkingen van AWT zijn het gevolg van de principi ele keuze die destijds is gemaakt voor een zogeheten zwaargewicht GUI-bibliotheek. Die keuze is op de snelle computers van deze eeuw minder noodzakelijk, en daarmee ligt de weg open naar een alternatieve GUI-bibliotheek. Zwaargewicht en lichtgewicht GUIs Elk modern operating system biedt ondersteuning voor het programmeren van grasche userinterfaces (GUIs). Apple was de eerste commerci ele aanbieder van GUIs met de Mac look&feel, Microsoft volgde met Microsoft Windows, voor Unix heb je Motif, en Linux heeft KDE. Het mooie is dat als je eenmaal gewend bent aan zon operating systeem, je precies weet wat je van de verschillende controls kun verwachten: wanneer menus openklappen, hoe je eventueel met het toetsenbord een button kunt aanklikken, enzovoort. Er zijn verschillen tussen de GUIs van verschillende operating systemen: dat verklaart de aanpassingsproblemen als een Mac-gebruiker op een Windows-systeem moet werken of andersom. Zo lang iedereen merktrouw blijft en zich niet met het andere kamp bemoeit gaat dat goed.

182

Objectgeori enteerd ontwerp

Bij het ontwerp van Java, dat immers beoogt platform-onafhankelijk te zijn, gaf dat wel een probleem. De verschillende platforms bieden verschillende primitieven aan voor het samenstellen van GUIs. Daarom hebben de Java-ontwerpers hun window-toolkit abstract gemaakt, dat wil zeggen dat je het onderliggende window-systeem er niet in herkent. De toolkit vertaalt de abstracte idee en uit het Java-programma naar concrete aanroepen in het onderliggende window-systeem. Dat heeft twee voordelen: de werking van de GUI is snel, omdat gebruik wordt gemaakt van de zwaar geoptimaliseerde primitieven van het operating system. het Java-programma ziet er voor de gebruikers van de diverse operating systemen altijd vertrouwd uit: de buttons en dergelijke hebben precies de vorm die ze gewend zijn. Maar er zijn ook nadelen: Java kan slechts de algemeen gangbare controls aanbieden: buttons zijn er altijd wel, maar de Mac kent geen combobox ` a la Microsoft, en Microsoft kent weer niet de schuifregelaar van Motif. De in AWT beschikbare controls zijn dus beperkt tot de grootste gemene deler van alle systemen. als Java-programmeur weet je nooit zeker hoe de GUI er uit komt te zien: die is immers op elk systeem anders. Dat kan nog voor lelijke verrassingen zorgen. Een GUI-toolkit die zo veel mogelijk beroep doet op het onderliggende operating system heet een zwaargewicht GUI-toolkit. AWT is een zwaargewicht GUI-toolkit. Wat is dan een lichtgewicht GUI? Hierbij wordt zo min mogelijk gebruik gemaakt van het onderliggende operating system. Die moet zijn toetsenbord- en muisakties doorgeven en graphics op het scherm kunnen tekenen, maar zich verder nergens mee bemoeien. De complete vormgeving van de controls, inclusief de driedimensionale suggestie dat een button wordt ingedrukt wordt helemaal door de Java-bibliotheek verzorgd. Tja, is dat nou handig? Een mogelijk bezwaar is de traagheid. Zon simulatie van een button kan nooit zo snel zijn als de in het operating system ingebouwde functie. Dat was ten tijde van het begin van Java een belangrijke reden om AWT zwaargewicht te maken. Maar met de snelle computers van tegenwoordig is een lichtgewicht GUI-toolkit goed haalbaar. Een groot voordeel is dat zon lichtgewicht GUI-toolkit allerlei zeer aantrekkelijke nieuwe controls kan aanbieden. Als het een nieuw ontworpen toolkit is, kan die ook proteren van allerlei moderne enteerde inzichten. object-geori Een dilemma is hoe de lichtgewicht GUI er uit moet komen te zien: ` a la Microsoft? ` a la Motif? of geen van beide, maar juist iets heel nieuws? Java is sinds versie 1.2 voorzien van een lichtgewicht GUI-toolkit, in de vorm van het package javax.swing, kortweg bekend als als Swing. Die is snel genoeg, en inderaad voorzien van allerlei aantrekkelijke controls. Uit het laatste dilemma hebben de ontwerpers zich gered door door Swing te voorzien van een congureerbare look&feel: verwisselbare skins, zogezegd. Standaard zijn er drie look&feels: Motif, Windows, en een nog niet elders bestaande look&feel genaamd Metal. In principe is elke look&feel te gebruiken op elke systeem het is immers een lichtgewicht GUItoolkit, die de look&feel zelf simuleert zonder gebruik te maken van de faciliteiten het operating system. Das leuk: nou kan je Metal-look maken op een Windows-computer, Motif-look op een Mac, of Windows-look op een Unix-systeem, of andere combinaties. Helaas, Microsoft was minder blij met een Windows-look op vreemde systemen. Het gevolg is dat de Microsoft-look nu alleen maar ingesteld kan worden op een Microsoft-computer. De default look&feel is Metal. De gebruiker kan iets anders uitkiezen met een ingewikkelde optie voor de Java-interpreter. Dat kan door de programmeur weer worden overruled door in de methode main de look&feel van de doel-computer te kiezen met:
UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );

of juist de Metal look&feel met:


UIManager.setLookAndFeel( UIManager.getCrossPlatformLookAndFeelClassName() );

In de klasse UIManager zijn nog veel meer methoden beschikbaar, onder andere om te onderzoeken of er naast de drie standaard-vormen wellicht nog extra look&feels beschikbaar zijn. Ajn, daar kun je heel knap in worden door veel in de online documentatie te lezen maar je kunt hem natuurlijk ook gewoon op de default laten staan. . .

13.3 Uitbreidingen van AWT

183

Component

java.awt java.applet javax.swing


Panel Applet JApplet Abstract Button JMenu Item JToggle Button

Container

Window

Frame

JFrame

JMenu JCheck Box JRadio Button

Dialog

JDialog

JWindow

nog 20 meer
JComboBox JLabel JToolTip JMenuBar JPanel JToolBar Box JScrollBar JSlider

JButton

JComponent CheckBox Choice List Button Canvas Label Scrollbar Text Component

TextArea JTable TextField JList JText Area JText Field JEditor Pane JFormatted TextField JPassword Field JText Pane

Menu Component

MenuBar MenuItem Menu

JTree JText Component Abstract Action Image Icon

java.awt.event

Action Listener

Action Icon

Figuur 27: Componenten in Swing (en in AWT)

184

Objectgeori enteerd ontwerp

Klassen in Swing Net als in AWT zit er in Swing voor elk soort interactiecomponent een klasse. Om ze te onderscheiden van de klassen in AWT begint de naam steeds met een J. Dus waar Button een AWT-button beschrijft, is JButton een Swing-button. De klassen voor een toplevel window, zoals JApplet en JFrame, zijn de enige zwaargewicht componenten van Swing. Ze zijn een directe uitbreiding van de overeenkomstige klassen in AWT. Alle andere componenten zijn lichtgewicht; ze zijn een uitbreiding van de abstract klasse JComponent, die op zijn beurt een subklasse is van de AWT-klasse Container. Dat laatste is een beetje raar, want niet elke JComponent is bedoeld om iets in te stoppen. Maar JPanel en Box zijn dat wel, en bij de andere controls, zoals JButton en JSlider heb je er geen last van. Indirect zijn in ieder geval alle componenten ook subklasse van Component. Anders dan bij AWT zijn ook de klassen die met menus te maken hebben (zoals JMenu en JMenuBar) in de hi erarchie opgenomen. Dat heeft als voordeel dat je het menu gewoon in de layout kunt opnemen. Nu kunnen dus ook applets een menu krijgen! Geef je applet een BorderLayout en voeg een JMenuBar-component toe in het noorden. Een verschil tussen JApplet en JFrame enerzijds, en hun AWT-tegenhanger Applet en Frame anderszijds, is dat je aan de Swing-versies niet direct componenten kunt toevoegen met add. In plaats daarvan moet je de methode getContentPane aanroepen. Die levert een Container op, en daar kun je de componenten aan toevoegen. Die container heeft altijd een BorderLayout, ook in applets. Als dat je niet bevalt, kun je hem natuurlijk alsnog een FlowLayout geven. Dit is een voorbeeld van een AWT-applet dat een enkele button maakt:
class Hallo extends Applet { public void init() { Button b = new Button("druk hier"); this.add(b); } }

Dit is de overeenkomstige Swing-versie:


class Hallo extends JApplet { public void init() { JButton b = new JButton("druk hier"); Container c = this.getContentPane(); c.setLayout( new FlowLayout() ); c.add(b); } }

Een laatste verschil tussen AWT en Swing is het gedrag van paint. Door het lichtgewicht karakter van Swing is paint nu ook verantwoordelijk voor het tekenen van de onderdelen van een container. Als je dus een panel hebt met daarin bijvoorbeeld een aantal buttons, maar ook een tekening op de achtergrond, dan heb je een probleem: als je paint zoals gewoonlijk herdeni eert, dan raak je de buttons kwijt! In plaats daarvan moet je in deze situatie de methode paintComponent herdeni eren, die verantwoordelijk is voor het tekenen van de component zonder de onderdelen. Ongelukkigerwijze werkt dit weer niet in het toplevel JApplet, omdat dat geen lichtgewicht component is. Je moet het dus vermijden om in het toplevel applet componenten toe te voegen en een tekening te gebruiken. In plaats daarvan kun je beter een canvas toevoegen, en de tekening daarop maken. In dit verband is het ook belangrijk om op te merken dat er geen klasse JCanvas is. Maar als je een canvas nodig hebt in een Swing-programma kun je voor dit doel een JPanel gebruiken. In sectie 13.4 staat een uitgebreid voorbeeldprogramma, dat gebruik maakt van de Swing-toolkit. Tevens wordt daarin gedemonstreerd hoe je omgaat met een Action. Dat is een interface die in Swing wordt gedenieerd: een geavanceerde versie van ActionListener. Het voert te ver om hier de circa 50 verschillende Swing-componenttypen te bespreken. In guur 27 staan er een aantal opgesomd, sommige worden gebruikt in het voorbeeldprogramma in sectie 13.4. Voor de overige klassen kun je de online documentatie raadplegen op het moment dat je ze nodig hebt.

13.3 Uitbreidingen van AWT

185

Graphics2D Zodra je in Java een vrije tekening wil maken, krijg je te maken met de klasse Graphics. Een objecten van deze klasse vervult verschillende rollen: Het is de abstractie van het medium waarop getekend wordt. Dat kan een component zijn, maar ook een bitmap in het geheugen, of een printer. Het heeft geheugen voor een aantal eigenschappen de huidige tekenkleur, te veranderen met setColor het huidige lettertype, te veranderen met setFont een rechthoek waarbuiten niets getekend mag worden, te veranderen met setClip de oorsprong van het co ordinatenstelsel, te veranderen met translate de huidige teken mode : XOR of gewoon de achtergrondkleur: niet veranderbaar, maar wordt gebruikt bij clear Het biedt methoden om iets te tekenen lijnen met drawLine open guren met drawRect, drawOval, en nog vier andere opgevulde guren met fillRect, fillOval, en nog vier andere tekst met drawString tekeningen met drawImage Dat is tamelijk beperkt. Er is bijvoorbeeld geen eigenschap huidige lijndikte, je kunt geen curves (anders dan cirkelbogen) tekenen, en je kunt guren weliswaar helemaal opvullen, maar niet met een stippelpatroon. Deze tekortkomingen worden verholpen in een subklasse van Graphics genaamd Graphics2D. (In de toekomst komt er hopelijk ook nog eens een Graphics3D). Maar hoe krijg je zon Graphics2Dobject te pakken met die extra mogelijkheden? Het antwoord is dat (vanaf versie Java 1.2) zon Graphics2D-object gewoon aan paint wordt meegegeven. Men heeft in Java 1.2 de header van paint echter niet meer willen wijzigen, omdat oude programmas dan niet meer bruikbaar zouden zijn. Maar als je zeker weet dat je nieuwe programma ook met een Java-interpreter verise 1.2 of nieuwer wordt bekeken, kun je de parameter van paint gewoon casten naar Graphics2D:
public void paint(Graphics g) { Graphics2D g2 = (Graphics2D) g; g2.setStroke( new BasicStroke(10) ); g2.drawLine(0,0,100,100); } // cast // zet lijndikte // teken een dikke lijn

Wat kan je met zon Graphics2D? In ieder geval alles wat met Graphics ook kan, want het is een subklasse. Maar daarnaast heeft een Graphics2D-object: extra geheugen voor een aantal nieuwe eigenschappen: de nu wel wijzigbare achtergrondkleur de huidige lijn-stijl, bestaande uit lijndikte, stippeligheid, en vorm van de eindpunten de huidige opvul-stijl: massief (solid ), met een patroon (texture ), of een overvloeiende kleur (gradient ) de huidige manier waarop nieuwe kleuren worden gecombineerd met oude de huidige draaiing en/of vergroting enkele opties (hints ) betreende de tekenkwaliteit nieuwe methoden om iets te tekenen een generieke methode draw en fill die een willekeurig Shape-object kan tekenen. In het apart te construeren Shape-object kun je een simpele rechthoek, maar ook een ingewikkelde curve speciceren. Het voorbeeld hierboven demonstreert hoe je de lijndikte kunt instellen: maak een BasicStrokeobject, en geef dat mee aan setStroke. Behalve de hier gebruikte constructormethode van BasicStroke die alleen de lijndikte meekrijgt, zijn er ook varianten waarmee je andere eigenschappen van de getekende lijnen kunt instellen. Zie de online help voor de details. De opvul-stijl speciceer je door aan setPaint hetzij een TexturePaint-object of een GradientPaint-object mee te geven, of een simpele Color voor massieve invulling. Met een aanroep van setRenderingHint kun je nog wat extra aanwijzingen geven over de kwaliteit van de tekening. Hoe hoger de kwaliteit, hoe langer het meestal gaat duren. Niet elk Graphicsobject kan elke hint opvolgen, maar ze doen hun best. Dit is een voorbeeld van gebruik van deze

186 methode:

Objectgeori enteerd ontwerp

g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );

Met deze hint zal het Graphics-object proberen om ronde en schuine lijnen een zachte rand te geven, zodat er niet zon karakteristiek zigzagpatroon ontstaat. Het kan wat langer duren, maar het wordt wel mooier. En als het je niet bevalt, zet je het weer . . . OFF.

13.4

Toepassing: een schets-programma

Beschrijving van het programma In deze sectie schrijven we een compleet tekenprogramma. In guur 29 is het programma in werking te zien. Anders dan de bitmap-editor in het vorige hoofdstuk kent dit programma tools waarmee je verschillende guren kunt tekenen: pennetje, lijntje, open en gesloten rechthoekje, en een gummetje om weer te kunnen wissen. Ook is er een tool om tekst toe te voegen aan de tekening. Behalve de tools zijn er onderin het window nog wat bedieningselementen, die we ter onderscheid maar controls zullen noemen: een knop om het plaatje leeg te maken, eentje om hem te draaien (die doet het trouwens nog niet: dat is een van de opgaven), en een combobox om de penkleur uit te kiezen. Als de gebruiker een rechthoek-tool uitkiest kan hij/zij een blok trekken op het canvas. Het gekozen gebied wordt met een grijze contour aangegeven, en pas bij het loslaten van de muis wordt de rechthoek denitief getekend. In plaats van met de knoppen langs de linkerrand kunnen de tools ook worden uitgekozen via het uitklapmenu Tool; de functie van de controls is ook beschikbaar via het menu Edit. Zon dubbele bediening komt in professionelere programmas vaak voor, en het is een uitdaging aan de programmeur om dit met inachtneming van het once and only once principe voor elkaar te krijgen. Dat is hier met name van belang, want er komt in een latere versie van het programma vast nog wel eens een tool bij, en het zou mooi zijn als die met e en wijziging aan het programma zowel in de toolbox als in het menu terecht komt. Het programma bouwt de gebruikersinterface met behulp van Swing. Er is zowel een applicatieals een applet-versie, maar dankzij Swing kan ook de applet-versie een menu krijgen. Opzet van het programma In dit programma zijn we nu eens niet zuinig met klassen. Het programma bestaat uit maar liefst 22 verschillende klassen. In guur 28 is de subklasse-hi erarchie van deze klassen getekend, en ook hoe ze samenhangen met de bestaande klassen in Swing en AWT. Het is belangrijk om deze guur goed te bekijken alvorens het programma te bestuderen, anders ga je al die klassen door elkaar halen. Zon groot aantal klassen is typerend voor een object-geori enteerd programma. Als je de listings van de aparte klassen leest, zul je zien dat er nergens echt opzienbarende dingen gebeuren. Veel van de listings zijn ook erg kort, en bevatten maar een paar methoden met een handjevol opdrachten. De magie zit hem in de samenwerking van de klassen. Globale opzet van de klassen Voordat we de klassen in detail gaan bekijken, nemen we globaal hun rol in het programma door. Het begint met een idee dat we overnemen van de bitmap-editor: er zijn aparte klasse voor de applicatie, de applet, en het eigenlijke canvas, en er is een klasse die modelleert wat er op dat canvas te zien is. SchetsApplic is het startpunt van de applicatie. Hierin zit ook de methode main die een window aanmaakt. Op dit window is precies e en ding te zien: de complete applet-versie van het programma. De overeenkomstige klasse uit de bitmap-editor moest nog een menu aanmaken, maar dat hoeft hier niet: dankzij Swing gebeurt dit hier in de applet. SchetsApplet is verantwoordelijk voor de opbouw van de interface: het canvas, de toolbox, het menu, en het control panel. Dit is de langste listing, voornamelijk omdat het opbouwen van een interface nu eenmaal veel gedoe geeft. Er is een membervariabele om het canvas in te bewaren (want daar moet nog wel eens wat mee gebeuren), en een om de momenteel uitgekozen tool in te onthouden. En tenslotte is er een boolean die vastlegt of het applet is opgestart als los applet, of als deel van een applicatie; voor beide doeleinden is er een eigen constructormethode.

13.4 Toepassing: een schets-programma

187

java.util
Event Listener Mouse Listener Iterator Collection ArrayList Schets Kleur

java.applet
MouseMotion Listener Key Listener Item Listener Window Listener Action Listener Component Applet

javax.swing
JApplet SchetsApplet

Frame

JFrame

SchetsApplic

Container Window Adapter

JComponent

SchetsCanv Window Sluiter

Action Abstract Action

About Aktie Tool Aktie Control Aktie

java.awt.event

Icon Image Icon Point

Tool Icon

Tekst Icon Tweepunt Icon

javax.swing
Color

Tool Startpunt Tool Tekst Tool Tweepunt Tool

Font

Graphics

Graphics2D Buffered Image

Lijn Tool PenTool GumTool Rect Tool

Image

java.awt

java.awt.image

schets

FillRect Tool

interface

implements

abstract class

extends

class

Figuur 28: De klassen in het schets-programma

188

Objectgeori enteerd ontwerp Het object luistert naar muisbewegingen en toetsenbordakties op het canvas; als daar wat interessants gebeurt, waarschuwt hij de momenteel uitgekozen tool. Verder luistert het object naar keuze-veranderingen in de kleurkeuze-combobox; als daar wat gebeurt, waarschuwt hij het canvas. Het object luistert echter niet naar ingedrukte buttons of gekozen menus: die doen dat zelf (daarover zodadelijk meer). SchetsCanv is het canvas met de eigenlijke tekening. Er is een membervariabele waarin de tekening wordt bewaard, en eentje voor de huidige penkleur. De tekening kan zichzelf tekenen en van afmeting veranderen, dus de methoden van de klasse SchetsCanv doen weinig meer dan verzoekjes over en weer doorspelen. Schets is het model van de tekening. Het bewaren van de huidige tekening besteedt-ie uit aan een BitMap-object. Die klasse hadden we in het vorige hoofdstuk al geschreven, dus die komt hier goed van pas. Als de schets groter moet worden wordt de bitmap vervangen door eerd. een groter exemplaar, waar de oude alvast in wordt gekopi Zoals bekend is een ActionListener een object dat kan reageren op een aktie. Swing denieert een uitbreiding van deze interface: Action. Die beschrijft een luister-actie, maar daarnaast nog een aantal eigenschappen, zoals een naam, desgewenst een toelichting, een afkorting, en een bijbehorend icoon. Om het helemaal makkelijk te maken denieert Swing ook een abstracte klasse AbstractAction die Action implementeert, met alvast wat standaardfuncties voor de meestgebruikte eigenschappen. Een Action-object bundelt alle informatie die van belang is voor een aktie: zowel voor het weergeven in een menu o.i.d. (gebruikmakend van naam en/of icoon), maar ook voor het afhandelen van de aktie. Van dit mechanisme maken we in het programma dankbaar gebruik: we maken drie gespecialiseerde klassen AboutAktie, ToolAktie, en ControlAktie, die de informatie voor de drie soorten akties die in de schets-editor voorkomen vastleggen. Ook voor het gedrag van een icoon biedt Swing een interface: Icon. Een icoon moet zichzelf kunnen tekenen, en moet kunnen zeggen hoe groot-ie is. Swing biedt alvast een implementatie: ImageIcon. Die gebruiken we in het programma voor het icoon van het pennetje en het gummetje. Maar de andere iconen maken we op een andere manier. Die tonen immers een kleine versie van een guur die het programma toch al moet kunnen tekenen op het canvas (lijn, open of gesloten rechthoek, tekst). We werken netjes once and only once door dat te gebruiken bij twee eigen implementaties van Icon: TekstIcon voor de tekst, en TweepuntIcon voor de guren waar een start- en eindpunt een rol speelt (lijn, rechthoek). Om het helemaal mooi te maken zetten we het gemeenschappelijke van deze twee klassen, namelijk de afmeting van het icoon, in een zelfgemaakte abstracte superklasse ToolIcon. Het deni eren van interfaces hoeft niet beperkt te blijven tot de standaard-bibliotheken: je kunt ook zelf in een programma een nieuwe interface deni eren, en vervolgens klassen schrijven die die interface implementeren. Hier komt dat goed van pas voor het begrip Tool. Een tool is iets dat muis- (en toets-)bewegingen op een schets-canvas kan omzetten in iets blijvends. In de interface Tool speciceren we daarom een viertal methoden die dit idee in methode-met-parameter-vorm vastleggen. De abstracte klasse StartpuntTool werkt het idee alvast wat nader uit voor het soort van tools waarbij je eerst een startpunt aanklikt, dat bewaard moet worden om het guur later denitief te kunnen tekenen. Deze klasse is abstract, want wat er dan precies op dat startpunt getekend wordt is in deze klasse nog niet uitgewerkt. In het programma hebben we twee soorten tools die voortborduren op het idee van de StartpuntTool: TekstTool, die een tekst toont op het startpunt, en TweepuntTool voor de tools die behalve een startpunt ook nog een tweede punt nodig hebben. Die laatste is abstract, omdat we nog niet hebben uitgewerkt wat er dan met die twee punten gebeurt. LijnTool en RectTool zijn twee concrete invullingen van het abstracte TweepuntTool. FillRectTool werkt voor de contour hetzelfde als RectTool, maar voor het denitief tekenen een beetje anders. Door het een subklasse van RectTool te maken wordt once and only once het contour-tekenen uitgeprogrammeerd. Zelfs PenTool blijkt nog weer wat gemeenschappelijk te hebben met LijnTool: een pentekening bestaat immers uit een serie (korte) lijnen. Een gum-spoor is in feite niets anders dan een dikke, witte lijn, dus wordt GumTool een subklasse van PenTool.

13.4 Toepassing: een schets-programma

189

We zullen nu de details van een aantal van deze klasse wat uitgebreider bespreken. Neem steeds de betreende listing erbij! De klasse SchetsApplet De tekst van de klasse SchetsApplet is verpreid over vier listings, vanaf listing 34. In listing 34 staat de methode init. Hier wordt de GUI opgebouwd: het canvas, de toolbar, het controlpanel, en het menu. Eerst worden de akties verzameld die in respectievelijk de toolbar en het controlpanel nodig zijn. Voor de overzichtelijkheid gebeurt dat in twee aparte methoden maakToolAkties en maakControlAkties. Collections komen hierbij goed van pas om de lijst akties op te slaan (al had het eventueel ook wel met een array gekund). De verkregen collections worden doorgespeeld aan drie methoden, die de eigenlijke toolbox, controlpanel en menu opbouwen. Ook dit gebeurt voor de duidelijkheid in aparte methoden. Die methoden leveren een Component op, die aan de verschillende randen van het applet worden neergezet. Of liever gezegd: de randen van de met getContentPane verkregen container, want zo moet dat in Swing-programmas. De andere niet-triviale methode in deze listing is getImageIcon. Deze roept de juiste constructor van de Swing-klasse ImageIcon aan, afhankelijk van het feit of we een los applet zijn of deel uitmaken van een application. In listing 35 (nog wel deel van de klasse SchetsApplet) staan de methoden die de aktie-lijsten samenstellen. Het enige wat de buitenwereld (d.w.z. de methode init) van ze vraagt is dat ze een Collection opleveren. Intern in de methode moeten we de keuze maken welk van de mogelijke collections (zie de hi erarchie in guur 26) we gebruiken. Omdat het wel handig is als de elementen in dezelfde volgorde in het menu verschijnen als we ze hier opsommen, moeten we voor een list kiezen en niet voor een set. Welk van de twee concrete lists we kiezen maakt niet zoveel uit: in plaats van de hier gekozen LinkedList had dat ook ArrayList kunnen zijn. De lijsten zijn opgebouwd uit allemaal losse Action-objecten, of meer speciek: ToolAkties in maakToolAktie en ControlAkties in maakControlAkties. Als je nauwkeurig de parameters telt, zie je dat er twee verschillende contructoren zijn van ToolAktie: eentje voor akties met een grasch icoon, en eentje voor akties met een zelfgetekend icoon. Die constructoren worden gedenieerd in listing 45. De details van het opbouwen van de GUI staan in listing 36. Het controlpanel is inderdaad een JPanel, die uit twee onderdelen bestaat: de rij buttons, die worden ondergebracht in een JToolBar, en de JComboBox voor de penkleurkeuze. De akties worden met behulp van de bijzondere syntax van de for-opdracht, di over alle elementen van een collection intereert, e en voor e en met add aan de toolbar toegevoegd. Het handige van een JToolBar is dat je daar Actions direct aan kunt adden; het maken van de button en het koppelen van de ActionListener gebeurt dan automatisch. Alle benodigde informatie kan-ie immers in de Action vinden. Ook JMenu lust Actions rauw, dus het opbouwen van de drie menus is eenvoudig. Voor het maken van de toolbox hadden we ook weer een JToolBar kunnen gebruiken. Maar die maakt buttons zonder plaatje, en in zon toolbox zijn die icoontjes nou juist zo leuk. Voor de toolbox kiezen hier dus juist niet voor een JToolBar, maar voor een eenvoudige Box (dit is de enige Swing-component die niet met een J begint). Dat heeft als prijs dat we nu zelf de buttons moeten maken en van actionlistener voorzien. Dat geeft meteen een idee van het soort werk dat JToolBar achter de schermen doet. Het vullen van de combobox gebeurt door het aanroepen van addItem. Die methode lust elk object dat de methode toString kent. Zon beetje alles dus, en in ieder geval objecten van de speciaal voor dit doel gemaakte klasse Kleur (zie listing 41). Het vierde en laatste deel van SchetsApplet staat in listing 37. Hier worden de diverse event-listen methodes gedenieerd. De relevante muis-methoden (maar niet de irrelevante) en keyTyped worden aan de momenteel geselecteerde Tool doorgespeeld, die daarbij ook het canvas krijgt doorgespeeld: daar moeten de tools immers hun eect op uitoefenen. De interface Tool (zie listing 49) is precies gemaakt om dit soort boodschappen in ontvangst te nemen. De methode itemStateChanged reageert op het veranderen van de keuze in de kleurkeuzecombobox. Hier vissen we de gekozen kleur uit het Kleur-object, en maken dat tot de huidige penkleur, die door het canvas wordt beheerd.
blz. 192 blz. 192

blz. 193

blz. 200 blz. 194

blz. 197 blz. 195

blz. 202

190

Objectgeori enteerd ontwerp

blz. 196 blz. 198

blz. 198

De klasse SchetsCanv en diens model Schets De klasse SchetsCanv (zie listing 39) beheert zoals gezegd een penkleur, en heeft daartoe een private Color-variabele. Door aanroep van setPenkleur kan die door anderen worden veranderd. Een tweede variabele bevat een Schets-object, die op zijn beurt (zie listing 42) een BufferedImageobject bevat. We gaan er voor zorgen dat die steeds een bitmap van het plaatje bevat, zodat het complete plaatje steeds gemakkelijk getekend kan worden. In de klasse SchetsCanv is er daarom de beschikking over twee verschillende Graphics-objecten: Elke Component heeft een Graphics tot zijn beschikking, die je kunt opvragen met erfd door SchetsCanv, en we gaan hem gebruiken getGraphics. Deze methode wordt ge om tijdelijk op het canvas te kunnen knoeien terwijl de gebruiker bezig is om een blok te trekken. Een BufferedImage heeft ook een Graphics tot zijn beschikking. De klasse Schets maakt die toegankelijk via methode getBitmapGraphics, en die gaan we gebruiken om de permanente tekening in te bewaren. Om ook de buitenwereld hier toegang toe te geven, stelt ook SchetsCanv een methode getBitmapGraphics ter beschikking. Merk op dat de buitenwereld niet om de huidige penkleur kan vragen. De variabele is private, en er is geen methode getPenkleur. In plaats daarvan wordt de penkleur alvast geselecteerd als de buitenwereld om de permanente Graphics vraagt. Een Schets moet zichzelf kunnen tekenen. Hij doet dat (zie listing 42) simpelweg door zijn bitmap te tekenen. Het enige wat Schets verder nog doet is de bitmap vervangen door een grotere, wanneer dat nodig is. De Icon-hi erarchie Volgens de specicatie van de interface Icon in Swing is een icon een object dat zijn breedte en hoogte kan meedelen, en dat zichzelf op een bepaalde plaats op een Graphics kan tekenen. In Swing is er al een implementatie ImageIcon, die we gebruiken voor het icon van het pennetje en het gummetje. Voor onze zelfgemaakte icons hebben we om te beginnen de abstracte klasse ToolIcon in listing 46. Deze implementeert twee van de drie gevraagde methoden: het opvragen van de breedte en de hoogte. Subklasse TekstIcon in listing 47 voegt de derde methode toe, die een symbolische tekst van het juiste formaat tekent. Een andere invulling van het tekenen wordt gegeven in subklasse TweepuntIcon in listing 48. Voor het eigenlijke tekenen doet deze een beroep op het TweepuntToolobject dat bij constructie wordt meegegeven. De Aktie-hi erarchie Een Action is, volgens de specicatie van deze interface in Swing, een uitgebreide ActionListener. Er moet dus in ieder geval een methode actionPerformed zijn. De uitbreiding bestaat daaruit, dat een Action ook een aantal eigenschappen heeft, zoals naam, toelichting en icoon. Al onze drie implementaties van Action deni eren dus in ieder geval een methode actionPerformed. De klasse AboutAktie (zie listing 43) vult die in met het tonen van een informatie-dialoog. Zie je meteen hoe je dat doet. De naam "About" (die in het menu wordt getoond) wordt doorgespeeld aan super, de constructormethode van de superklasse AbstractAction. De klasse ControlAktie (zie listing 44) maakt in actionPerformed het canvas schoon, als tenminste de naam van het event Clear is. De identiteit van het canvas moet aan de constructormethode worden meegegeven, en wordt daar bewaard voor later gebruik. De overige parameters van de constructor woren doorgespeeld aan de superklasse. De klasse ToolAktie (zie listing 45) geeft de invulling voor actionPerformed voor de tools (pennetje, lijntje, enz). We geven simpelweg aan de applet door welke tool vanaf dit moment moet worden gebruikt. De voor deze action relevante tool moet bij constructie worden meegegeven, en wordt voor gebruik in actionPerformed bewaard in een private variabele. Ook de identiteit van de applet wordt op die manier bewaard. Verder heeft de constructormethode een naam en een toelichting als parameter, die aan de superklasse worden doorgespeeld. De vijfde parameter van de constructor tenslotte is een Icon. Die hebben we in listing 35 ingevuld met een ImageIcon van het pennetje of het gummetje. Maar er zijn ook vier-parameterversies van de constructormethode: in dat geval wordt, afhankelijk van het type van de vierde parameter, een TekstIcon of een TweepuntIcon gemaakt, waarmee alsnog de vijf-parameterversie wordt aangeroepen.

blz. 201 blz. 201 blz. 201

blz. 199

blz. 199

blz. 200

blz. 193

13.4 Toepassing: een schets-programma

191

De Tool-hi erarchie De interface en de klassen in deze hi erarchie zijn speciek voor dit programma ontworpen. Ze borduren dus niet voort op een interface in Swing. In de interface Tool (zie listing 49) leggen we vast wat we voor dit programma eigenlijk onder een tool verstaan: een object dat als reaktie op het indrukken, verslepen, of loslaten van de muis iets kan doen met een punt en een SchetsCanvas. Het feit dat er een SchetsCanvas bij betrokken is maakt het natuurlijk speciek voor dit programma. Herinner je dat we het applet zo hebben ingericht dat bij er bij mouse-events altijd een van deze methoden wordt aangeroepen van de op dat moment actieve tool. Van deze interface kunnen we vervolgens diverse implementaties gaan maken. Het begint met een abstracte klasse StartpuntTool (zie listing 50), die alvast e en methode invult. Op het indrukken van de muis reageert zon startpunt-tool door dat punt voor later gebruik te bewaren in een speciaal daarvoor gedeclareerde variabele. De klasse TekstTool in listing 51 is een concrete uitbreiding daarvan. Bij het slepen en loslaten van de muis hoeft er niets te gebeuren, maar bij het intikken van een letter moet deze letter worden afgebeeld op de positie van het eerder bewaarde startpunt. Daarna moet de x-coordinaat van het startpunt worden verhoogd. Met hoeveel precies hangt af van de eigenschappen van het font. Het is een beetje gepeuter om dat te berekenen, maar gelukkig kun je het zo gek niet bedenken of Java heeft er een klasse voor. In dit geval is FontRenderContext wat we nodig hebben. De klasse TweepuntTool in listing 52 is een andere uitbreiding van StartpuntTool. Deze reageert op het slepen van de muis met het tekenen van een grijze contour. Bij het loslaten van de muis wordt de guur denitief getekend. Toetsindrukken worden genegeerd. Daarmee zijn alle vier de methoden van de interface Tool ingevuld, maar er zijn twee nieuwe bijgekomen: tekenContour en tekenFiguur. We kunnen tekenFiguur alvast een default-invulling geven, namelijk dat hij ook alleen de contour tekent. Maar hoe je een contour tekent (lijn? rechthoek? cirkel?) kunnen we nog niet vastleggen. Deze methode wordt daarom abstract gedeclareerd, en hoeft daarom geen body te hebben. De prijs is dat ook de klasse daarmee nog abstract is. Wel concreet zijn de subklassen LijnTool in listing 53 en RectTool in listing 54. Deze geven ieder een eigen invulling aan de ontbrekende methode tekenContour. De default-invulling dat het denitieve tekenen net zo gebeurt als het voorlopige, is voor deze twee tools in orde. De klasse FillRectTool in listing 55 erft het tekenen van de contour van RectTool, maar geeft het denitieve tekenen van de guur een nieuwe invulling, die het default-gedrag vervangt. De guur wordt nu ingevuld getekend met behulp van fillRect. De implementatie van PenTool in listing 56 is subtiel. Deze herdenieert de methode muisVersleept: bij elk verslepen van de muis wordt nu zogenaamd de muis even losgelaten en weer ingedrukt. Die methoden waren al ingevuld met het tekenen van een lijn tot hier, en het beginnen van een nieuw lijnsegment. Het gevolg is dat de pen-tool bij elke muisbeweging (met ingedrukte muisknop) een klein lijnstukje tekent en aan een nieuw lijntje gaat beginnen. Dat is precies wat we nodig hebben voor een pen. De klasse GumTool in listing 57 tenslotte is weer een verjning van PenTool. Bij het tekenen van de contour (en daarmee ook van de denitieve guur, want voor lijnen, en dus ook voor pennen, en dus ook voor gummen, is die hetzelfde als de contour) wordt eerst de pendikte 7 uitgekozen, en de tekenkleur wit. Daarna wordt alsnog de oorspronkelijke methode in de superklasse aangeroepen (met behulp van super). Op deze manier doet een gum hetzelfde als een pen, maar dan met een dikke witte lijn in plaats van een dunne gekleurde lijn.

blz. 202

blz. 202

blz. 202

blz. 203

blz. 203 blz. 204 blz. 204

blz. 204

blz. 204

192

Objectgeori enteerd ontwerp

import import import import import

java.awt.*; java.awt.event.*; java.util.*; java.net.URL; javax.swing.*;

10

public class SchetsApplet extends JApplet implements MouseListener, MouseMotionListener, ItemListener, { SchetsCanv canvas; Tool currentTool; boolean isDeelVanApplicatie; public SchetsApplet() { this(false); } SchetsApplet(boolean app) { this.isDeelVanApplicatie = app; if (app) this.init(); } public void init() { canvas = new SchetsCanv(); Collection<ToolAktie> tools = maakToolAkties(); Collection<ControlAktie> controls = maakControlAkties(); Container c = this.getContentPane(); c.setLayout(new BorderLayout()); c.add(canvas , BorderLayout.CENTER); c.add(maakToolBox(tools) , BorderLayout.WEST ); c.add(maakMenuBar(tools, controls), BorderLayout.NORTH ); c.add(maakControlPanel( controls), BorderLayout.SOUTH ); canvas.addMouseListener(this); canvas.addMouseMotionListener(this); canvas.addKeyListener(this); }

KeyListener

15

20

25

30

35

40

public void setCurrentTool(Tool tool) { currentTool = tool; } private ImageIcon getImageIcon(String filename) { try { if (isDeelVanApplicatie) return new ImageIcon(filename); else return new ImageIcon(new URL(this.getCodeBase(), filename)); } catch (Exception e) { return null; } } Listing 34: Schets/src/SchetsApplet.java, deel 1 van 4

45

50

13.4 Toepassing: een schets-programma

193

Figuur 29: Het schets-programma in werking

55

private Collection<ToolAktie> maakToolAkties() { currentTool = new PenTool(); LinkedList<ToolAktie> result; result = new LinkedList<ToolAktie>(); result.add( new ToolAktie(this, "Pen", "Vrije pentekening" , currentTool, getImageIcon("pen.gif"))); result.add( new ToolAktie(this, "Lijn", "Lijntekening", new LijnTool())); result.add( new ToolAktie(this, "Open rect", "Open rechthoek", new RectTool())); result.add( new ToolAktie(this, "Fill rect", "Gevulde rechthoek", new FillRectTool())); result.add( new ToolAktie(this, "Tekst", "Tekst", new TekstTool())); result.add( new ToolAktie(this, "Gum", "Uitgummen van de tekening" , new GumTool(), getImageIcon("gum.gif"))); return result; }

60

65

70

75

private Collection<ControlAktie> maakControlAkties() { LinkedList<ControlAktie> result; result = new LinkedList<ControlAktie>(); result.add( new ControlAktie(canvas, "Clear" , "Tekening wissen" )); result.add( new ControlAktie(canvas, "Rotate", "Tekening draaien")); return result; } Listing 35: Schets/src/SchetsApplet.java, deel 2 van 4

194

Objectgeori enteerd ontwerp

80

private Component maakControlPanel(Collection<ControlAktie> controls) { JPanel controlPanel = new JPanel(); JToolBar toolbar = new JToolBar(JToolBar.HORIZONTAL); toolbar.setFloatable(false); for (Action act : controls) toolbar.add(act); controlPanel.add(toolbar); controlPanel.add(Box.createHorizontalStrut(20)); controlPanel.add(new JLabel("Penkleur")); JComboBox combo = new JComboBox(); combo.addItem( new Kleur("zwart", Color.BLACK) ); combo.addItem( new Kleur("rood", Color.RED ) ); combo.addItem( new Kleur("groen", Color.GREEN) ); combo.addItem( new Kleur("blauw", Color.BLUE ) ); combo.addItemListener(this); controlPanel.add(combo); return controlPanel; }

85

90

95

100

private Component maakMenuBar(Collection<ToolAktie> tools, Collection<ControlAktie> controls) { JMenuBar menubar = new JMenuBar(); JMenu menu;
105

menu = new JMenu("Tool"); for (Action tool : tools) menu.add(tool); menubar.add(menu); menu = new JMenu("Edit"); for (Action ctrl : controls) menu.add(ctrl); menubar.add(menu); menu = new JMenu("Help"); menu.add(new AboutAktie()); menubar.add(menu); return menubar;

110

115

120

} private { Box for { Component maakToolBox(Collection<ToolAktie> tools) toolbox = new Box(BoxLayout.Y_AXIS); (Action act : tools) JButton button = new JButton((Icon) act.getValue(Action.DEFAULT)); button.setToolTipText((String) act.getValue(Action.SHORT_DESCRIPTION)); button.addActionListener(act); toolbox.add(button);

125

130

} toolbox.add(Box.createVerticalGlue()); return toolbox; } Listing 36: Schets/src/SchetsApplet.java, deel 3 van 4

13.4 Toepassing: een schets-programma

195

135

public void mousePressed(MouseEvent e) { this.currentTool.muisIngedrukt (canvas, e.getPoint()); } public void mouseReleased(MouseEvent e) { this.currentTool.muisLosgelaten (canvas, e.getPoint()); } public void mouseDragged(MouseEvent e) { this.currentTool.muisVersleept (canvas, e.getPoint()); }

140

145

public void mouseClicked(MouseEvent e) { canvas.requestFocus(); }


150

public void itemStateChanged(ItemEvent event) { Object item = event.getItem(); if (item instanceof Kleur) canvas.setPenkleur( ((Kleur)item).getKleur() ); } public void keyTyped(KeyEvent e) { this.currentTool.letterIngetikt(canvas, (char) e.getKeyChar()); }

155

160

165

public void keyReleased(KeyEvent e) {} public void keyPressed(KeyEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} public void mouseMoved(MouseEvent e) {} private static final long serialVersionUID = 1; Listing 37: Schets/src/SchetsApplet.java, deel 4 van 4

import java.awt.event.*; public class WindowSluiter extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } } Listing 38: Schets/src/WindowSluiter.java

196

Objectgeori enteerd ontwerp

import java.awt.*; import javax.swing.*; class SchetsCanv extends JPanel { private Schets schets; private Color penkleur = Color.BLACK; public SchetsCanv() { schets = new Schets(); } public void update(Graphics g) { this.paint(g); } public void paint(Graphics g) { schets.teken(g); }
20

10

15

25

public void setBounds(int x, int y, int w, int h) { schets.resize(w, h); super.setBounds(x, y, w, h); this.repaint(); } public Graphics getBitmapGraphics() { Graphics g = schets.getBitmapGraphics(); g.setColor(penkleur); return g; } public void setPenkleur(Color c) { penkleur = c; } public void clear() { schets.clear(); this.repaint(); } private static final long serialVersionUID = 1; } Listing 39: Schets/src/SchetsCanv.java

30

35

40

13.4 Toepassing: een schets-programma

197

import java.awt.*; import javax.swing.JFrame; public class SchetsApplic extends JFrame { SchetsApplic() { this.setSize(800,600); this.setTitle("Schets editor"); this.setBackground(Color.LIGHT_GRAY); this.addWindowListener( new WindowSluiter() ); this.getContentPane().add( new SchetsApplet(true), BorderLayout.CENTER); } public static void main(String[] args) { new SchetsApplic().setVisible(true); } private static final long serialVersionUID = 1; } Listing 40: Schets/src/SchetsApplic.java

10

15

import java.awt.Color; class Kleur { String naam; Color kleur; public Kleur(String naam, Color kleur) { this.naam = naam; this.kleur = kleur; } public Color getKleur() { return kleur; } public String toString() { return naam; } } Listing 41: Schets/src/Kleur.java

10

15

198

Objectgeori enteerd ontwerp

import java.awt.Graphics; import java.awt.Color; import java.awt.image.BufferedImage;


5

class Schets { private BufferedImage bitmap; Schets() { bitmap = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB ); } public void teken(Graphics g) { g.drawImage(bitmap, 0, 0, null); } public void resize(int w, int h) { if (w!=bitmap.getWidth() || h!=bitmap.getHeight()) { BufferedImage nieuw = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB ); Graphics g = nieuw.getGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, w, h); g.drawImage(bitmap, 0, 0, null); bitmap = nieuw; } } public Graphics getBitmapGraphics() { return bitmap.getGraphics(); } public void clear() { Graphics g = bitmap.getGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, bitmap.getWidth(), bitmap.getHeight()); } } Listing 42: Schets/src/Schets.java

10

15

20

25

30

35

13.4 Toepassing: een schets-programma

199

import java.awt.event.*; import javax.swing.*; public class AboutAktie extends AbstractAction { public AboutAktie() { super("About"); } public void actionPerformed(ActionEvent event) { JOptionPane.showMessageDialog( null , "SchetsEditor versie 1.0" , "About" , JOptionPane.INFORMATION_MESSAGE ); } private static final long serialVersionUID = 1; } Listing 43: Schets/src/AboutAktie.java

10

15

import java.awt.event.*; import javax.swing.*; public class ControlAktie extends AbstractAction { private SchetsCanv canvas; public ControlAktie(SchetsCanv canvas, String naam, String tip) { super(naam); this.canvas = canvas; putValue(Action.SHORT_DESCRIPTION, tip); } public void actionPerformed(ActionEvent event) { String naam = (String) this.getValue(Action.NAME); if (naam.equals("Clear")) canvas.clear(); } private static final long serialVersionUID = 1; } Listing 44: Schets/src/ControlAktie.java

10

15

200

Objectgeori enteerd ontwerp

import java.awt.event.*; import javax.swing.*; public class ToolAktie extends AbstractAction { protected Tool tool; protected SchetsApplet applet; public ToolAktie( SchetsApplet applet, String naam , String tip, Tool tool, Icon icon ) { super(naam /* ,icon */ ); // twee parameters geeft icons in de menus this.applet = applet; this.tool = tool; putValue(Action.DEFAULT, icon); putValue(Action.SHORT_DESCRIPTION, tip); } public ToolAktie(SchetsApplet applet, String naam, String tip, TweepuntTool tool) { this(applet, naam, tip, tool, new TweepuntIcon(tool) ); } public ToolAktie(SchetsApplet applet, String naam, String tip, TekstTool tool) { this(applet, naam, tip, tool, new TekstIcon() ); } public void actionPerformed(ActionEvent event) { applet.setCurrentTool(tool); } private static final long serialVersionUID = 1; } Listing 45: Schets/src/ToolAktie.java

10

15

20

25

30

13.4 Toepassing: een schets-programma

201

import java.awt.Dimension; import javax.swing.Icon; public abstract class ToolIcon implements Icon { private Dimension dim = new Dimension(36, 36); public int getIconWidth() { return dim.width; } public int getIconHeight() { return dim.height; } } Listing 46: Schets/src/ToolIcon.java

import java.awt.*; public class TekstIcon extends ToolIcon { public void paintIcon(Component c, Graphics g, int x, int y) { int w = this.getIconWidth(), h = this.getIconHeight(); g.setColor(c.getBackground()); g.fillRect(x, y, w, h); g.setColor(Color.BLACK); g.setFont(new Font("Helvetica", Font.BOLD, h-5 )); g.drawString("Tx", x+2, y+h-2 ); } } Listing 47: Schets/src/TekstIcon.java

10

import java.awt.*; public class TweepuntIcon extends ToolIcon { private TweepuntTool tool;
5

public TweepuntIcon(TweepuntTool t) { this.tool = t; }


10

public void paintIcon(Component c, Graphics g, int x, int y) { int w = this.getIconWidth(), h = this.getIconHeight(); g.setColor(c.getBackground()); g.fillRect(x, y, w, h); g.setColor(Color.BLACK); tool.tekenFiguur (g, new Point(x+2,y+2), new Point(x+w-3, y+h-3) ); } } Listing 48: Schets/src/TweepuntIcon.java

15

202

Objectgeori enteerd ontwerp

import java.awt.Point; public interface Tool { void muisIngedrukt void muisLosgelaten void muisVersleept void letterIngetikt }

(SchetsCanv (SchetsCanv (SchetsCanv (SchetsCanv

canvas, canvas, canvas, canvas,

Point p); Point p); Point p); char c );

Listing 49: Schets/src/Tool.java

import java.awt.*; public abstract class StartpuntTool implements Tool { protected Point startpunt;
5

public void muisIngedrukt(SchetsCanv canvas, Point p) { startpunt = p; } } Listing 50: Schets/src/StartpuntTool.java

import java.awt.*; import java.awt.font.*; class TekstTool extends StartpuntTool { private Font font = new Font("Helvetica", Font.BOLD, 24); public void letterIngetikt(SchetsCanv canvas, char c) { startpunt.x += this.tekenTekst(canvas, startpunt, ""+c); canvas.repaint(); } public void muisVersleept (SchetsCanv canvas, Point p) {} public void muisLosgelaten (SchetsCanv canvas, Point p) {}
15

10

20

public int tekenTekst(SchetsCanv canvas, Point p, String s) { Graphics g = canvas.getBitmapGraphics(); g.setFont(font); this.tekenTekst(g, p, s); FontRenderContext frc = ((Graphics2D)g).getFontRenderContext(); return font.getStringBounds(s, frc).getBounds().width; } public void tekenTekst(Graphics g, Point p, String s) { g.drawString(s, p.x, p.y ); } } Listing 51: Schets/src/TekstTool.java

25

13.4 Toepassing: een schets-programma

203

import java.awt.*; public abstract class TweepuntTool extends StartpuntTool { protected static Point minimumPunt(Point p1, Point p2) { return new Point( Math.min(p1.x, p2.x), Math.min(p1.y, p2.y) ); } protected static Dimension puntAfstand(Point p1, Point p2) { return new Dimension( 1+Math.abs(p1.x - p2.x), 1+Math.abs(p1.y - p2.y) ); } public void muisVersleept (SchetsCanv canvas, Point p) { Graphics g = canvas.getGraphics(); canvas.paint(g); // Teken het plaatje, om de vorige contour kwijt te raken g.setColor(Color.GRAY); this.tekenContour(g, startpunt, p); } public void muisLosgelaten (SchetsCanv canvas, Point p) { this.tekenFiguur(canvas, startpunt, p); canvas.repaint(); } public void letterIngetikt (SchetsCanv canvas, char c) {} public void tekenFiguur(SchetsCanv canvas, Point p1, Point p2) { this.tekenFiguur(canvas.getBitmapGraphics(), p1, p2); }
30

10

15

20

25

public void tekenFiguur(Graphics g, Point p1, Point p2) { this.tekenContour(g, p1, p2); }
35

public abstract void tekenContour(Graphics g, Point p1, Point p2); } Listing 52: Schets/src/TweepuntTool.java

import java.awt.*; class LijnTool extends TweepuntTool { public void tekenContour(Graphics g, Point p1, Point p2) { g.drawLine(p1.x, p1.y, p2.x, p2.y); } } Listing 53: Schets/src/LijnTool.java

204

Objectgeori enteerd ontwerp

import java.awt.*; class RectTool extends TweepuntTool { public void tekenContour(Graphics g, Point p1, Point p2) { Point xy = minimumPunt(p1, p2); Dimension wh = puntAfstand(p1, p2); g.drawRect(xy.x, xy.y, wh.width, wh.height); } } Listing 54: Schets/src/RectTool.java

import java.awt.*; class FillRectTool extends RectTool { public void tekenFiguur(Graphics g, Point p1, Point p2) { Point xy = minimumPunt(p1, p2); Dimension wh = puntAfstand(p1, p2); g.fillRect(xy.x, xy.y, wh.width, wh.height); } } Listing 55: Schets/src/FillRectTool.java

import java.awt.Point; public class PenTool extends LijnTool { public void muisVersleept(SchetsCanv canvas, Point p) { this.muisLosgelaten(canvas, p); this.muisIngedrukt (canvas, p); } } Listing 56: Schets/src/PenTool.java

import java.awt.*; public class GumTool extends PenTool { public void tekenContour(Graphics g, Point p1, Point p2) { ((Graphics2D) g).setStroke( new BasicStroke(7) ); g.setColor(Color.WHITE); super.tekenContour(g, p1, p2); } } Listing 57: Schets/src/GumTool.java

13.5 File input/output

205

13.5

File input/output

Klassen voor le input/output Er is een package met ruim 40 verschillende klassen beschikbaar die iets te maken hebben met input en output (afgekort: I/O) van en naar les, en daarbij zijn de bijbehorende exceptions nog erarchie, niet eens meegerekend. Deze package heet java.io. De klassen vormen een subklassenhi compleet met tussenliggende abstracte klassen. De hi erarchie maakt dat het allemaal nog wel te begrijpen valt: zonder het hi erarchische beeld zou je het overzicht over die 40 klassen snel verliezen. Dat is trouwens in het algemeen de kracht van object-geori enteerde talen. Jammer genoeg hebben de ontwerpers van de package java.io niet alle mogelijkheden voor hi erarchische ordening gebruikt. In de nu volgende bespreking zijn daarom een paar fantasie-klassen toegevoegd, die dus niet ` echt in de package java.io zitten, maar het begrip wel vergemakkelijken. De hele hi erarchie staat afgebeeld in guur 30. Klassen zijn, net als in de eerdere voorbeelden, aangegeven met een gele/lichtgrijze rechthoek, abstracte klassen met een groen/donkerder parallellogram. De fantasie-klassen zijn getekend als omgekeerd parallellogram en met een cursieve naam. De pijlen aan de rechterkant van het schema behoren niet tot de hi erarchie, maar geven aan welk type parameter nodig is voor de constructor-methode van de klasse ernaast. Op het eerste gezicht komt dit overzicht natuurlijk nogal overweldigend over. Dat geeft niet; we gaan de hi erarchie gewoon stap voor stap bespreken, te beginnen bij de klasse bovenin. Uiteraard is dat de klasse Object, want alle klassen in Java zijn een subklasse van Object. Files: vorm of inhoud? Het zal geen verbazing wekken dat er in de package java.io als directe subklasse van Object een klasse File is, waarmee een le wordt gemodelleerd. Je zou je zelfs kunnen afvragen waarom er eigenlijk nog meer klassen nodig zijn. . . Bij de constructie van een File object geef je als parameter de naam van de le mee. Daarmee is het nog niet zeker dat de le ook daadwerkelijk op de disk aanwezig is; een File-object beschrijft dus een mogelijk op de computer aanwezige le. Door het aanroepen van de methode exists kun je nagaan of de le inderdaad aanwezig is:
File f; f = new File("hallo.txt"); if (f.exists()) uitvoer.setText("file bestaat");

Met andere methodes (isFile en isDirectory, die net als exists een boolean resultaat hebben) kun je nagaan of het een le dan wel een directory betreft. Is het een directory, dan kun je een methode listFiles aanroepen, die een array van File-objecten oplevert met alle les die in de directory zitten. Als je een programma schrijft dat onder verschillende operating systems moet kunnen werken, en dat is in Java over het algemeen wel het streven, dan moet je oppassen met de tekens waarmee je les en directories in de lenaam scheidt. In Windows wordt daar het teken \ voor gebruikt, maar in Unix juist / . Als je het universeel wilt houden gebruik je geen van beiden, maar de constante pathSeparator, die in de klasse beschikbaar is. Of je gebruikt de constructor van File met twee parameters: een File-object die de directory aanduidt, en een String met de directe naam van de le. Er zijn nog wel wat meer methodes beschikbaar (zie de online help), maar geen van deze methodes zijn in staat om de inhoud van de le te lezen; het gaat altijd om de le als geheel. Om de inhoud van een le te kunnen lezen, of nieuwe inhoud in een le te kunnen schrijven, zijn er andere klassen nodig. Die klassen zijn, net als File, directe subklassen van Object. Stream-I/O versus Random-access-I/O Er zijn twee manieren om de inhoud van een le te benaderen: als continue stream gegevens, dat wil zeggen dat de gegevens in de le alleen op volgorde worden gelezen of geschreven met random access, waarbij op willekeurige posities stukjes van de le kunnen worden gelezen of veranderd. Voor de laatste is er de klasse RandomAccessFile. Die zullen we hier verder niet gebruiken, maar wie hierin ge nteresseerd is kan in de online help de beschikbare methodes opzoeken. Voor streams van gegevens is er een grote hoeveelheid klassen. Het zou wel logisch geweest zijn als die subklasse

206

Objectgeori enteerd ontwerp

Object

java.io Stream Byte Stream


Input Stream

Direct InputStream

File InputStream ByteArray InputStream

File / String

Random AccessFile Filter InputStream

byte [ ]

Buffered InputStream Data InputStream

InputStream

InputStream

File Output Stream

Direct OutputStream

File OutputStream ByteArray OutputStream

File / String

Filter OutputStream

Buffered OutputStream Data OutputStream PrintStream

OutputStream

OutputStream

Character Stream

Reader

Direct Reader

InputStream Reader

InputStream File Reader File / String

String Reader

String

Indirect Reader

Buffered Reader

Reader LineNumber Reader Reader Reader

Filter Reader Writer

PushBack Reader

Direct Writer

OutputStream Writer

OutputStream File Writer File / String

String Writer

Indirect Writer

Buffered Writer Filter Writer Print Writer

Writer

abstract class

fantasie klasse

class

parameter v.d. constructor

Writer / OutputStream

Figuur 30: De klassen voor le-input en -output

13.5 File input/output

207

waren van een abstracte klasse Stream. Dat is niet het geval; er zijn een viertal klassen die direct subklasse zijn van Object. Om toch een beetje structuur aan het overzicht te geven, is in guur 30 toch een abstracte klasse Stream ingetekend. Om aan te geven dat dit eigenlijk een fantasieklasse is, is het parallellogram andersom getekend en de naam cursief gemaakt. Byte-streams versus Character-streams Er zijn twee soorten streams: byte-streams, waarbij iedere byte in de le ook als e en byte in het programma beschikbaar komt character-streams, waarbij de inhoud van een le als characters wordt ge nterpreteerd, en waarbij mogelijk meerdere bytes tot e en character worden gecombineerd. Voor deze twee categorie en zijn er geen abstracte klassen, maar we doen toch maar even alsof:
Stream
Random AccessFile

Byte Stream

Input Stream Output Stream

Character Stream

Reader

Writer

Beide soorten streams kun je lezen (waarbij je de inhoud van een al bestaande le kunt bekijken) of schrijven (waarbij een nieuwe le ontstaat). In het geval van byte-streams gebruik je respectievelijk een InputStream of een OutputStream, in het geval van character-streams heten de klassen respectievelijk Reader en Writer. Afwisselend lezen en schrijven van dezelfde le kan niet met streams; daarvoor zou je random-access nodig hebben. Tot voor kort (in talen als C en C++) werden bytes en characters zo ongeveer als synoniem beschouwd. Omdat Java de Unicode-characterset met veel meer dan 256 mogelijke symbolen gebruikt, is dat echter niet meer het geval. Bij het gebruik van character-streams worden de benodigde conversies automatisch uitgevoerd. De te gebruiken conversie kan daarbij zonodig worden opgegeven. Zo kun je bijvoorbeeld tekstles waarin de oude ibm/dos-codering wordt gebruikt toch gemakkelijk inlezen. Een andere conversie die automatisch wordt uitgevoerd, is de codering van het einde-regel-teken. Die is bij verschillende operating systems verschillend: Windows gebruikt twee bytes (13 en 10) aan het eind van elke regel; Apple gebruikt alleen de 13, en Unix gebruikt alleen de 10. Door character-stream-gebaseerde klassen te gebruiken voor het lezen van tekstles, kun je tekstbestanden toch moeiteloos uitwisselen. Byte-streams zijn daarentegen geschikt voor binair gecodeerde bestanden, waarbij je echt elke byte wilt kunnen gebruiken in het programma. Lezen en schrijven Heb je een InputStream beschikbaar, dan kun je de methode read aanroepen om e en byte te lezen. Je zou verwachten dat die methode als resultaat een byte oplevert. Dat is echter niet het geval; de methode levert een int waarde op. De reden daarvoor is, dat het nu mogelijk is om te signaleren of het einde van de stream bereikt is: dan levert read namelijk de speciale waarde 1 op. Je kunt nu dus dingen schrijven als
int n; n = inpstr.read(); if (n==-1) uitvoer.setText("er valt niets te lezen"); else uitvoer.setText("gelezen byte is " + n );

Hierin is inpstr de InputStream waaruit je wilt lezen. Hoe je die variabele kunt initialiseren bespreken we zodadelijk. Een tweede manier om te lezen is het aanroepen van read, waarbij je een array van bytes meegeeft waarin je het resultaat wilt hebben. Je kunt zo in e en klap een heleboel bytes lezen. Het resultaat van de methode is het aantal gelezen bytes (de lengte van de array, of minder als er niet zoveel bytes meer beschikbaar waren).

208

Objectgeori enteerd ontwerp

Schrijven naar een OutputStream gaat door aanroep van methode write, waarbij je naar keuze e en byte of een hele array van bytes meegeeft. Ook de klassen Reader en Writer kennen respectievelijk een methode read en write, maar hier zijn de parameters niet (arrays van) byte maar van char. Bij een Writer is er ook een versie van write beschikbaar met een String parameter; die string wordt dan netjes in characters opgedeeld en naar de Writer gestuurd. Constructie van streams: subklassen van InputStream Heb je eenmaal een stream gemaakt (hetzij een byte-stream in de vorm van een InputStream of een OutputStream, of een character-stream in de vorm van een Reader of Writer), dan kun je dus dingen lezen of schrijven. Maar hoe krijg je zon stream in handen? Dat gebeurt niet door met new de constructor van een van deze vier klassen aan te roepen. In plaats daarvan moeten we kiezen voor e en van de (vele) subklassen die er beschikbaar zijn. We bekijken daarom eerst de subklassen van InputStream wat nader (voor de andere drie is de onderverdeling net zoiets).
Input Stream

Direct InputStream

File InputStream ByteArray InputStream

File / String

byte [ ]

Filter InputStream

Buffered InputStream Data InputStream

InputStream

InputStream

Er zijn twee mogelijkheden om een InputStream te maken: een directe, en een indirecte, zogeheten gelterde inputstream, een FilterInputStream. Om met de directe constructie te beginnen: ook hier zijn weer twee mogelijkheden: een FileInputStream of een ByteArrayInputStream. Een FileInputStream kun je gebruiken om uit een le te lezen. De gewenste File moet je meegeven als parameter van de constructor. Daarna kun je het object gebruiken om methode read aan te roepen; het is tenslotte een subklasse van InputStream. Bijvoorbeeld:
int n; File f; FileInputStream fis; f = new File("hallo.txt"); fis = new FileInputStream(f); n = fis.read();

Voor het gemak is er ook een alternatieve constructor van FileInputStream beschikbaar, waarbij je niet een le-object maar een string met de naam van de le meegeeft. Daarmee spaar je uit dat je eerst een le-object moet cre eren. Een hele andere manier om een InputStream te maken is het gebruik van de subklasse ByteArrayInputStream. Van zon object kun je (natuurlijk!) ook de methode read aanroepen. De gegevens komen dan echter niet uit een echte le, maar worden in plaats daarvan geput uit de array van bytes die bij de constructie werd meegegeven. Je kunt hiermee als het ware een le in het geheugen simuleren, zonder dat de rest van het programma daar iets van hoeft te merken (daar roep je immers gewoon read aan, zonder je druk te maken waar de gegevens precies vandaan komen). Dan zijn er nog de indirecte manieren om een InputStream te maken. Ze zijn indirect omdat je eerst een andere InputStream moet maken (bijvoorbeeld een FileInputStream of een ByteArrayInputStream). Die geef je dan mee aan de constructor van de indirecte stream, bijvoorbeeld DataInputStream. Dat is dus een heel gedoe:
File f; FileInputStream fis; DataInputStream dis; f = new File("hallo.dat"); fis = new FileInputStream(f); dis = new DataInputStream(fis);

maar dan heb je ook wat: zon DataInputStream kent namelijk een aantal extra methodes,

13.5 File input/output waarmee je bijvoorbeeld double waarden (binair gecodeerd!) uit een le kunt lezen:
double d; d = dis.readDouble();

209

Een BufferedInputStream is een andere indirecte InputStream. Ook hier moet je eerst een andere InputStream maken, en die meegeven bij de constructie. Heb je eenmaal zon enter werkt dan bijvoorbeeld een BufferedInputStream, dan zul je merken dat die eci gewone FileInputStream, zeker bij langzame netwerkconnecties. Een gebuerde stream pakt namelijk een grote hoeveelheid data tegelijk van het netwerk, bewaart die in een interne buer, en verstrekt die vervolgens mondjesmaat bij elke aanroep van read. In de rest van het programma heb je er geen omkijken meer naar: hij werkt net zoals een ongebuerde stream in de zin dat je de methode read kunt aanroepen. Constructie van streams: subklassen van OutputStream De subklasse-hi erarchie onder OutputStream heeft dezelfde vorm als die onder InputStream, en bevat dus weinig verrassingen meer; zie het grote schema in guur 30. Ook hier zijn er weer twee directe manieren om een stream te maken, en twee indirecte manieren. De subklasse PrintStream van FilterOutputStream heeft geen constructor-methode, en daar heb je dus weinig aan. Het is een historische vergissing, die we in sectie 13.6 zullen bespreken. Opvallend is dat je bij constructie van een ByteArrayOutputStream geen byte[] hoeft mee te geven bij constructie. De benodigde array wordt automatisch gemaakt, en groeit wanneer dat nodig is. Als je de array op een gegeven moment wilt bekijken, dan kun je hem opvragen door aanroep van de methode toByteArray. Constructie van streams: subklassen van Reader Character-streams zijn onderverdeeld in Reader-klassen (voor input) en Writer-klassen (voor output). Net als bij bytestreams kun je objecten van deze klassen niet direct cre eren, maar moet je een keuze maken uit e en van de subklassen. De hi erarchische ordening daarvan doet sterk denken aan die van byte-streams. Er zijn twee directe manieren om een character-stream te maken en twee indirecte (waarbij je eerst een andere Reader moet maken). Ook nu kun je weer kiezen om te lezen uit een echte le, of uit een string in het geheugen. Om uit een le te kunnen lezen, moet je eerste een InputStream object maken (dat is dus een bytestream!). Helemaal direct gaat de constructie dus ook weer niet:
File f; FileInputStream fis; InputStreamReader isr; f = new File("hallo.txt"); fis = new FileInputStream(f); isr = new InputStreamReader(fis);

Uit de aldus geconstrueerde InputStreamReader kun je characters lezen, maar in de praktijk is het handiger om hele strings tegelijk te lezen. Om dat te kunnen doen, wordt het nog een stapje indirecter:
BufferedReader br; br = new BufferedReader(isr);

Zon BufferedReader kent namelijk een methode om een hele regel tekst regelijk te lezen:
String s; s = br.readLine();

Je krijgt dan precies e en tekstregel uit de le, waar de einde-regel-markering al van afgehaald is. Als er geen regels meer beschikbaar zijn, levert readLine de speciale objectverwijzing null als resultaat. Constructie van streams: subklassen van Writer De subklasse-hi erarchie onder Writer is weer een variatie op hetzelfde thema. Een handige klasse is vooral PrintWriter. Objecten van dat type kennen een methode println, waarmee je strings kunt schrijven, maar ook waarden van alle standaardtypen, die daarbij automatisch naar String worden geconverteerd.

210

Objectgeori enteerd ontwerp

Het is een beetje omslachtig om zon PrintWriter-object te maken, maar de stappen zijn direct uit het schema af te lezen: bij de constructie heb je een andere Writer nodig, wat bijvoorbeeld een OutputStreamWriter kan zijn. Bij de constructie daarvan het je een OutputStream nodig, wat bijvoorbeeld een FileOutputStream kan zijn. Bij de constructie daarvan heb je een File nodig, en bij de constructie daarvan een String. Je kunt dat overigens in e en grote expressie opschrijven:
PrintWriter pw; pw = new PrintWriter(new OutputStreamWriter(new FileOutputStream(new File("hallo.txt")))); pw.println("deze tekst komt in de file");

Convenience klassen Een veelvoorkomende klus als het lezen of schrijven van een tekstle is al met al toch wel omslachtig. Daarom zijn er in package java.io een paar extras toegevoegd die de logica van het bouwwerk enigszins ondermijnen, maar in de praktijk wel handig zijn. Sommige klassen hebben een alternatieve constructormethode, en er zijn zelfs bovendien een paar extra subklassen (zogenaamde convenience klassen). In het grote schema in guur 30 vind je deze terug: De constructor van een FileInputStream heeft een File-parameter, die op zijn beurt bij constructie een String-parameter (de lenaam) heeft. Je kunt echter ook de String direct meegeven aan de constructor van FileInputStream. Hetzelfde geldt voor FileOutputStream. De constructor van InputStreamReader heeft een InputStream als parameter. Vaak wordt daar een FileInputStream voor gebruikt, die een File-parameter heeft, die een Stringparameter heeft. Er is echter een subklasse FileReader van InputStreamReader die direct de File, of zelfs een String (de lenaam) als parameter heeft. Hetzelfde geldt voor de subklasse FileWriter van OutputStreamWriter. De constructor van PrintWriter heeft een Writer als parameter. Vaak wordt daar een OutputStreamWriter voor gebruikt, die op zijn beurt een OutputStream nodig heeft. Je kunt die OutputStream echter ook direct aan PrintWriter meegeven. Lezen uit een tekstle kan daarom als volgt gedaan worden:
String s; BufferedReader br; br = new BufferedReader(new FileReader("in.txt")); s = br.readLine();

Schrijven naar een tekstle gaat als volgt:


PrintWriter pw; pw = new PrintWriter(new FileOutputStream("uit.txt")); pw.println("tekst");

blz. 211

Voorbeeld: tekst-editor In listing 58 staat het volledige programma voor de tekst-editor. De meeste idee en zijn hierboven al besproken. Nog een paar details: er zijn twee interactie-componenten: een TextArea voor de eigenlijke tekst, en een Label waarop eventuele foutmeldingen kunnen worden getoond. de individuele menu-items worden niet in membervariabelen bewaard. Daardoor kunnen we in actionPerformed ook niet de identiteit van e.getSource() testen tegen de verschillende items. In plaats daarvan achterhalen we met de methode getLabel het opschrift van het item dat de actie veroorzaakte; dat kunnen we vervolgens als string vergelijken met de mogelijkheden. als reactie op menukeuze Open wordt de methode open aangeroepen. Daar wordt de gebruiker gevraagd om een lenaam. Het eigenlijke leeswerk van de le gebeurt in een aparte methode lees. Zo is de communicatie met de gebruiker en die met het lesysteem mooi gescheiden. deze scheiding komt meteen goed van pas. We kunnen lees nu namelijk ook aanroepen vanuit main. In de methode main is er als parameter een array van strings beschikbaar, die de teksten bevat die de gebruiker op de commandoregel heeft ingetikt. Zo kunnen we meteen aan het begin al een le openen.

13.5 File input/output

211

import java.awt.*; import java.awt.event.*; import java.io.*;


5

class Editor extends Frame implements ActionListener, WindowListener { TextArea tekst; Label status; FileDialog openDial, saveDial; private Editor() { tekst = new status = new openDial = new saveDial = new

10

15

TextArea(20,60); Label(); FileDialog(this, "Open File...", FileDialog.LOAD); FileDialog(this, "Save File...", FileDialog.SAVE);

20

this.setSize(400,350); this.setTitle("Simpele teksteditor"); this.addWindowListener(this); this.add(BorderLayout.CENTER, tekst); this.add(BorderLayout.SOUTH, status); this.maakMenu(); }

25

private void { MenuBar Menu MenuItem

maakMenu() bar; menu; item;

30

35

bar = new MenuBar(); menu = new Menu("File"); item = new MenuItem("Open"); menu.add(item); item.addActionListener(this); item = new MenuItem("Save"); menu.add(item); item.addActionListener(this); menu.addSeparator(); item = new MenuItem("Quit"); menu.add(item); item.addActionListener(this); bar.add(menu); menu = new Menu("Edit"); item = new MenuItem("Upper"); menu.add(item); item.addActionListener(this); item = new MenuItem("Lower"); menu.add(item); item.addActionListener(this); bar.add(menu); this.setMenuBar(bar); } Listing 58: Editor/src/Editor.java, deel 1 van 3

40

45

212

Objectgeori enteerd ontwerp

public void actionPerformed(ActionEvent e) { MenuItem item; String keuze;


55

item = (MenuItem) (e.getSource() ); keuze = item.getLabel(); if (keuze.equals("Open")) this.open(); else if (keuze.equals("Save")) this.save(); else if (keuze.equals("Quit")) System.exit(0); else if (keuze.equals("Upper")) this.vertaal(true); else if (keuze.equals("Lower")) this.vertaal(false); }
70

60

65

private void open() { String naam; openDial.setVisible(true); naam = openDial.getFile(); if (naam!=null) this.lees(openDial.getDirectory() + naam); }
80

75

private void save() { String naam; saveDial.setVisible(true); naam = saveDial.getFile(); if (naam!=null) this.schrijf(saveDial.getDirectory() + naam); } private void vertaal(boolean upper) { String inhoud; inhoud = tekst.getText(); if (upper) inhoud = inhoud.toUpperCase(); else inhoud = inhoud.toLowerCase(); tekst.setText(inhoud); } Listing 59: Editor/src/Editor.java, deel 2 van 3

85

90

95

13.5 File input/output

213

100

105

110

115

private void lees(String naam) { String regel; BufferedReader invoer; try { invoer = new BufferedReader( new FileReader(naam) ); tekst.setText(""); regel = invoer.readLine(); while (regel != null) { tekst.append(regel + "\n" ); regel = invoer.readLine(); } invoer.close(); } catch (FileNotFoundException fnf) { status.setText("File " + naam + " bestaat niet"); } catch (IOException io) { status.setText("Leesfout in " + naam ); } status.setText(naam + " ingelezen."); } private void schrijf(String naam) { PrintWriter uitvoer; try { uitvoer = new PrintWriter( new FileWriter(naam), true); uitvoer.print( tekst.getText() ); uitvoer.close(); } catch (IOException io) { status.setText("Schrijffout in " + naam ); } } public public public public public public public void void void void void void void windowClosing (WindowEvent windowClosed (WindowEvent windowOpened (WindowEvent windowIconified (WindowEvent windowDeiconified(WindowEvent windowActivated (WindowEvent windowDeactivated(WindowEvent e) e) e) e) e) e) e) {System.exit(0);} {} {} {} {} {} {}

120

125

130

135

140

public static void main(String [] params) { Editor e; e = new Editor(); e.setVisible(true); if (params.length>0) e.lees(params[0]); } private static final long serialVersionUID = 1; Listing 60: Editor/src/Editor.java, deel 3 van 3

145

214

Objectgeori enteerd ontwerp

13.6

Non-window-programmas

Console input/output Niet elk programma hoeft een window te maken om zijn resultaten in te tonen. Hoewel op PCs het gebruik van dos-programmas nauwelijks meer gangbaar is, is het vooral op Unix-computers nog vrij gebruikelijk dat programmas tekstuitvoer genereren in hetzelfde window waarin ze (door het intikken van een commando) werden gestart. Dit soort programmas kunnen de muis niet gebruiken, en krijgen al hun input van de gebruiker dus vanaf het toetsenbord. De klasse System De klasse System mag in elk Java-programma gebruikt worden, zelfs zonder hem te importeren. In deze klasse zitten twee interessante variabelen, die nodig zijn bij console-programmas: System.in en System.out. Dit zijn variabelen met als type respectievelijk InputStream en PrintStream. De variabele System.in stelt de stroom van bytes voor die afkomstig zijn van het toetsenbord. Alle strings die je naar de stream System.out stuurt verschijnt als tekst op de console. De console-versie van een Hallo-programma is wel heel gemakkelijk te schrijven. Er zijn geen windows nodig, je hoeft dus geen objecten te cre eren, en het is dus ook niet nodig om een subklasse van Frame te maken met een hergedenieerde paint. In plaats daarvan kun je direct vanuit de methode main de tekst sturen naar het standaard aanwezige object System.out:
import java.io.*; class Hallo { public static void main(String [] ps) { System.out.println("Hallo!"); } }

Het object System.out mag in elk Java-programma gebruikt worden, ook in applets. In windowprogrammas kan het zijn diensten bewijzen bij het debuggen van het programma. In het geval van applets kun je de tekst zichtbaar maken door in de browser voor het menuitem Java console te kiezen. Voorbeeld: grep-programma Een voorbeeld van een typisch console-programma is het programma grep. Onder Unix is dit een standaard aanwezig programma, waarvan de naam betekent: get regular expression pattern. We gaan dit programma, althans een eenvoudige versie ervan, zelf even maken. Bij het starten van het programma moet op de commandoregel een tekst worden gespeciceerd (het zogenaamde patroon), en een of meer lenamen. Het programma zal nu alle regels uit de genoemde les tonen waarin het tekstpatroon voorkomt. Het patroon en de lenamen zijn in de methode main beschikbaar in de vorm van de string-arrayparameter: het patroon staat op de nulde positie in deze array, en de lenamen op de posities vanaf 1. Omdat eenzelfde soort actie op alle les moet worden uitgevoerd, is het het gemakkelijkst om een aparte methode te maken die het werk voor e en le uitvoert. Die methode kan dan vanuit main voor alle les worden aangeroepen. In listing 61 heet deze methode bewerk. De methode kan (en moet zelfs) statisch zijn, omdat er geen window-object beschikbaar is. Het werk dat resteert in de methode main is controleren of de gebruiker wel voldoende strings heeft ingetikt. Zo niet, dan krijgt de gebruiker een korte uitleg van wat hij geacht wordt in te tikken. Toekenningen hebben een resultaat Toekenningen aan variabelen hebben we tot nu toe altijd als opdracht gebruikt. Maar toekenningen zijn ook bruikbaar als expressie. Terwijl deze expressie wordt uitgerekend krijgt de variabele zijn nieuwe waarde. Deze waarde is bovendien de resultaatwaarde van de expressie, en kan dus in een grotere expressie worden gebruikt. Een simpel voorbeeld van zon toekenning-expressie is:
int x, y; x = (y=0);

blz. 215

De toekenning y=0 is hier als expressie gebruikt, die zelf aan de rechterkant van de toekenningsopdracht aan x staat. Met de toekenning y=0 krijgt y de waarde 0. Dit is bovendien de waarde van het geheel (y=0), en dus ook x krijgt de waarde 0.

13.6 Non-window-programmas

215

import java.io.*; class Grep { private static void bewerk(String patroon, String filenaam ) { BufferedReader br; InputStreamReader reader; String regel; try { if (filenaam.equals("")) reader = new InputStreamReader(System.in); else reader = new FileReader(filenaam);
15

10

br = new BufferedReader(reader); for (int nr=1; (regel=br.readLine()) != null; nr++) if (regel.indexOf(patroon)!=-1) System.out.println( filenaam + " regel " + nr + ": " + regel );
20

} catch (Exception e) { System.out.println( filenaam + ": " + e ); } }

25

30

35

public static void main(String [] parameters) { switch (parameters.length) { case 0: System.out.println("Usage: java Grep pattern [files]"); break; case 1: Grep.bewerk( parameters[0], "" ); break; default: for (int i=1; i<parameters.length; i++) Grep.bewerk( parameters[0], parameters[i] ); } } } Listing 61: Grep/src/Grep.java

216

Objectgeori enteerd ontwerp

Voor dit geval is het niet zon nuttige notatie, want we hadden net zo goed twee aparte toekenningen kunnen doen:
x=0; y=0;

Toekennings-expressies worden dan ook zelden gebruikt. Er is echter e en situatie waarin ze goed van pas komen, en dat is bij het lezen van les. In het editor-programma schreven we de volgende code om alle regels van een le te lezen:
String regel; regel = invoer.readLine(); while (regel!=null) { tekst.append(regel); regel = invoer.readLine(); }

De aanroep van de methode readLine staat dus op twee plaatsen in het programma: eenmaal om de eerste regel te lezen; dat moet immers gebeuren alvorens de test regel!=null kan worden uitgevoerd. En eenmaal aan het eind van de body van de while-opdracht, om de volgende regel te lezen. Met gebruikmaking van een toekennings-expressie kan dit handiger: we kunnen in dezelfde expressie die test of de regel ongelijk null is, in het voorbijgaan de regel inlezen. De opdracht wordt dan:
while ( (regel=invoer.readLine()) != null) tekst.append(regel);

Omdat er nu nog maar e en opdracht in de body staat, kunnen zelfs de accolades vervallen. In het voorbeeldprogramma wordt bovendien met een teller bijgehouden wat het regelnummer is. In plaats van een while-opdracht kunnen we dus beter een for-opdracht gebruiken, in de header waarvan alles gecombineerd wordt:
for (nr=1; (regel=invoer.readLine())!=null; nr++) tekst.append(nr + ":" + regel);

Lezen van System.in Het gebruik van System.in is iets gecompliceerder dan dat van System.out. Je kunt in System.in weliswaar gebruiken om losse bytes van het toetsenbord te lezen, maar in de praktijk komt het vaker voor dat je complete strings wilt lezen. Omdat strings uit characters bestaan, en niet uit bytes, moeten we de InputStream die System.in is transformeren tot een Reader. Dat gebeurt door System.in als parameter mee te geven bij constructie van een InputStreamReader object. Dat object gebruiken we vervolgens bij de constructie van een BufferedReader, zodat we niet alleen losse characters, maar ook hele strings kunnen lezen. Samengevat:
InputStreamReader isr; BufferedReader br; String s; isr = new InputStreamReader(System.in); br = new BufferedReader(isr); s = br.readLine();

Het is in console-programmas zoals grep gebruikelijk om, als er geen lenamen worden gespeciceerd, in plaats daarvan vanaf System.in te lezen. Daarom staat er in de methode bewerk een if-opdracht die beslist hoe de BufferedReader gemaakt wordt: met de InputStreamReader zoals hierboven beschreven, of met de FileReader die met een le is geassocieerd. Een historische vergissing: PrintStream Het object System.out is een byte-stream, of preciezer een OutputStream, of nog preciezer een PrintStream. Die klasse kent een methode println, waarmee een string kan worden vertoond. Dat is eigenlijk raar, want een string bestaat uit characters, en het converteren daarvan naar bytes is een taak van een character-stream, of preciezer een Writer, of nog preciezer een PrintWriter. In oudere versies van Java (versie 1.0) werd het onderscheid tussen byte- en character-streams echter nog niet gemaakt. Vanaf Java versie 1.2 is de constructor van PrintStream deprecated, dat wil zeggen dat wordt aangeraden hem niet meer te gebruiken omdat er betere alternatieven voorhanden zijn (namelijk PrintWriter). Inmiddels waren er echter al te veel programmas die

13.6 Non-window-programmas

217

veel gebruik maken van System.out, zodat het veranderen van het type daarvan in PrintWriter teveel problemen zou geven. We zitten dus nu al opgescheept met een erfenis van het (recente) verleden, die tot in lengte van dagen in de Java-library zichtbaar zal zijn. Als je System.out gebruikt om tijdens het debuggen van je programma even wat tekst op het scherm te schrijven, is het niet-ondersteunen van de Unicode-codering niet zon probleem. Maar als je programmas voor internationaal gebruik wil schrijven, dan zou je de PrintStream eigenlijk moeten verpakken in een PrintWriter, als volgt:
OutputStreamWriter osr; PrintWriter pw; osr = new OutputStreamWriter(System.out); pw = new PrintWriter(osr); pw.println(s);

Dat is mooi symmetrisch als je het vergelijkt met het verpakken van System.in in een BufferedReader, zoals in de vorige paragraaf.

Opgaven
13.1 Stream-hi erarchie De standaard packages zijn duidelijk door meerdere personen geschreven. Vergelijk: a. de onderlinge positie van gelterde en gebuerde streams in het geval van byte- en characterstreams; b. de namen van de methodes waarmee een string kan worden gelezen en geschreven, en de klassen waarin deze methoden zich bevinden. 13.2 Implementatie stream-klassen We verplaatsen ons even in de rol van de schrijvers van het package java.io. Schrijf de volgende methoden en/of klassen, zonder gebruik te maken van de al bestaande exemplaren: a. de methode read in de klasse InputStream die een array van bytes inleest, daarbij gebruikmakend van de al bestaande methode read die e en byte inleest. b. de klasse FileReader, met daarin een constructormethode en een herdenitie van de methode read die e en byte inleest c. de klasse BufferedInputStream, met daarin een constructormethode en een herdenitie van de methode read die e en byte inleest 13.3 Database application Maak een database-programma (application). Het programma heeft twee tekstvelden, die een naam en een telefoonnummer kunnen bevatten. De gegevens worden opgeslagen in een array (je mag aannemen dat er nooit meer dan 1000 ingangen zijn). Als de gebruiker tekst wijzigt en op Enter drukt, moeten de gegevens in de array veranderd worden. Verder is er een knop Nieuw; als de gebruiker die indrukt, krijgt zhij een nieuwe, blanco, ingang te zien, die vervolgens veranderd kan worden. Tenslotte is er een scrollbar waarmee de gebruiker door de ingangen kan bladeren. Die moet zo zijn gemaakt, dat er precies mee door de aanwezige ingangen kan worden gebladerd. Dat betekent dat het bereik van de scrollbar moet worden aangepast als er een nieuwe ingang bij komt! (dat kan met een aanroep van setMaximum(n) ); Bij het starten van het programma moet het een le inlezen, waarvan de naam data.txt luidt, of nog mooier: waarvan de naam op de commandoregel wordt gespeciceerd. Die le bevat op elke regel een naam en een telefoonnummer, gescheiden door een #-teken. Die gegevens komen aan het begin in de array te staan. Op het moment dat de gebruiker de applicatie afsluit, moet op de valreep nog snel de (mogelijk gewijzigde) array worden weggeschreven naar de le.

218

Objectgeori enteerd ontwerp

13.4 Maak Index Schrijf een statische methode maakIndex, met de volgende specicaties. De methode heeft als parameter een array met namen van les. De methode moet deze les lezen, en een nieuwe le schrijven met een samenvatting van die les. Deze le krijgt de naam index.txt. De samenvatting geeft van elke le: een vermelding van de naam van de le, zoals in:
filenaam: aap.txt

vermelding van het aantal regels, karakters en woorden, zoals in:


12 regels 417 karakters 74 woorden

Bij de karakters worden regel-overgangen niet meegerekend. Woorden zijn gescheiden door e en of meer van de symbolen spatie, punt, komma of puntkomma. Aan het eind van de regel is een woord ook altijd afgelopen. vermelding van het langste woord in de le, zoals in:
langste woord: primatendagverblijf

Aan het eind van de index wordt tenslotte de naam van de moeilijkste tekst vermeld, zoals in:
moeilijkste tekst: noot.txt

Een tekst is moeilijker dan een andere tekst, als de woorden gemiddeld langer zijn. Als er problemen zijn bij het lezen van een le, dan moet op de console-output worden gemeld dat er problemen zijn met die le, zoals in:
probleem bij lezen van file mies.txt

Die le mag dan verder worden genegeerd, en hoeft niet in de index te komen.

219

Hoofdstuk 14

Algoritmen
In het voorbeeldprogramma Bitmap-editor in sectie 12.5 lag de nadruk sterk op de opbouw van de gebruikersinterface en de opzet van een programma met een frame, een canvas en een model. Het eigenlijke werk waar het allemaal om begonnen is (het opschuiven, bold maken enz. van een bitmap) is uiteindelijk erg eenvoudig. In het Schets-programma in sectie 13.4 gebeurt inhoudelijk zo mogelijk nog minder: uiteindelijk tekent het programma alleen maar lijntjes en rechthoeken. . . Wat het programma ingewikkeld maakt is de zeer strikte opzet volgens het once and only once principe. Het komt natuurlijk ook wel voor dat het werk dat het programma moet doen minder triviaal is. In dit hoofdstuk geven we daarvan twee voorbeelden: een programma dat een route zoekt door een netwerk, en een programma dat de taal waarin een tekst is geschreven probeert te herkennen aan de hand van een aantal voorbeeld-teksten. Het gaat in deze programmas dus vooral om de algoritmen of berekeningsmethoden; we laten de GUI opzettelijk eenvoudig. Wel maken we natuurlijk dankbaar gebruik van alle in eerdere hoofdstukken besproken Java-klassen, met name voor collections en les.

14.1

Toepassing: een zoekend programma

Beschrijving van de casus Het programma in deze sectie heet Net. Het maakt het mogelijk dat de gebruiker op een landkaart met wegen (of spoorlijnen, of telecommunicatieverbindingen) de snelste (of goedkoopste of anderszins beste) route tussen twee plaatsen kan vinden. De plaatsen van het netwerk en de verbindingen daartussen leest het programma uit een le. Daarna wordt aan de gebruiker een grasche presentatie hiervan aangeboden. De gebruiker mag vervolgens twee plaatsen aanklikken, waarop de beste route tussen die plaatsen in een andere kleur wordt weergegeven. Daarna mag de gebruiker opnieuw twee plaatsen aanklikken. In guur 31 is het programma in werking te zien. Als netwerk zijn de belangrijkste spoorwegverbindingen in Nederland ingelezen. Zojuist is de beste route van Den Haag naar Almelo gevonden, en heeft de gebruiker Nijmegen alweer aangeklikt voor een nieuwe zoekopdracht. De modellering van het probleem is tamelijk ingewikkeld. Zowel het opdelen van het programma in klassen, als het ontwerp van sommige methoden vergen zorgvuldige planning. Opdeling in klassen Omdat dit een interactief programma is, hebben we in ieder geval een subklasse van Frame nodig. We gebruiken deze klasse puur als een wrapper om een NetApplet te tonen, waarin het eigenlijke werk gebeurt. Die applet kan desgewenst ook stand-alone gebruikt worden. De klasse NetApplet, die bovendien als MouseListener fungeert, verzorgt de interactie met de gebruiker. Voor het eigenlijke model van het netwerk maken we een aparte klasse Netwerk. Op dezelfde manier als de klasse Calc een membervariabele van het type Proc had, waarin de toestand van de rekenmachine was vastgelegd, zal NetApplet een membervariabele van het type Netwerk krijgen. Alle dingen die te maken hebben met frames, muis-activiteit en dergelijke horen thuis in NetApplet; de opbouw van het netwerk uit steden en wegen, alsmede het zoekproces worden gemodelleerd in de klasse Netwerk. Belangrijke onderdelen van een netwerk zijn de steden daarin en de verbindingswegen tussen de steden. Daarom maken we aparte klassen Stad en Weg die objecten van dat type modelleren. Bij het zoeken is verder sprake van een route door het netwerk. Om objecten die zon route door het

220

Algoritmen

Figuur 31: Het programma Route in werking

14.1 Toepassing: een zoekend programma netwerk voorstellen te modelleren, maken we een aparte klasse Pad.

221

Klasse NetApplet: De grasche interface De muiskliks van de gebruiker kunnen worden opgevangen door implementatie van de methode mousePressed, en de grasche output kan getekend worden in de hergedenieerde methode paint. Maar welke membervariabelen zijn er nodig? Zoals hierboven al werd opgemerkt is er een Netwerk-object nodig, waarin het netwerk ligt opgeslagen. Verder zijn er objecten nodig waarmee de inhoud van het window op verzoek getekend kan worden. Daarvoor is nodig: Een tekening van het netwerk. Met deze klus kunnen we de Netwerk membervariabele opzadelen, door daarin een methode te maken die zichzelf tekent op een als parameter meegegeven Graphics-object. Een tekening van de route in een andere kleur. Er is dus een Pad-object nodig, die ook in staat moet zijn om zichzelf te tekenen. Opmerkelijk aan de methode mousePressed is dat de eerste en de tweede klik verschillend behandeld moeten worden. De eerste klik moet alleen maar bewaard worden: oh, dit is blijkbaar de stad waar de route moet beginnen. Pas na de tweede klik kan het zoekproces beginnen. Er is dus een membervariabele nodig waarin de begin-stad bewaard kan worden. Aan het wel of niet ge nitialiseerd zijn van deze variabele kan mousePressed ook zien of het de tweede of de eerste klik betreft. Een eerste opzet van de klasse NetApplet is al met al als volgt:
class NetApplet extends Applet { Netwerk netwerk; // Pad pad; // Stad stad1; // init() {...} // paint(...) {...} // mousePressed(...) {...} // } modellering van het netwerk de gevonden route door het netwerk eerste aangeklikte stad initialisatie laat netwerk en pad zichzelf tekenen eerste keer: bewaar stad1; tweede keer: zoek pad

Klasse Netwerk: de modellering van een netwerk Een netwerk bestaat uit een aantal steden en wegen daartussen. Een voor de hand liggende keuze voor de membervariabelen van een netwerk is een collectie van Stad-objecten en een collectie van Weg-objecten. Als we er echter voor zorgen dat elke stad zijn eigen uitgaande wegen bewaart, hoeven de wegen niet eens apart in het netwerk bewaard te worden, en bestaat een netwerk dus alleen maar uit een collectie van steden. Een van de methoden van Netwerk hebben we al beloofd: een netwerk-object moet zichzelf kunnen tekenen op een mee te geven Graphics-object. Ook moet een netwerk zichzelf kunnen opbouwen met de informatie uit een le. Verder komt er natuurlijk een methode voor de centrale vraag: het zoeken van een pad tussen twee steden. Tijdens het opbouwen van het netwerk is het handig om aparte methoden te maken die een enkele stad, respectievelijk een enkele weg aan het netwerk toevoegen. De gebruiker klikt punten aan, geen steden. Er zal dus een vertalingsmethode moeten komen die aangeeft of er een stad ligt op de aangegeven plaats, en zo ja, welke. Dit is ook een mooie taak voor het netwerk. Het ontwerp voor de klasse Netwerk is hiermee als volgt:
class Netwerk { Collection<Stad> steden; Netwerk() {...} void bouwStad(...) {...} void bouwWeg(...) {...} Stad vindStad(int,int) {...} Pad zoekPad(Stad,Stad) {...} void lees(String) {...} void teken(Graphics) {...} } // membervariabele // initialisatie

// // // //

ligt er een stad op dit punt? zoek route tussen twee steden bouw netwerk volgens data in file teken jezelf

222

Algoritmen

class Pad { ArrayList<Stad> steden;

class Pad { Stad hier; Pad rest; int kosten;

null

Figuur 32: Twee mogelijke representaties van een Pad

Klassen Stad en Weg: de onderdelen van het netwerk Een stad heeft een naam en een positie op de kaart. Verder hadden we beloofd bij een stad de uitgaande wegen op te slaan. Het is handig als een stad zichzelf op een Graphics-object kan tekenen, zodat Netwerk dat werk kan delegeren. Omdat een stad zijn uitgaande wegen beheert, moet er ook een methode zijn om een nieuwe weg te bouwen. Het ontwerp van de klasse Stad is dus op zn minst:
class Stad { String naam; int x,y; Collection<Weg> wegen; Stad(...) {...} void bouwWeg(...) {...} void teken(Graphics) {...} }

Als we straks naar de goedkoopste route tussen twee steden willen gaan zoeken, dan moet het wel bekend zijn wat het gebruik van een weg tussen twee steden kost. Verder is het van een weg van belang waar hij naartoe loopt. Een weg wordt dus gemodelleerd door:
class Weg { Stad doel; int kosten; Weg(...) {...} void teken(Graphics) {...} }

Klasse Pad: een route door het netwerk Een pad lijkt op het eerste gezicht te bestaan uit een aantal aan elkaar gekoppelde wegen, bijvoorbeeld opgeslagen in een list. Het zal echter handiger blijken om een pad te representeren door de steden waar het pad langs loopt. Een pad zou dus een collection van steden kunnen zijn. Maar een andere leuke representatie is (zie ook guur 32):
class Pad { // List steden // vervangen door een eigen implementatie: Stad hier; Pad rest; int kosten; Pad(...) {...} boolean bevat(Stad) {...} void teken(Graphics) {...} }

Een pad wordt dus voorgesteld door de stad waar hij begint, en daarbij weer een pad-object waarin de rest van het pad is opgeslagen. Daarin bevindt zich dan de volgende stad, en opnieuw een pad-

14.2 Het zoekalgoritme

223

object waarin het dan nog resterende deel van het pad ligt. In de laatste schakel van het pad laten we eenvoudigweg de membervariabele rest onge nitialiseerd. Behalve een constructor- en een teken-methode is er een methode waarmee we kunnen kijken of een bepaalde stad op het pad ligt.

14.2

Het zoekalgoritme

De zoekmethode Het zoeken van een pad tussen twee steden is een lastige aangelegenheid. De essentie van deze methode is dat we een verzameling van veelbelovende, maar nog niet complete paden gaan aanleggen. Die paden gaan we vervolgens proberen uit te breiden tot paden die het doel nog beter benaderen. Dat alles net zo lang totdat we een pad tegenkomen dat op de gewenste bestemming eindigt. De verzameling paden kan worden ge nitialiseerd met een zeer kort pad: het pad met lengte 0 dat in de begin-stad begint en eindigt. Dat is weliswaar nog geen geweldig goed pad, maar door het maar gestaag uit te breiden komen we vanzelf dichter bij het doel. Een eerste opzet van de methode zoekPad is:
Pad zoekPad(Stad van, Stad naar) { paden.push(beginpad); // while (...) // { pad = ...; // if (pad.hier==naar) // return pad; // for (...) // paden.push(...); // } return null; // } voeg een pad van van naar van toe aan de paden; zolang er nog veelbelovende paden zijn pak een pad uit de verzameling eindigt het pad in de bestemming? gevonden! voor alle hier vertrekkende wegen voeg een pad toe aan de verzameling helaas, niets gevonden

Zoeken: niet in een kringetje lopen Door de paden aldoor maar uit te breiden lopen we het gevaar dat een pad weer uitkomt bij het vertrekpunt, zonder dat de bestemming is bereikt. Op die manier blijf je steeds in kringetjes lopen, zonder ooit op de bestemming te geraken. We moeten daarom in de methode een nieuw pad alleen aan de verzameling toevoegen als er geen lus in zit, dat wil zeggen als de nieuwe stad nog niet op het pad voorkomt. De methode bevat in de klasse Pad komt daarbij goed van pas. Zoeken: niet de eerste, maar de beste Bovenstaande opzet van de methode stopt direct met zoeken zodra er een pad gevonden is. Dat wil nog niet zeggen dat dit ook het beste pad is dat er bestaat. Daarvoor moeten we doorzoeken, in de hoop nog een beter pad te vinden. We kunnen hiertoe het algoritme combineren met het patroon zoek de grootste. We krijgen dan:
Pad zoekPad(Stad van, Stad naar) { paden.push(beginpad); beste = null; // while (...) { pad = ...; if (pad.hier==naar) { if (...) // beste = pad; // } for (...) if (...) // paden.push(...); } return beste; // }

voorlopig nog niets gevonden

nieuwe oplossing beter dan de beste? dan is dit de nieuwe beste

geen kringetje?

retourneer de beste, of niets

Sneller zoeken: geen hopeloze gevallen Als we eenmaal een oplossing hebben gevonden, blijven we nu dus verder zoeken in de hoop een betere oplossing te vinden. Maar het heeft natuurlijk geen zin om nog niet-complete oplossingen

224 import java.awt.*; class Net extends Frame { public Net() { this.setSize(800,800); this.setTitle("Routezoeker"); this.addWindowListener( new WindowSluiter() ); this.add( new NetApplet(true), BorderLayout.CENTER ); } public static void main(String [] ps) { new Net().setVisible(true); } private static final long serialVersionUID = 1; } Listing 62: Route/src/Net.java

Algoritmen

10

te blijven exploreren die nu al duurder zijn dan een eerder gevonden oplossingen. Die kunnen dus, net als de circulaire paden, van het zoekproces worden uitgesloten. Sneller zoeken: veelbelovendste eerst Door de truc hierboven gaat het zoeken dus aanzienlijk sneller zodra er een eerste oplossing gevonden is. Het is dus zaak zo snel mogelijk alvast een oplossing te vinden. Een manier om dit te bespoedigen is om als eerste te zoeken in een richting die veelbelovend lijkt. We kennen tenslotte de co ordinaten van de steden, en als de road prizing een beetje logisch is opgebouwd dan zijn wegen die de verkeerde kant op gaan een goede manier om de reis duur te maken. . . Wil je van Utrecht naar Groningen reizen, dan kun je het beste eerst Amersfoort of desnoods Hilversum proberen, en niet Geldermalsen! Dit gaat helaas niet altijd goed. Van Hoorn naar Leeuwarden reis je niet via Enkhuizen, hoewel dat op het eerste gezicht de goede kant op lijkt te gaan. Maar geen nood: kwaad kan het ook niet om de wegen in een bepaalde volgorde te proberen. Tot nu toe hadden we ze tenslotte in een willekeurige volgorde geprobeerd. (Een poging tot versnelling van een algoritme die in veel gevallen, maar niet altijd, goed werkt, maar verder ook geen kwaad kan, heet een heuristiek). Klasse Stack: een stapel objecten In de package java.util staat nog een bijzondere implementatie van List: de klasse Stack. Deze klasse modelleert een stapel objecten, waarbovenop nieuwe elementen kunnen worden toegevoegd, en waar elementen alleen maar van bovenop de stapel kunnen worden teruggepakt (slecht idee om je post op deze manier te behandelen). Deze klasse kent een paar extra methoden, waaronder: boolean empty() kijkt of de stapel leeg is void push(Object x) voegt een object toe bovenop de stapel Object pop() verwijdert het object bovenop de stapel en levert dat op Uitwerking van het programma Het routezoek-programma staat in de hiernavolgende klassen. Netwerk, met drie taken: het opbouwen van het netwerk (zie listing 67); het zoekalgoritme (zie listing 68); het inlezen van een netwerk vanuit een le, en het tekenen van een netwerk op een Graphics-object (zie listing 69). NetApplet, voor de grasche userinterface (zie listing 63); Stad, voor de modellering van een stad (zie listing 64); Weg, voor de modellering van een weg tussen twee aangrenzende steden (zie listing 65); Pad, voor de modellering van een route tussen twee steden (zie listing 66). Net, een applicatie die een applet opstart. (zie listing 62).

blz. 228 blz. 229 blz. 230 blz. 225 blz. 226 blz. 227 blz. 227 blz. 224

14.2 Het zoekalgoritme import java.awt.*; import java.awt.event.*; import java.applet.Applet;


5

225

public class NetApplet extends Applet implements MouseListener { Netwerk netwerk; Pad pad; Stad stad1; public void init() { this.addMouseListener(this); netwerk = new Netwerk(); if (isDeelVanApplicatie) netwerk.lees("spoor.txt"); else netwerk.leesUrl( this.getDocumentBase(), this.getParameter("netwerk") ); } public void paint(Graphics g) { netwerk.teken(g); g.setColor( Color.RED ); if (pad!=null) pad.teken(g); g.setColor( Color.BLUE ); if (stad1!=null) stad1.teken(g); } public void mousePressed (MouseEvent e) { Stad s = netwerk.vindStad(e.getX(), e.getY()); if (s!=null) { if (stad1==null) stad1 = s; else { pad = netwerk.zoekPad(stad1, s, true); stad1 = null; } this.repaint(); } } public public public public void void void void mouseEntered (MouseEvent mouseExited (MouseEvent mouseReleased(MouseEvent mouseClicked (MouseEvent e) e) e) e) {} {} {} {}

10

15

20

25

30

35

40

45

boolean isDeelVanApplicatie; public NetApplet() { this(false); }

50

55

NetApplet(boolean app) { this.isDeelVanApplicatie = app; if (app) this.init(); } private static final long serialVersionUID = 1; } Listing 63: Route/src/NetApplet.java

226

Algoritmen

import java.util.*; import java.awt.Graphics; class Stad implements Comparator<Weg> { String naam; Collection<Weg> wegen; int x, y; public Stad(String s, int ax, int ay) { naam = new String(s); wegen = new LinkedList<Weg>(); x = ax; y = ay; } public void bouwWeg(Stad doel, int kosten) { wegen.add( new Weg(doel, kosten) ); }
20

10

15

public void teken(Graphics g) { g.fillRect( x-5, y-5, 10, 10 ); g.drawString(naam, x+6, y ); }


25

30

// orden twee wegen in *dalende* volgorde // wat betreft de afstand van hun doel tot deze Stad public int compare(Weg a, Weg b) { return afstand(b.doel) - afstand(a.doel); } int afstand(Stad a) { return afstand(a, this); }

35

static int afstand(Stad a, Stad b) { return kwadraat(a.x-b.x) + kwadraat(a.y-b.y); }


40

static int kwadraat(int x) { return x*x; } } Listing 64: Route/src/Stad.java

14.2 Het zoekalgoritme

227

import java.awt.Graphics; class Weg { Stad doel; int kosten; public Weg(Stad s, int k) { doel = s; kosten = k; } public void teken(Graphics g, Stad s) { g.drawLine( s.x, s.y, doel.x, doel.y ); } } Listing 65: Route/src/Weg.java

10

15

import java.awt.Graphics; class Pad { Stad hier; Pad rest; int kosten; public Pad( Stad s, Pad r, int k) { hier = s; rest = r; kosten = k; if (rest!=null) kosten += rest.kosten; } public boolean bevat(Stad s) { if (hier==s) return true; if (rest==null) return false; return rest.bevat(s); } public void teken(Graphics g) { hier.teken(g); if (rest!=null) { g.drawLine( hier.x, hier.y, rest.hier.x, rest.hier.y ); rest.teken(g); } } } Listing 66: Route/src/Pad.java

10

15

20

25

228

Algoritmen

import import import import


5

java.util.*; java.io.*; java.net.*; java.awt.Graphics;

class Netwerk { Collection<Stad> steden;


10

public Netwerk() { steden = new LinkedList<Stad>(); } public void bouwStad(String naam, int x, int y) { steden.add( new Stad(naam, x, y) ); } public void bouwWeg(String naam1, String naam2, int prijs) { Stad st1 = vindStad(naam1); Stad st2 = vindStad(naam2); st1.bouwWeg(st2, prijs); st2.bouwWeg(st1, prijs); } public Stad vindStad(String naam) { for (Stad st : steden) { if (st.naam.equals(naam)) return st; } return null; } public Stad vindStad(int x, int y) { for (Stad st : steden) { if ( Math.abs(x-st.x)<5 && Math.abs(y-st.y)<5 ) return st; } return null; }

15

20

25

30

35

40

Listing 67: Route/src/Netwerk.java, deel 1 van 3

14.2 Het zoekalgoritme

229

public Pad zoekPad(Stad van, Stad naar, boolean zoekSlim) { Stack<Pad> paden = new Stack<Pad>(); Pad beste = null;
45

paden.push( new Pad(van,null,0) ); while( !paden.empty() ) { Pad pad = paden.pop(); if (pad.hier==naar) { if (beste==null || pad.kosten<beste.kosten) beste = pad; } Collection<Weg> wegen; if (zoekSlim) { // sorteer de wegen op grond van afstand tot "naar" wegen = new TreeSet<Weg>( naar ); wegen.addAll(pad.hier.wegen); } else wegen = pad.hier.wegen; for (Weg weg : wegen) { if (!pad.bevat(weg.doel) && (beste==null || pad.kosten+weg.kosten<=beste.kosten) ) paden.push( new Pad(weg.doel, pad, weg.kosten) ); } } return beste; }

50

55

60

65

70

75

public void lees(String filenaam) { try { this.lees(new BufferedReader(new FileReader(filenaam))); } catch (Exception e) {} } public void leesUrl(URL base, String naam) { try { BufferedReader br = new BufferedReader(new InputStreamReader(new URL(base, naam) . openConnection() . getInputStream() )); this.lees(br); } catch (Exception e) {System.out.println(""+e); } } Listing 68: Route/src/Netwerk.java, deel 2 van 3

80

85

230

Algoritmen

private void lees(BufferedReader regels) throws Exception { String regel;


90

95

100

105

while ( (regel=regels.readLine()) != null) { Scanner sc = new Scanner(regel); if (sc.hasNext()) { String command = sc.next(); if (command.equals("stad")) { String naam = sc.next(); int x = sc.nextInt(); int y = sc.nextInt(); bouwStad( naam, x, y ); } else if (command.equals("weg")) { String van = sc.next(); String tot = sc.next(); int kosten = sc.nextInt(); bouwWeg( van, tot, kosten ); } } } }

110

115

public void teken(Graphics g) { for (Stad stad : steden) { stad.teken(g); for (Weg w : stad.wegen) { w.teken(g, stad); } } } } Listing 69: Route/src/Netwerk.java, deel 3 van 3

14.3 Toepassing: automatische taalherkenning

231

14.3

Toepassing: automatische taalherkenning

Beschrijving van de casus Het programma in deze toepassing heet taal, omdat het van een ingetikte tekst kan herkennen in welke taal het geschreven is. Om zon programma te kunnen schrijven moeten we ons eerst afvragen: hoe doe je dat eigenlijk, talen herkennen? Bekijk eens het onderstaande couplet uit het gedicht Jabberwocky van Lewis Carroll, en drie vertalingen daarvan: Twas brillig, and the slithy toves Did gyre and gimble in the wabe: All mimsy were the borogoves, And the mome raths outgrabe. Es brillig war. Die schlichten Toven Wirrten und wimmelten in Waben; Und aller-m umsigen Burggroven ath ausgraben. Die mohmen R Il brigue: les t oves libricilleux Se gyrent en vrillant dans la guave. Enm m es sont les goubebosqueux Et le m omerade horsgrave. t Wier bradig en de slijple torfs driltolden op de wijde weep. Misbrozig stonden borogorfs t verdwoolde grasvark schreep.

De woorden zijn in alle vertalingen net zo onzinnig als in het origineel. Toch zie je direct om welke taal het gaat. Dat kan blijkbaar zonder de inhoud te begrijpen, dus puur op grond van de vorm van de tekst. Een aspect van de vorm is de frequentie van de verschillende letters. Ook als je geen Albanees es, en een Finse tekst aan en Fins kent kun je een Albanese tekst herkennen aan de vele qs en de vele us en andere klinkers. Op deze manier moet het programma ook werken: op grond van de letterfrequentie wordt de beslissing genomen in welke taal de aangeboden tekst waarschijnlijk geschreven is. Natuurlijk kent het programma de talen niet echt, zoals wij Nederlands of Engels kennen. Maar wat doet dat er toe, als hij ze op deze manier wel feilloos kan onderscheiden? Of zou je toch kunnen zeggen dat een computer zon taal dan kent ? Modellering We kunnen natuurlijk de kennis over diverse talen in het programma inbouwen. Het programma wordt daar behoorlijk lang van; met veel if-opdrachten moeten de kenmerken van de verschillende talen worden onderscheiden. Bovendien moeten we van tevoren weten welke talen de gebruiker zoal wil onderscheiden, en voor al deze talen een letterfrequentie-analyse plegen. Omdat het programma toch al de letterfrequentie voor de aangeboden tekst moet kunnen bepalen, kunnen we het programma met gebruikmaking van dezelfde methoden ook de letterfrequenties in de diverse talen laten uitzoeken. Daarvoor is het nodig dat we van de te herkennen talen een representatieve voorbeeldtekst aanbieden. Op grond daarvan kan het programma een beeld vormen van de letterfrequenties in de verschillende talen. In het programma zelf zit dus geen kennis over talen ingebouwd; het programma leert door het bekijken van voorbeelden. Een voordeel hiervan is dat de gebruiker zelf kan beslissen welke talen er herkend moeten kunnen worden (op voorwaarde dat er een voorbeeldtekst beschikbaar is). In het programma zijn tabellen nodig die voor elke letter uit het alfabet aangeven hoe vaak hij voorkomt. We beperken ons tot letters; leestekens en andere symbolen worden niet in de analyse meegenomen. Hoofdletters en kleine letters worden niet onderscheiden; dat zou maar aeiden van de hoofdzaak: de letterfrequentie. Voor het gemak beperken we ons tot het westerse 26-letter alfabet (het programma kan desgewenst aangepast worden om ook andere letters mee te tellen). Verschil van frequentietabellen We moeten een manier bedenken waarmee we kunnen beslissen wanneer een frequentietabel meer op een bepaalde tabel lijkt dan op een andere. Daarvoor moeten we een maat hebben voor het verschil van twee tabellen: hoe groter de waarde daarvan, hoe minder de tabellen op elkaar lijken. Een tabel lijkt dan het meeste op de tabel waarmee het verschil het kleinste is. Er zijn verschillende mogelijkheden voor de denitie van zon maat. We moeten een aantal arbitraire beslissingen nemen. Misschien kan het programma later nog verbeterd worden door deze keuzes aan te passen of te verjnen.

232

Algoritmen

Figuur 33: Het programma Taalherkenning in werking

De frequentie van alle letters draagt bij in het totaal. We bepalen daarom het verschil van alle 26 individuele tellers, en totaliseren dat. Eventueel zouden sommige letters zwaarder meegewogen kunnen worden dan andere. Het is echter niet meteen duidelijk welke letters dat zouden moeten zijn, dus daarom wegen we alle letters voorlopig maar even zwaar. In een langere tekst komen letters natuurlijk vaker voor dan in kortere teksten. Omdat de lengte van de tekst niet van invloed mag zijn op de keuze van de taal, vergelijken we de relatieve letterfrequenties ten opzichte van het totaal. Zowel een te kleine als een te grote letterfrequentie moet bijdragen aan een groter verschil. Het mag niet zo zijn dat een tekort aan as een teveel aan bs opheft. Daarom gebruiken we de absolute waarde van het (relatieve) verschil van de tellers. Het kwadraat van het verschil had ook gekund. Hier is nog ruimte om te experimenteren. Als maat voor het verschil van twee tabellen nemen we dus de som van de absolute waardes van de verschillen tussen de relatieve letterfrequenties. Om op een mooi getal tussen 0 en 1 uit te komen delen we het geheel nog door 26. Gebruikersinterface Op de gebruikersinterface is ruimte om de te determineren tekst in te tikken. Verder moet de gebruiker een aantal voorbeeldtalen kunnen aangeven. Daarvan wordt steeds de naam opgegeven, en een representatieve voorbeeldtekst. Die voorbeeldtekst wordt gegeven in de vorm van een url op het www. Er zijn genoeg buitenlandse sites waarvandaan we gemakkelijk voorbeeldteksten kunnen ophalen. Verder is er een knop aanwezig, waarmee de gebruiker het herken-proces kan starten. Tenslotte is er een Label waarop het resultaat getoond wordt. De vijf windrichtingen van de BorderLayout komen precies van pas om deze componenten onder te brengen. De taal-namen en -urls worden ondergebracht in een aparte Panels, die ieder een GridLayout hebben. De interface is afgebeeld in guur 33. Opbouw van de klassen Een frequentietabel is in dit programma duidelijk een belangrijk object. Nu hadden we in sectie 11.4 al een klasse TurfTab gemaakt, waarmee de frequentie van afzonderlijke letters geturfd kan worden. Die klasse komt hier ook weer goed van pas. Er zat in de klasse echter nog geen methode waarmee de relatieve frequentie van de letters bepaald kon worden, en er was ook nog geen methode om de

14.3 Toepassing: automatische taalherkenning

233

invoer van het WWW te lezen. We gaan daarom een subklasse RelTurfTab maken van TurfTab, waarin de benodigde extra methoden worden toegevoegd. In een andere klasse, genaamd Taal, bouwen we de userinterface op. In de methode actionPerformed daarvan wordt het vergelijken van de frequentietabellen gestuurd. Algoritme zoek de kleinste Hart van het programma is de methode actionPerformed. Daarin maken we om te beginnen een frequentietabel van de onbekende tekst, die uit het tekstveld wordt geplukt:
onbekend = new RelTurfTab(); onbekend.turf( tekst.getText() );

Die moet vergeleken worden met alle voorbeeldteksten. In een for-opdracht pakken we daarom e en voor e en de URL van zon voorbeeldtekst, en construeren daarmee een nieuwe frequentietabel:
for (int i=0; i<aantal; i++) { naam = url[i].getText(); voorbeeld = new RelTurfTab(); voorbeeld.leesUrl(naam);

De frequentietabel van de onbekende tekst wordt nu vergeleken met die van de onderhavige voorbeeldtekst. Het resultaat is een verschilwaarde:
verschil = onbekend.verschil(voorbeeld);

Is die verschilwaarde kleiner dan wat we tot nu toe dachten dat de kleinste was, dan hebben we een nieuwe waarde voor kleinste gevonden. Bovendien onthouden we in dat geval de naam van de bijbehorende taal:
if (verschil<kleinste) { kleinste = verschil; antwoord = taal[i].getText(); }

Na aoop van de for-opdracht kunnen we het antwoord aan de gebruiker tonen:


} uitvoer.setText("best passende taal is " + antwoord );

Natuurlijk moeten de variabelen kleinste en antwoord worden ge nitialiseerd. Dat gebeurt helemaal aan het begin met:
kleinste = 1.0; antwoord = "niet te bepalen";

De waarde 1.0 is de groter dan het grootst mogelijke verschil tussen frequentietabellen. Dat kan dus alleen maar kleiner worden. De beginwaarde van antwoord is een relevante tekst voor het geval alle teksten onverhoopt onleesbaar blijken (bijvoorbeeld als er geen internet-verbinding is). Vergelijk relatieve frequenties De methode verschil in de klasse RelTurfTab moet het verschil van twee frequentietabellen berekenen, op de in de vorige sectie besproken manier. De methode heeft e en van deze twee tabellen onder handen (aanspreekbaar met de naam this). De tweede tabel geven we als parameter van de methode mee (met de naam andere). De methode verloopt precies zoals besproken:
public double verschil(RelTurfTab andere) { double totaal = 0.0; for (int i=0; i<26; i++) totaal += Math.abs( this.relatief(i) - andere.relatief(i) ); return totaal/26; }

De hierin benodigde methode relatief maakt gebruik van de van TurfTab ge erfde variabelen tellers en totaal:
double relatief(int i) { return (double)tellers[i] / totaal; }

234

Algoritmen

Initialisaties Voor het aantal talen dat de gebruiker als voorbeeld kan invoeren is in het programma gekozen voor 10. Het had echter net zo goed iets meer of minder kunnen zijn. Toch is die waarde 10 steeds nodig: bij het cre eren van de array, als bovengrens van de for-teller, enzovoort. Om het programma later eventueel te veranderen met een groter of kleiner aantal, is consequent de variabele aantal gebruikt, in plaats van de constante 10. Om er zeker van te zijn dat deze variabele niet (per ongeluk) veranderd wordt, is bij de declaratie aangegeven dat de waarde final is, dat wil zeggen: niet veranderd mag worden. Daarom ook moet de waarde direct bij declaratie al toegekend worden. Om de gebruiker niet op te zadelen met de vervelende taak elke keer weer de voorbeeldtalen in te tikken, zet het programma alvast een aantal voorbeelden neer. In het programma zijn deze talen met bijbehorende urls in een array neergezet, zodat ze in de for-opdracht waar de tekstvelden worden gecre eerd, meteen van een opschrift kunnen worden voorzien. Deze array wordt bij declaratie meteen van een waarde voorzien:
String [] defaultWaarden = { "Nederlands", "http://www.nrc.nl/" , "Engels", "http://www.washingtonpost.com/" , "Duits", "http://www.nzz.ch/" , "Frans", "http://www.lemonde.fr/" , "Spaans", "http://www.elpais.es/" };

Exceptions niet afhandelen In de methode leesUrl worden allerlei handelingen met les verricht, zonder dat deze opdrachten in een try-catch-opdracht staan. Normaal gesproken is dat niet toegestaan, maar hier wordt in de methode-header aangekondigd dat de methode niet zn eigen exceptions afhandelt:
public void leesUrl(String naam) throws Exception

Met de mededeling throws Exception wordt de verantwoordelijkheid voor het afvangen van de exception overgedragen aan degene die de methode aanroept. Inderdaad is er bij aanroep van leesUrl zorg gedragen voor een try-catch opdracht. Invoer van WWW Lezen van hypertekst op het WWW gebeurt op vrijwel dezelfde manier als lezen van lokale les. De kunst is om aan de gewenste InputStreamReader te komen. Dat gaat als volgt: uitgaande van een string die de locatie van de gewenste pagina bevat, wordt een object van type URL gemaakt: u = new URL(s); naar die URL wordt connectie gezocht (waarvoor dus een internet-verbinding noodzakelijk is): c = u.openConnection(); van zon connection kun je een byte-stream te pakken krijgen, door middel van de aanroep: is = c.getInputStream(); die kan geconverteerd worden naar een character-stream door hem als parameter mee te geven: isr = new InputStreamReader(is); daarmee kan tenslotte, net als bij les, een BueredReader worden gemaakt: br = new BufferedReader(isr); de BueredReader kan gebruikt worden om strings te lezen door aanroep van: regel = br.readLine(); Als deze opdrachten (behalve de laatste) zijn in het programma in listing 71 gecombineerd tot e en opdracht; het is tenslotte niet nodig om de tussenresultaten steeds in variabelen op te slaan. Herdenitie van ge erfde methoden In WWW-paginas komt vaak veel Engelse tekst voor in de vorm van tags en lenamen. Die kan de letterfrequentie van de voorbeeldtalen lelijk verstoren. Daarom bouwen we een vooziening in dat alles tussen < en > niet wordt meegeteld, zodat tags en hun parameters buiten beschouwing blijven. Hiertoe herdeni eren we de methode turf uit TurfTab, die e en character turft. In de herdenitie wordt gecontroleerd of dit zon punt-haakje is. Zo ja, dan wordt een boolean variabele veranderd die aangeeft of letters vanaf nu wel of niet meegeteld moeten worden. Als het character geen punthaakje is, wordt de oorspronkelijke methode turf (die bereikbaar is via de pseudo-variabele

blz. 236

14.3 Toepassing: automatische taalherkenning class TurfTab { int [] tellers; int totaal;
5

235

public TurfTab() { tellers = new int[26]; }


10

15

20

public void turf(char c) { if (c>=A && c<=Z) { tellers[c-A]++; totaal++; } else if (c>=a && c<=z) { tellers[c-a]++; totaal++; } } public void turf(String s) { for (int i=0; i<s.length(); i++) turf( s.charAt(i) ); } public String toString() { String s = ""; for (int i=0; i<26; i++) s += (char)(i+A) + ": " + tellers[i] + " keer\n"; s += "totaal: " + totaal; return s; } } Listing 70: Taalherkenning/src/TurfTab.java

25

30

35

super) alleen maar aangeroepen als de boolean variabele aangeeft dat we niet met een tag bezig zijn. De boolean variabele teltmee is, doordat hij als klasse-variabele is gedeclareerd, een onderdeel geworden van de toestand van de turftabel. In de constructormethode krijgt deze zijn initi ele waarde. De hergedenieerde methode turf wordt aangeroepen vanuit de klasse TurfTab. Zoals altijd bij herdenities, hebben we daarmee die oorspronkelijke klasse mooi te pakken: in plaats van zijn eigen turf-een-character methode, roept die nu de hergedenieerde versie aan!

236 import java.awt.*; import java.awt.event.*; public class Taal extends Frame implements ActionListener { TextArea tekst; Label uitvoer; TextField [] taal; TextField [] url; final int aantal = 10; public Taal(String [] startwaarde) { Panel talen, urls; Button knop; int i; // Maken van de interfacecomponenten talen = new Panel(); urls = new Panel(); tekst = new TextArea(10,50); uitvoer = new Label(); knop = new Button("Herken"); taal = new TextField[10]; // de array als geheel url = new TextField[10]; for (i=0; i<aantal; i++) { taal[i] = new TextField(10); // de aparte elementen url [i] = new TextField(30); } // Configureren van de interfacecomponenten this.setSize(600,500); this.setTitle("Taalherkenning"); talen.setLayout( new GridLayout(aantal+1,1) ); urls .setLayout( new GridLayout(aantal+1,1) ); // Opbouw van de interface talen.add( new Label("Taal")); urls .add( new Label("Voorbeeldtekst")); for (i=0; i<aantal; i++) { talen.add(taal[i]); urls .add(url [i]); if (i<startwaarde.length/2) { taal[i].setText(startwaarde[2*i] ); url [i].setText(startwaarde[2*i+1]); } } this.add(tekst, BorderLayout.NORTH ); this.add(talen, BorderLayout.WEST ); this.add(urls, BorderLayout.CENTER); this.add(knop, BorderLayout.EAST ); this.add(uitvoer, BorderLayout.SOUTH ); // Toevoegen van interactie this.addWindowListener(new WindowSluiter()); knop.addActionListener(this); }

Algoritmen

10

15

20

25

30

35

40

45

50

Listing 71: Taalherkenning/src/Taal.java, deel 1 van 2

14.3 Toepassing: automatische taalherkenning

237

55

public void actionPerformed(ActionEvent e) { RelTurfTab onbekend, voorbeeld; double kleinste, verschil; String naam, antwoord; onbekend = new RelTurfTab(); onbekend.turf( tekst.getText() ); kleinste = 1.0; antwoord = "niet te bepalen";

60

65

70

75

uitvoer.setText("bezig met vergelijken..."); for (int i=0; i<aantal; i++) { naam = url[i].getText(); if (!naam.equals("")) try { voorbeeld = new RelTurfTab(); voorbeeld.leesUrl(naam); verschil = onbekend.verschil(voorbeeld); uitvoer.setText(naam + ": " + verschil + "..." ); if (verschil<kleinste) { kleinste = verschil; antwoord = taal[i].getText(); } } catch (Exception ex) { uitvoer.setText( naam + ": " + ex ); }

80

85

} uitvoer.setText("best passende taal is " + antwoord + " (" + kleinste + ")" ); } public static void main(String [] parameters) { String [] defaultWaarden = { "Nederlands", "http://www.nrc.nl/" , "Engels", "http://www.washingtonpost.com/" , "Duits", "http://www.nzz.ch/" , "Frans", "http://www.lemonde.fr/" , "Spaans", "http://www.elpais.es/" }; if (parameters.length==0) parameters = defaultWaarden;

90

95

100

Taal t; t = new Taal(parameters); t.setVisible(true);


105

} private static final long serialVersionUID = 1; } Listing 72: Taalherkenning/src/Taal.java, deel 2 van 2

238

Algoritmen

import java.io.*; import java.net.*; class RelTurfTab extends TurfTab { boolean teltmee; public RelTurfTab() { teltmee = true; } public double relatief(int i) { return (double)tellers[i] / totaal; }
15

10

20

public double verschil(RelTurfTab andere) { double totaal = 0.0; for (int i=0; i<26; i++) totaal += Math.abs( this.relatief(i) - andere.relatief(i) ); return totaal/26; } public void leesUrl(String naam) throws Exception { BufferedReader reader; String s; reader = new BufferedReader(new InputStreamReader (new URL(naam) . openConnection() . getInputStream())); while ( (s = reader.readLine()) != null) this.turf(s); } public void { if else if else if } } Listing 73: Taalherkenning/src/RelTurfTab.java turf(char (c==<) (c==>) (teltmee) c) teltmee = false; teltmee = true; super.turf(c);

25

30

35

14.3 Toepassing: automatische taalherkenning

239

Opgaven
14.1 Preprocessing Bekijk het taalherkennings-programma. Elke keer als de gebruiker op de knop drukt, wordt de taal in de TextArea vergeleken met de voorbeeld-talen. De voorbeeld-talen worden op dat moment ingelezen. Als de gebruiker in de TextArea een andere tekst intikt en weer op de knop drukt, worden de voorbeeld-talen nogmaals ingelezen, en wordt er weer een frequentietabel van gemaakt. Dat is eigenlijk zonde van het werk. Hoe kan het programma aangepast worden, zo dat de voorbeeld-talen alleen worden ingelezen als dat echt nodig is? 14.2 Klasse Stack In deze opgave verplaatsen we ons in de schrijver van het package java.util: je moet zelf (een deel van) de klasse Stack schrijven. De bestaande collection-klassen mag je daarbij niet gebruiken. Je mag wel arrays gebruiken. De klasse Stack kent vier methodes: Stack, push, pop en empty (er zijn nog wel meer methodes, maar die laten we hier buiten beschouwing). In een stack kunnen objecten worden opgeslagen op een stapel: met de methode push leg je iets bovenop de stapel, en met de methode pop kun je het object van de stapel pakken dat het laatst bovenop de stapel is gelegd (en daarbij tevens van de stapel verwijderen). De methode Stack cre eert een lege stapel, en de methode empty kijkt of een stapel op dit moment leeg is. Schrijf de klasse Stack en de methoden daarvan, waarbij je de objecten achter de schermen in een array opslaat. Om te beginnen kun je in deze array ruimte maken voor 10 elementen, maar als de gebruiker push aanroept op een moment dat er al 10 objecten op de stapel liggen, moet je de beschikbare ruimte verdubbelen, door een nieuwe array te cre eren en de bestaande gegevens daarheen te kopi eren. Als later ook die ruimte op is, moet de ruimte weer worden verdubbeld, zodat er ruimte komt voor 40 objecten, enzovoort. Schrijf een statische methode test waarin de klasse Stack als volgt uitgeprobeerd wordt: er wordt een nieuwe stack gemaakt, daarop worden de strings aap, noot, en mies neergezet, daarna wordt het bovenste element van de stack gepakt, en er wordt getest of dat een string is die de letters mies bevat. De uitkomst van de vergelijking wordt als waarheidwaarde opgeleverd als resultaat van de methode test. (Als de test goed verloopt, zal deze methode dus de waarde true opleveren). Hint: zorg ervoor dat in elk stack-object een array beschikbaar is waarin de strings worden opgeslagen, plus een teller die aangeeft tot hoever de array op dit moment gevuld is. 14.3 Klasse hashtabel Nog een kijkje achter de schermen van de collection-klassen. Een hashtabel is een datastructuur waarin een verzameling strings bewaard kan worden. Er zijn twee operaties op gedenieerd: voegtoe, waarmee een string aan de verzameling kan worden toegevoegd; zoek, waarmee bepaald kan worden of een bepaalde string deel uitmaakt van de verzameling Natuurlijk kan zon datastructuur gemaakt worden door de strings domweg in een array te zetten. Toevoegen is dan gemakkelijk, maar voor het zoeken zul je de hele array moeten doorlopen. Een hash-tabel is daarom slimmer opgebouwd: zowel toevoegen als zoeken kunnen doorgaans in e en of enkele stappen worden gedaan. In deze opgave gebruiken we hash-tabellen met ruimte voor 100 strings. We gaan aan elke string een getalswaarde koppelen: de zogenaamde hashwaarde. De hashwaarde wordt als volgt berekend: de ascii-waarde van alle symbolen in de string worden opgeteld, en van het resultaat wordt het gedeelte kleiner dan 100 gebruikt. Voorbeeld: de hash-waarde van de string AAP is 10, want de opgetelde ascii-waarden geven 65+65+80=210. De hash-waarde van NOOT is 20, want 78+79+79+84=320. Opgave: schrijf een statische methode hash, die de hash-waarde van een string berekent. Bij het toevoegen van een string aan de verzameling wordt deze in principe op de plaats aangeduid door de hash-waarde gezet. Alleen als deze plaats al bezet is, wordt de volgende plaats gebruikt. Als die ook bezet is de daaropvolgende, enzovoorts. Als je zo het eind van de tabel bereikt, kun je weer van voren af doortellen: na plaats 99 komt plaats 0. Opgave: schrijf een methode voegtoe, die een string toevoegt aan een hash-tabel. Schrijf nu de methode zoek. Deze methode begint te zoeken op de plaats aangeduid door de hash-waarde van de string. Als deze bezet is door een andere string, moeten we nog even verder kijken. Kom je echter een lege plaats tegen, dan is verder zoeken onnodig.

240 14.4 Totalizator Gegeven is de volgende klasse:


public class Hoofd { public static void main(String [] ps) { Totalizator tot; for (int t=0; t<ps.length; t++) { tot = new Totalizator(); try { tot.leesBytes(ps[t]); System.out.println(tot.resultaat()); } catch (Exception e) { System.out.println(""+e); } } } }

Algoritmen

die gebruik maakt van de klasse Totalizator:


class Totalizator { int totaal = 0; void data(int x) { totaal += x; } String resultaat() { return "totaal: " + totaal + "\n"; } // methode leesBytes nog toe te voegen }

a. Hoe krijgt de array ps zijn waarde? b. Wat gebeurt er als de regel


tot = new Totalizator();

buiten de for-opdracht zou worden geschreven? c. Wat gebeurt er als het woord try en het catch-gedeelte buiten (voor, resp. na) de for-opdracht worden geschreven? d. Schrijf de methode leesBytes in de klasse Totalizator. Zorg ervoor dat, mocht er bij het lezen van de le iets fout gaan, de methode main deze fout kan afhandelen. e. We vervangen in de methode main nu de expressie new Totalizator() door new Teller(). Schrijf de klasse Teller, zodat het programma in plaats van output als totaal: 1234 een tabelletje maakt waarin van elke mogelijke byte wordt aangegeven hoe vaak die in de le voorkwam, met daarin regels van de vorm:
byte 0 komt 5 keer voor byte 1 komt 12 keer voor byte 2 komt 0 keer voor

enzovoorts. Daarbij mag de methode leesBytes niet opnieuw worden geschreven; voor dat werk moet gebruik worden gemaakt van de methode in Totalizator. f. We vervangen de expressie in main nu nog eens, ditmaal door new PauzeTeller(). Schrijf de klasse PauzeTeller, zodat het programma net zon tabelletje maakt als onder e. Maar ditmaal wordt de byte 0 niet meegeteld, en alle andere bytes die tussen zon 0 en de volgende 0 in de le staan ook niet. Een tweetal nullen kan dus worden beschouwd als een aanduiding negeer alles wat hier tussen staat. De al geschreven klasse Teller moet worden gebruikt voor het eigenlijke tel- en schrijfwerk; ga dus niet nog eens vrijwel hetzelfde opschrijven als bij onderdeel e.

241

Bijlage A

Gebruik van de compiler met Eclipse


Deze appendix beschrijft het gebruik van de Java-compiler Java Standard Edition version 6 van Sun, met gebruikmaking van de ge ntegreerde ontwikkelomgeving (IDE) Eclipse.

A.1

Installatie van de software

Als je de software op een eigen computer wilt gebruiken, moet je deze eerst installeren. Op de practicumcomputers is dit niet nodig; daar is de software al ge nstalleerd. Het installeren gaat als volgt: Stap 1: download compiler en documentatie Ga naar http://java.sun.com/javase/downloads. Download hier twee dingen: JDK 6 Update 14 (zonder JavaFX SDK) (75 megabyte). Java SE 6 Documentation (52 Megabyte). Stap 2: installeer compiler Start het installatieprogramma van de compiler: jdk-6u14.exe. Volg de aanwijzingen van de wizard. De wizard vraagt in welke directory de software moet worden neergezet. Kies een geschikte plek. Stap 3: installeer help Unzip de documentatie jdk-6u10-doc.zip. Laat het bestand uitpakken in de directory die je in stap 2 hebt gekozen. Door het uitpakken van van de zip-le onstaat daar een nieuwe subdirectory docs. Stap 4: download Eclipse IDE Ga naar http://www.eclipse.org/downloads. en download de ge ntegreerde ontwikkelomgeving (IDE) Eclipse Classic 3.5.0 (162 Megabyte). Stap 5: installeer Eclipse Unzip de ge ntegreerde ontwikkelomgeving (IDE) Eclipse eclipse-SDK-3.5-win32.zip. Laat het bestand uitpakken in bijvoorbeeld C:\Program Files (er wordt vanzelf een subdirectory eclipse gemaakt). Misschien vindt je het handig om een shortcut naar het programma eclipse.exe uit die directory op de desktop te zetten. Let op dat het echt een shortcut is (icoon verslepen met de Alt-toets ingedrukt) en dat je niet het programma verplaatst of kopieert.

242

Gebruik van de compiler met Eclipse

A.2

Conguratie van de Eclipse IDE

De eerste keer dat je de ontwikkelomgeving start, moet je een aantal extra handelingen verrichten om de boel goed te congureren. Dat gaat zo: Stap 6: start IDE Run Eclipse. Het icoon staat waarschijnlijk op de desktop, en op de practicumcomputers kun je het vinden onder menukeuze StartProgramsProgrammingEclipse. Het splash screen van Eclipse verschijnt, plus een dialoog waarin gevraagd wordt om een directory voor de workspace. Kies hier een directory waar je minstens 5 Megabyte data kunt schrijven. Op je eigen computer bijvoorbeeld My Documents\studie\imp\practicum. Op de practicumcomputers moet het een directory op de H-drive zijn, bijvoorbeeld H:\imp\practicum. Op de practicumcomputers moet het vooral niet op de C-drive, want daar kun je niet schrijven! Als je zowel thuis als op de universiteit wilt werken, kun je overwegen om de workspace op een USB-stick te zetten.

Je kunt het vinkje do not ask again ook aanzetten, want meerdere workspaces erop na houden is overdreven (en kost elke keer opnieuw 5 Megabyte). Er verschijnt nu een window Eclipse platform met een welkoms-boodschap.

Stap 7: Even wennen aan de parts Je kunt de welkoms-boodschap wegklikken door op het kruisje op het tabblad naast het woord Welcome te klikken. Er verschijnen dan meerdere subwindows (outline, navigator en tasks) in het hoofdwindow.

A.2 Conguratie van de Eclipse IDE

243

De subwindows heten in het Eclipse-jargon parts. Er zijn twee soorten parts: editors en views. In editors kun je een tekst, of een programma of iets dergelijks, intikken. In een view krijg je een overzicht, bijvoorbeeld van alle methoden in het Java-programma dat je aan het schrijven bent. Parts werken een beetje anders dan subwindows die je van andere windows-programmas gewend bent. Ze zijn handiger, en dat is maar goed ook, want we zullen vaak een heleboel parts nodig hebben. Probeer de parts maar eens te verschuiven en te resizen. Je kunt ze ook onderling stapelen, waarbij de titelbalk toch als tabblad zichtbaar blijft. Stap 8: Kies het Java-perspectief Welke parts er precies zichtbaar zijn heet een perspectief. Standaard zijn de hierboven genoemde drie parts zichtbaar; dit perspectief heet Resource. Wij gaan echter Java-programmas ontwikkelen, en kunnen daarom beter een perspectief gebruiken dat daarvoor is toegerust. Je kunt een nieuw perspctief openen met het icoontje naast het woord Resource in de rechterbovenhoek van het window. Er klapt een lijstje uit, waaruit je het perspectief Java kunt kiezen. Dit perspectief bestaat uit zes parts, met nieuwe views als Package explorer en Problems. En, daaronder op de stapel, Hierarchy, Javadoc en Declaration.

244

Gebruik van de compiler met Eclipse

A.3

Een programma schrijven en uitvoeren met Eclipse

Alle voorbereidende werkzaamheden zijn nu gedaan. Op dit punt begin je elke keer als je een nieuw programma wilt gaan beginnen. Stap 10: maak project We gaan een project maken, waarin alle les die bij een programma horen zijn gebundeld. Kies menukeuze FileNewProject Kies uit het lijstje: Java project en klik op Next Bedenk een naam voor het project, bijvoorbeeld Hallo en klik op Next Klik op Finish In de Package explorer view zie je nu een icoon voor dit project. Stap 11: maak Java-klasse Omdat een Java-programma uit e en (of meerdere) klassen bestaat, gaan we een klasse aanmaken: Kies menukeuze FileNewClass Tik in de wizard een naam voor de klasse, bijvoorbeeld weer Hallo Laat alle andere opties ongewijzigd en klik op Finish Negeer de eventuele waarschuwing die gebruik van het default package ontraadt Je ziet in de Package explorer dat ons project nu een default package heeft gekregen bestaande uit de le Hallo.java. De le is alvast geopend in een editor-part. Een stukje van de klasse (de header en wat commentaar) is alvast ingevuld dat scheelt alweer tikwerk. In de Outline-view staat een lijst van alle klassen; tot nu toe is dat er slechts e en.

Stap 12: edit programma Tik nu de rest van het programma Hallo in, zoals beschreven in hoofdstuk 2: Voeg de import-aanwijzingen toe Vul de header van de klasse aan met extends Tik de methode paint in de body van de klasse Kies menukeuze FileSave om de wijzigingen te bewaren Je zult merken dat de editor je helpt: als je een haakje-openen tikt, wordt bijvoorbeeld automatisch een haakje-sluiten toegevoegd. Merk ook op dat de diverse views worden bijgewerkt met de nieuwe dingen die je intikt. Stap 13: compileer en run het programma Je kunt het programma nu uitvoeren met menukeuze RunRun asJava applet. Of klik op het groene rondje met de witte pijl. Als het programma correct is ingetikt, krijg je nu een appletviewer te zien. Zaten er nog fouten in het programma, dan worden die opgesomd in de Problems view.

A.3 Een programma schrijven en uitvoeren met Eclipse

245

Anders dan in hoofdstuk 2 staat vermeld, is er dus nog geen HTML-le nodig. De afmeting van het window staat dus ook niet in de HTML-le, maar kun je instellen met menukeuze RunRun... in het tabblad parameters. Stap 14: maak de HTML-le Wil je het programma ook op een website kunnen plaatsen, dan moet je de HTML-le wel maken: Kies menukeuze FileNewFile en vul de naam Hallo.html in. Rechtsklik de lenaam in de Package explorer, en kies menukeuze Open WithText Editor. Dubbelklik nu op de lenaam In de editor kun je nu de tekst van de HTML-le intikken. Stap 15: run programma met browser In plaats van met Appletviewer, kun je het programma ook interpreteren met een webbrowser. Zoek de html-le met Windows Explorer en dubbelklik op het icoon. De uitvoer verschijnt ditmaal in een Internet Explorer window. Merk op dat je ditmaal de hele inhoud van de html le te zien krijgt, dus ook de tekst Dit is een simpel applet. Het eigenlijke applet verschijnt in een aparte (grijze) rechthoek. Dit in tegenstelling tot de Appletviewer-aanpak uit stap 13, die alleen de uitvoer van het eigenlijke applet laat zien (op een witte achtergrond), met daarbij alleen de tekst Applet started.

Er zijn natuurlijk veel makkelijkere manieren om de tekst Hallo in een Internet Explorer window te krijgen (bijvoorbeeld door het gewoon in de html-le te tikken). Dit eenvoudige voorbeeld was natuurlijk alleen maar bedoeld om alle stappen van het ontwikkelen van een programma te demonstreren voor interessantere programmas gaat dit op dezelfde manier.

246

Gebruik van de compiler met Eclipse

stap 16: documentatie raadplegen Het wordt je tegenwoordig wel heel erg gemakkelijk gemaakt. Als je eventjes aarzelt tijdens het tikken na bijvoorbeeld g. in de body van paint, krijg je direct een lijst van de in de klasse Graphics (en dus voor het object g) beschikbare methodes. Zet je de muis boven een methodenaam, dan zie je de types en volgorde van de benodigde parameters.

Wil je nog meer uitleg over bijvoorbeeld een methode of een klasse, zet de cursor daar dan op en druk op Shift+F2.

247

Bijlage B

Gebruik van de compiler met JCreator


Deze appendix beschrijft het gebruik van de Java-compiler Java Standard Edition version 6 van Sun, met gebruikmaking van de ge ntegreerde ontwikkelomgeving (IDE) JCreator van Xinox software.

B.1

Installatie van de software

Als je de software op een eigen computer wilt gebruiken, moet je deze eerst installeren. Op de practicumcomputers is dit niet nodig; daar is de software al ge nstalleerd. Het installeren gaat als volgt: Stap 1: download compiler en documentatie Ga naar http://java.sun.com/javase/downloads. Download hier twee dingen: JDK 6 Update 14 (zonder JavaFX SDK) (75 megabyte). Java SE 6 Documentation (52 Megabyte). Stap 2: installeer compiler Start het installatieprogramma van de compiler: jdk-6u14.exe. Volg de aanwijzingen van de wizard. De wizard vraagt in welke directory de software moet worden neergezet. Kies een geschikte plek. Stap 3: installeer help Unzip de documentatie jdk-6u10-doc.zip. Laat het bestand uitpakken in de directory die je in stap 2 hebt gekozen. Door het uitpakken van van de zip-le onstaat daar een nieuwe subdirectory docs. Stap 4: download JCreater IDE (alleen als je de CD niet hebt): Download de ge ntegreerde ontwikkelomgeving (IDE) Jcreator LE versie 4.50 van de website http://www.jcreator.com/download.htm. Kies voor de light edition (LE) en niet voor de Pro-editie. Stap 5: installeer IDE (JCreator) Start het installatieprogramma van de ge ntegreerde ontwikkelomgeving (IDE) JCreator: jcreator450.exe. Volg de aanwijzingen van de wizard.

248

Gebruik van de compiler met JCreator

B.2

Conguratie van de JCreator IDE

De eerste keer dat je de ontwikkelomgeving start, moet je een aantal extra handelingen verrichten om de boel goed te congureren. Dat gaat zo: Stap 6: start IDE Run Jcreator. Het icoon staat waarschijnlijk op de desktop, en anders kun je het vinden onder menukeuze StartProgramsProgrammingJcreator (op de practicumcomputers) of onder menukeuze StartProgramsJcreator (op je eigen computer). Het hoofdwindow van JCreator verschijnt. Links in dit window staan twee lege panelen, en bovenin staat de gebruikelijke overvloed aan menus en knoppen.

Stap 7: kies IDE prole JCreator kan worden gebruikt voor verschillende versies van de compiler, bijvoorbeeld voor Java 6 update 14 en de beta-versie Java 7. Welke versie je gebruikt staat vermeld in een prole. Ook als je maar e en versie van de compiler gebruikt, moet je zon prole kiezen: Kies uit het menu menukeuze CongureOptions In de boomstructuur aan de linkerkant van de dialoog kies je JDK Proles De lijst in het midden is waarschijnlijk nog leeg. Druk op de knop New... en blader naar de directory C:\Program Files\Java\jdk1.6.0_14 (op de practicumcomputers), of naar de directory die je in stap 3 hebt gekozen In de lijst in het midden verschijnt nu JDK version 160. Druk op OK. Stap 8: kies directories Je moet nu aan JCreator vertellen waar je les bewaard moeten worden. Standaard is dat op de C-schijf, en dat is geen goed idee in de practicumomgeving, want daar kun je niet schrijven op disk C. Je kunt dat als volgt veranderen: Kies menukeuze CongureOptions In de boomstructuur aan de linkerkant van de dialoog kies je Directories Druk op de . . . -knop naast Default project directory, en kies een plek uit waar je les kunt schrijven. Het is handig om een nieuwe directory aan te maken, want zeer binnenkort zal deze directory overspoeld worden met les. Goede keuzes zijn H:\imp/practicum (op de practicumcomputers) of My Documents\studie\imp\practicum (op je eigen computer). De andere twee directories (Syntax en Project Templates) kun je onveranderd laten.

B.2 Conguratie van de JCreator IDE

249

Stap 9: maak workspace Files die bij elkaar horen (zoals de java-le en de html-le van e en programma) worden geadministreerd in een project. Al gauw zul je aan meerdere projecten tegelijk werken. De verschillende projecten worden op hun beurt geordend in een workspace. We gaan zon workspace maken: Kies menukeuze FileNew Selecteer het tabblad Workspaces Tik een naam voor de workspace achter Workspace name:, bijvoorbeeld Imp of Practicum. Onder Location wordt automatisch een subdirectory met diezelfde naam aangemaakt onder de directory die je in stap 8 hebt aangegeven. Dat is een goed idee; druk op OK.

In een tweede workspace zou je de voorbeeldprogrammas kunnen opnemen, die op de CD in de directory Demo beschikbaar zijn. Die directory zou je in zijn geheel in je Java-directory kunnen eren. Een JCreator-workspace is bijgeleverd. kopi

250

Gebruik van de compiler met JCreator

B.3

Een programma schrijven en uitvoeren met JCreator

Alle voorbereidende werkzaamheden zijn nu gedaan. Op dit punt begin je elke keer als je een nieuw programma wilt gaan beginnen. Stap 10: maak project We gaan een project maken, waarin alle les die bij een programma horen zijn gebundeld. De makkelijkste manier is om een wizard te gebruiken, die een kant-en-klaar project neerzet: Kies menukeuze FileNew Selecteer het tabblad Projects Kies het icoon Basic Java Applet (en dus niet Application) Tik een naam voor het project achter Project name:, bijvoorbeeld Hallo. Deze naam moet met een hoofdletter beginnen. Onder Location wordt automatisch een subdirectory met diezelfde naam aangemaakt onder de workspace-directory die je in stap 9 hebt aangegeven. Dat is een goed idee; druk op OK.

Stap 11: edit programma In de boomstructuur det de opbouw van de workspace weergeeft, heeft de wizard twee les neergezet: Hallo.java en Hallo.html. Dubbelklik op zon lenaam, en de le verschijnt in een edit-window. Het programma lijkt sterk op het in hoofdstuk 2 besproken programma.

B.3 Een programma schrijven en uitvoeren met JCreator

251

Stap 12: compileer programma Nu moet het programma gecompileerd worden. Kies daartoe menukeuze BuildCompile Project.

Als je het programma zelf hebt ingetikt, is het goed mogelijk dat het programma fouten bevat. Je kunt bijvoorbeeld een tikfout hebben gemaakt. In de afbeelding is bijvoorbeeld Grahpics getikt, in plaats van het correcte Graphics. Daarom verschijnt er in het onderste paneel een foutmelding van de compiler, die cannot resolve the symbol. Andere fouten die veel worden gemaakt is het verwarren van hoofdletters en kleine letters: in Java (anders dan in html), is er een verschil tussen paint en Paint. Als je dubbelklikt op een foutmelding, springt de cursor naar de regel die de fout bevat. Vooral in lange programmas is dat erg handig. Aldaar kun je de fout herstellen, en het programma opnieuw compileren met menukeuze BuildCompile Project. Ga hiermee door totdat er geen fouten meer over zijn, en in het Build-paneel de melding Process completed verschijnt. Stap 13: run programma met appletviewer Nu het programma succesvol is gecompileerd, kun je het programma runnen, om te zien wat het resultaat is. Kies daartoe menukeuze BuildExecute Project. Er verschijnt een zwarte DOS-box, die je kunt negeren. Maar er verschijnt ook een Appletviewer window appears, waarin de uitvoer van het applet verschijnt. Eindelijk staat er Hallo! op het scherm. . .

Je kunt de appletviewer sluiten door op de X in de rechterbovenhoek te klikken. De DOS-box

252 verdwijnt dan ook.

Gebruik van de compiler met JCreator

Stap 14: run programma met browser In plaats van met Appletviewer, kun je het programma ook interpreteren met een webbrowser. Zoek de html-le met Windows Explorer en dubbelklik op het icoon. De uitvoer verschijnt ditmaal in een Internet Explorer window. Merk op dat je ditmaal de hele inhoud van de html le te zien krijgt, dus ook de tekst Dit is een simple applet. Het eigenlijke applet verschijnt in een aparte (grijze) rechthoek. Dit in tegenstelling tot de Appletviewer-aanpak uit stap 8, die alleen de uitvoer van het eigenlijke applet laat zien (op een witte achtergrond), met daarbij alleen de tekst Applet started.

Er zijn natuurlijk veel makkelijkere manieren om de tekst Hallo in een Internet Explorer window te krijgen (bijvoorbeeld door het gewoon in de html-le te tikken). Dit eenvoudige voorbeeld was natuurlijk alleen maar bedoeld om alle stappen van het ontwikkelen van een programma te demonstreren - voor interessantere programmas gaat dit op dezelfde manier. Stap 15: documentatie raadplegen Meer informatie over een bepaalde klasse of methode kun je krijgen door de naam in de editor te selecteren (bijvoorbeeld Graphics), en dan menukeuze HelpJDK Help te kiezen. Of klik het woord met de rechter muisknop en kies Show JDK Help uit het context menu, of druk op Ctrl-F1.

253

Bijlage C

Programmeerprojecten
C.1 Mandelbrot
Mandelgetallen Voor elk punt (x, y ) van het platte vlak, waarbij x en y re ele getallen zijn, kan een bijbehorend getal worden bepaald laten we dit het mandelgetal noemen. Om het mandelgetal te kunnen uitrekenen, bekijken we eerst de volgende functie, die punten (a, b) van het vlak transformeert naar andere punten: f (a, b) = (a a b b + x, 2 a b + y ) Let op: deze functie transformeert het punt (a, b), maar in de berekening speelt ook de waarde van x en y , dat is het punt waarvan we het mandelgetal willen bepalen, een rol. Deze functie f nu, passen we toe op het punt (a, b) = (0, 0). Op het punt dat daar uitkomt, passen we nog eens de functie f toe. Op het punt dat daar weer het resultaat van is, passen we opnieuw f toe, enzovoorts enzovoorts. We stoppen pas met toepassen van f als het resultaat-punt een afstand van meer dan 2 tot het punt (0, 0) heeft. Het mandelgetal is nu gelijk aan het aantal keren dat f is toegepast. Voor sommige punten (x, y ) is dat meteen al zo, en is het mandelgetal dus gelijk aan 1. Voor andere punten duurt het langer: die hebben een groter mandelgetal. Er zijn ook punten waarbij je f kan blijven toepassen, zonder dat de afstand tot het punt (0, 0) ooit meer dan 2 wordt: die punten hebben mandelgetal oneindig. In de praktijk kunnen we niet oneindig vaak een functie toepassen; daarom stoppen we met toepassen van f bij een bepaald maximum aantal keren, zeg 100 keer; is dan de afstand nog niet groter dan 2, dan nemen we maar aan dat het mandelgetal oneindig is. Een voorbeeld Als voorbeeld berekenen we het Mandelgetal van het punt (0.5, 0.8). We beginnen met (a, b) = (0, 0) en zetten de teller op 0. Bij het toepassen van de formule zijn a en b nog 0, en komen we dus uit op het punt teller staat nu op 1. De afstand van (0.5, 0.8) tot (0, 0) is volgens (0.5, 0.8). De Pythagoras 0.25 + 0.64 = 0.89 0.94, en dat is niet groter dan 2. We mogen dus doorgaan. We berekenen met de formule de nieuwe a: 0.5 0.5 0.8 0.8 + 0.5 = 0.11 en de nieuwe b: 2 0.5 0.8 + 0.8 = 1.6, en de teller komt op 2. De afstand van (0.11, 1.6) tot (0, 0) is volgens Pythagoras ongeveer 1.6038, dus we mogen nog steeds doorgaan. We berekenen met de formule weer een nieuwe a: 0.11 0.11 1.6 1.6 + 0.5 = 2.0479 en een nieuwe b: 2 0.11 1.6 + 0.8 = 1.152. De afstand van (2.0479, 1.152) tot (0, 0) is royaal meer dan 2, dus de teller blijft staan op 3. Het mandelgetal van (0.5, 0.8) is dus 3. De Mandelbrot-guur Aan de hand van mandelgetallen kunnen we gemakkelijk een kleur van elk punt bepalen. Een mogelijkheid is deze: punten met een even mandelgetal worden wit, punten met een oneven mandelgetal worden zwart. Ook punten met mandelgetal oneiding worden zwart. Teken je van elk punt de aldus bepaalde kleur, dan zie je een afbeelding van de Mandelbrot-guur. Voor x en y tussen 2 en 2 ziet die er als volgt uit:

254

Programmeerprojecten

De guur is opmerkelijk grillig, zeker als je bedenkt hoe relatief eenvoudig het algoritme eigenlijk is. Het wordt nog mooier als je inzoomt op onderdelen van de guur, zoals in het tweede plaatje. Het programma We gaan een programma maken waarmee de gebruiker kan inzoomen op de Mandelbrotguur. In het window staan behalve de guur vier tekstvelden. De gebruiker kan daarin intikken: De x-coordinaat van het midden van het scherm De y -coordinaat van het midden van het scherm De schaalfactor. Bij een schaalfactor van bijvoorbeeld 0.01 kan in een window van 400 bij 400 beeldpunten een gebied voor x en y tussen bijvoorbeeld 2 en 2 worden weergegeven. Het maximum aantal herhalingen van f Druk de gebruiker in een van de tekstvelden daarna op de Enter-toets, dan moet het daarin aangegeven deel van de Mandelbrotguur worden getoond. Ook kan de gebruiker ook met de muis op de guur klikken. Het aangeklikte punt wordt het nieuwe middelpunt, en de schaal wordt twee keer zo klein; de gebruiker zoomt daarmee in op het aangeklikte punt. De nieuwe waarden van midden en schaal worden ook in de tekstvelden weergegeven. De HTML-le Aan het begin van het programma moet het midden (0, 0) zijn, de schaal 0.01, en het maximum aantal herhalingen 100. Als echter in de html-le <PARAM>-tags worden gespeciceerd met de naam x, y, schaal en max, dan moet die waarde als beginwaarde worden gebruikt. De auteur van de html-le kan daarmee alvast een bepaald detail van het plaatje tonen. Maak bij je programma een html-le, waarin je vier exemplaren van het applet opstart: eentje in de beginsituatie, en drie andere op een interessant detail van de guur (neem nou niet precies dezelfde als in het voorbeeld, maar zoek zelf een paar interessante punten met diverse instellingen van zoomfactor en maximum aantal herhalingen). De kleuren Hierboven is een zeer eenvoudige kleuring van de guur beschreven: punten met even mandelgetal worden wit, andere punten zwart. Daar kun je het programma in eerste instantie mee testen. Maar in het denitieve programma moeten de punten gekleurd worden, waarbij de rood-, groenen blauw-component alledrie van het mandelgetal afhangen. De kleur mag echter alleen maar van het mandelgetal afhangen, niet van x en y zelf. Hier zijn twee voorbeelden, maar er zijn nog vele andere mogelijkheden. Wees eens creatief!

C.1 Mandelbrot

255

Zorg er voor dat de guur goed zichtbaar blijft, ook bij andere keuzes van het maximum aantal herhalingen. Optionele extra (voor als je nog tijd over hebt): je kunt de kleurbepaling wellicht nog laten afhangen van extra, door de gebruiker te bepalen parameter(s). Daarvoor mag je eventueel nog extra interactiecomponenten toevoegen.

256

Programmeerprojecten

C.2

Reversi-spel
Spelers zijn niet helemaal vrij in hun keuze waar ze een steen kunnen zetten. Blauw mag een steen alleen maar neerzetten, als daarmee een of meer rode stenen worden ingesloten tussen de nieuwe steen en een van de al op het bord liggende blauwe stenen. Het insluiten kan horizontaal, vertikaal of diagonaal gebeuren. Bijvoorbeeld, in de beginsituatie heeft blauw (zwart), die als eerste aan zet is, de keus uit de velden zoals hiernaast met een rondje aangegeven. Door het doen van de zet veranderen alle stenen van de tegenspeler die met die zet worden ingesloten in de eigen kleur. Bijvoorbeeld, als blauw in de beginsituatie voor de bovenste van de vier mogelijkheden kiest, dan wordt de ingesloten, direct daaronder liggende rode steen blauw. Daarna is rood (grijs) aan zet, die drie mogelijkheden heeft om een blauwe steen in te sluiten.

Het spel Het spel wordt gespeeld op een bord met vierkante velden. De twee spelers kunnen de beurt een blauwe, respectievelijk een rode steen neerzetten op een vrij veld. Aan het begin van het spel liggen er al twee blauwe (zwarte) en twee rode (grijze) stenen op het bord, volgens het volgende patroon (hier getoond op een bord met 6x6 velden, maar het spel kan ook gespeeld worden op een bord met andere afmetingen).

Na nog een tijdje spelen kan de situatie onstaan zoals hier links onder getekend is. Rood is aan zet, en kiest bijvoorbeeld voor het veld aangegeven met de letter A. De twee blauwe stenen er diagonaal rechtsboven (aangegeven met de letters B en C) worden ingesloten, maar ook de blauwe steen er recht boven (aangegeven met de letter D). Die worden dus allemaal rood, en de eindsituatie is zoals hier rechts onder getekend is. Merk op dat als gevolg van het veranderen van kleur weer andere blauwe stenen ingesloten raken (zoals die aangegeven met de letter E), maar die veranderen niet van kleur: bij het insluiten moet de nieuw neergezette steen betrokken zijn.

In zeldzame gevallen kan het gebeuren dat een speler geen zet kan doen, omdat er geen enkele mogelijkheid is om stenen van de andere partij in te sluiten. De speler moet dan passen, en de andere speler is weer aan de beurt. Kan ook de andere speler geen zet doen, dan is het spel afgelopen. Winnaar is aan het eind degene met de meeste stenen op het bord (bij gelijk aantal is de uitslag remise).

C.2 Reversi-spel

257

Het programma Het programma moet een applet zijn dat de volgende dingen toont: Het bord met de huidige situatie De huidige stand: een indicatie van het aantal stenen van rood en blauw De status: blauw aan zet, rood aan zet, blauw is winnaar, rood is winnaar, of remise Twee knoppen: nieuw spel en help Het aantal rijen en kolommen van het bord moet via een <PARAM>-tag hoogte en breedte in de html-le kunnen worden gespeciceerd. Hoogte en breedte kunnen verschillend zijn. Als de <PARAM>-tags ontbreken of kleiner zijn dan 3 moet het bord 6x6 velden bevatten. De beginstand staat altijd in het midden van een bord, of (bij een oneven aantal rijen of kolommen) vlakbij het midden. Met de knop nieuw spel kan de gebruiker op elk moment een nieuw spel starten. Door het indrukken van de help-knop geeft het programma aan op welke velden een zet gedaan mag worden. Nogmaals indrukken van de help-knop schakelt deze functie weer uit. Door met de muis op een veld van het bord te klikken kan de gebruiker een zet doen voor de partij die aan de beurt is. Het moet natuurlijk wel een toegestane zet zijn, anders gebeurt er niets. (De twee spelers zullen dus om de beurt de muis moeten bedienen. Je hoeft niet tegen de computer te kunnen spelen). De afbeeldingen hieronder tonen het scherm aan het begin, en na een tijdje spelen en het aanschakelen van de help-functie. De layout van het scherm hoeft niet hetzelfde te zijn als in deze afbeeldingen, als de genoemde elementen maar aanwezig zijn.

Hints Geef namen aan de constanten die je arbitrair kiest, zoals de diameter van de stenen. Beperk het aantal object-variabelen tot een minimum: naast de constanten en de buttons zijn alleen variabelen nodig die de momentele toestand van het spel beschrijven. Maak methoden (met parameters!) voor spel-specieke vragen en handelingen, zoals is een zet op een bepaald veld voor een bepaalde kleur legaal? en verander de door een bepaalde zet in een bepaalde richting ingesloten stenen van kleur. Bedenk dat er in acht richtingen ingesloten kan worden: alle combinaties van 1, 0 en +1 in de x- en y -richting behalve (0,0). Vermijd het om de acht richtingen allemaal apart te behandelen: als er dan een foutje in zit, moet je die acht keer herstellen, en bovendien maakt het het programma onoverzichtelijk. Prober de regelmaat liever in een methode met handig gekozen parameters te vangen. Als je niet weet hoe te beginnen: teken eerst een (exibel ingevuld) bord, voeg dan de interactie toe maar nog zonder controle van legaliteit en veranderen van kleur van ingesloten stenen, maak dan de help-functie (daarvoor heb je een methode voor controle op legaliteit nodig, die je daarna ook mooi kunt gebruiken voor het controleren van de zetten), enz.

258

Programmeerprojecten

C.3

SchetsPlus

Anders dan de vorige opdrachten gaat het dit keer om de uitbreiding van een bestaand programma. Je hoeft dus niet from sratch te beginnen, maar kunt het programma Schets uit hoofdstuk 13.4 als uitgangspunt gebruiken. Dat bestaat uit 22 Java-les, waarin 18 klassen, 3 abstracte klassen en een interface worden gedenieerd. Sommige van die klassen kun je onveranderd laten. In sommige klassen zul je methoden moeten toevoegen, of bestaande methoden veranderen (extra opdrachten en/of extra parameters). Ook zul je waarschijnlijk nog enkele klassen moeten toevoegen. Om het voor anderen mogelijk te maken snel een overzicht te krijgen van de uitbreidingen die je hebt gedaan, moet je bij het programma ook een overzicht van de gedane wijzigingen maken. Dat is een tekstle, waarin je opsomt in welke methoden van welke klassen er iets is veranderd, waar zinvol met een korte motivatie waarom deze verandering noodzakelijk was. De toelichting mag een Word- (.doc), PDF- of HTML-bestand zijn. De le die je inlevert moet een ZIP-le zijn, met daarin alle Java-bestanden (de ongewijzigde en de gewijzigde en de toegevoegde) en de toelichtings-tekst. De class-les en project-les hoeven niet te worden ingeleverd. Beoordelingsnormen Bij de beoordeling van het programma wordt natuurlijk gelet op correcte werking. Maar daarnaast wordt ook gelet op de stijl van het programma. In het bijzonder gaat het ditmaal om het aanhouden van het once and only once principe, dus het vermijden van het kopieren en plakken van stukken code. Het gebruik van methoden met geschikte parameters, en het gebruik van overerving in subklassen (inheritance) kunnen daarbij helpen. Ook het zinvol inzetten van klassen uit de Javabibliotheek (de Collection-hierarchie, File-I/O enz.) kan leiden tot het vermijden van dubbel werk en draagt bij aan de duidelijkheid van het programma; dit wordt dus positief gewaardeerd. Bovendien geldt het als fraai als variabelen waar mogelijk lokaal worden gedeclareerd en/of als parameter doorgegeven aan methoden, zodat de declaraties bovenin de klasse beperkt blijven tot variabelen die inderdaad essentieel zijn voor de toestand van het object. Tenslotte wordt ook de duidelijkheid van de toelichtings-tekst wordt ook de beoordeling betrokken. 1. Cirkels Voeg twee tools toe voor het tekenen van een open en een gevulde ovaal. De nieuwe tools zijn natuurlijk zowel via de toolbox, als via het Tool-menu beschikbaar. Hint: laat je inspireren door de manier waarop de rechthoeken in het bestaande programma worden aangepakt. 2. Het nieuwe gummen De gum-tool werkt in het basisprogramma door eigenlijk door het tekenen van een dikke witte lijn over de bestaande tekening heen, waardoor het lijkt alsof die verdwijnt. We gaan het gedrag van de gum nu veranderen, zo dat de gebruiker een getekend element (bijvoorbeeld een rechthoek) in n keer kan uitwissen, door het met de nieuwe gum aan te klikken. Niet alleen verdwijnt zon element dan, maar ook komen andere elementen die geheel of gedeeltelijk door het weggehaalde element werden bedekt weer tevoorschijn. (Met de oude gum was dat niet mogelijk: als je daarmee een rechthoek wegveegde, bleef er slechts een witte vlek over). Het is niet nodig om een nieuwe icon te maken; je kunt simpelweg de functionaliteit van de gum veranderen. De oude manier van gummen komt gewoon te vervallen. Aanpak: Vanwege de mogelijkheid dat bedekte elementen weer tevoorschijn kunnen komen, is het noodzakelijk om ook onzichtbare (want bedekte) elementen nog te onthouden. Met een bitmap, zoals in het basisprogramma, is dat niet mogelijk: weg is weg. In theorie is het mogelijk om bij elk getekend element een kopie van de oude bitmap te maken, althans van het bedekte gedeelte. Dat vreet echter wel erg veel geheugen, en het is dan ook niet de aanpak die hier wordt gevraagd. Een betere aanpak is om de hele tekening, behalve als bitmap, ook nog te bewaren in de vorm van een lijst van getekende elementen. De objecten in die lijst vormen een compacte beschrijving van elk getekend element (soort, beginpunt, eindpunt, kleur, eventuele tekst). Bij elke teken-aktie wordt de lijst uitgebreid. De bitmap uit het basisprogramma mag ook blijven, en voldoet prima voor de meeste teken-akties. De nieuwe gum-aktie werkt echter door het te verwijderen element uit de lijst te halen, en aan de hand van die lijst de hele bitmap te reconstrueren, door alle overgebleven elementen in de lijst opnieuw te tekenen. Vrijheid: Wat je precies als elementen beschouwt, die met e en klik kunnen worden uitgewist, mag je zelf bedenken. Het ligt voor de hand dat een rechthoek en een ovaal in zijn geheel als e en

C.3 SchetsPlus

259

element te beschouwen. Maar of je een met de pen-tool gemaakte kromme lijn als e en element, of als allemaal losse lijntjes beschouwt, mag je zelf kiezen. Hetzelfde geldt voor een tekst: is dat in zijn geheel e en element, of is elke aparte letter een apart element? (Zet de gemaakte keuze in de toelichtings-tekst!) Hints: Het model van de tekening zal moeten worden uitgebreid: naast de al bestaande bitmap komt de elementen-lijst erbij, met een methode die de bitmap uit de elementenlijst kan reconstrueren. Het is ook niet raar als de methode die een guur tekent ingrijpend aangepast moet worden aan het aldus gewijzigde model. Wanneer heeft een gebruiker raak geklikt op een element van de tekening? Bij een gevulde rechthoek is dat duidelijk. Voor de overige elementen kun je in eerste instantie het programma uittesten door alles binnen de bounding box van zon guur als raak te beschouwen. Maar dat kan natuurlijk ook subtieler. Bij de gevulde cirkel kun je met een Pythagoras-achtige formule zorgen dat daadwerkelijk het gekleurde deel van de cirkel aangeklikt moet worden. En bij een open rechthoek? Het hele binnengebied als raak beschouwen is wel erg royaal. Maar om precies de rand te raken moet de gebruiker wel erg goed mikken. Je zou ervoor kunnen zorgen dat een zone van -zeg- vijf pixels rond de rand als raak telt. Licht hoe dan ook de gemaakte keuze toe in de toelichtings-tekst. Wanneer is er raak geklikt op een schuine lijn? Nou, bijvoorbeeld als de afstand van het geklikte punt tot de lijn niet te groot is. Hoe bepaal je de afstand van een punt tot een lijn? Tik distance point line in op Google, en je bent al snel op een pagina die de formule daarvoor geeft. Laat je niet afschrikken door de wiskunde eromheen: de benodigde formule staat er gewoon tussen. Het zou handig zijn als er een methode is die bepaalt of een punt raak is voor een bepaald type element. Die kun je in eerste instantie simpel houden, om snel te kunnen testen of de rest van het programma werkt; daarna kun je de raak-methode naar believen subtieler maken. 3. Opslaan en weer inlezen van de tekening Als de gebruiker een ingewikkelde tekening heeft gemaakt, wil hij daar misschien later weer mee verder werken. Daartoe is het nodig dat de tekening als le kan worden bewaard, en later weer kan worden ingelezen. Voeg een menu File aan de menubar toe, met daarin items Open en Save. Kiest een gebruiker zon item, dan verschijnt een dialoog waarmee een lenaam kan worden uitgekozen, en die le wordt vervolgens gelezen of geschreven. Dit hoeft natuurlijk alleen te werken in de Application-versie van het programma, want in een los Applet kun je geen les benaderen. Het is niet de bedoeling om de tekening als bitmap weg te schrijven. In de weer ingelezen tekening zou dan immers niet meer gegumd kunnen worden. Beter (en ook makkelijker) is het om de elementen-lijst naar de le te schrijven, en die bij het inlezen van de le weer te reconstrueren. Hint: De le mag een gewone tekst-le zijn, bijvoorbeeld met voor elk element een regel. De vorm van de le mag je helemaal zelf bepalen: enige vereiste is dat je eigen programma een geschreven le weer kan inlezen. Je hoeft je dus niet te conformeren aan bestaande standaarden. (Een voorbeeld kunje vinden in de manier waarop Steden en Wegen in een le worden opgeslagen in het Zoeknetwerk-programma in hoofdstuk 14.1). Overige toevoegingen 1. In het control-panel van het basisprogramma is een nog niet werkende knop Rotate aanwezig, die de tekening een kwart slag naar rechts draait. Die doet echter nog niets. Maak hem werkend! 2. Voeg een Undo-knop toe aan het control-panel en/of een menu. Daarmee kan de gebruiker het laatst getekende element weer weghalen, en door herhaaldelijk gebruik de hele tekening weer langzaam afbreken. Misschien kun je zelfs ook nog een Redo-knop maken,die de weggehaalde elementen toch weer toevoegt. Hint: In de bitmap-representatie is dit vrijwel onmogelijk, maar met behulp van de elementen-lijst, die voor onderdeel 2 sowieso nodig is, is het tamelijk eenvoudig. 3. Maak naast de kleur-control ook een control waarmee de gebruiker de lijndikte kan instellen.

260

Programmeerprojecten (En als je dan ook nog kleur Wit aan de kleur-control toevoegt, hebben we de mogelijkheid van de oude gum terug...)

4. Als de tekening af is, wil de gebruiker die waarschijnlijk in een andere programma gebruiken. Het fantasie-formaat dat we voor Save en Open (onderdeel 3) gebruiken is daarvoor natuurlijk niet geschikt. Maak daarom een extra menu-item, waarmee de tekening in een bestaand bitmap-formaat kan worden bewaard. Dit is eenrichtingsverkeer: zon bitmap hoeft in SchetsPlus niet meer ingelezen te kunnen worden; in die tekening zou dan immers de elementen-lijst representatie ontbreken, waardoor gummen-nieuwe-stijl onmogelijk zou zijn. Zoek zelf op Internet een geschikte Java-package die de gewenste bitmap-formaten ondersteunt; je hoeft dat niet allemaal zelf te maken. 5. Maak een extra tool waarmee je een aangeklikt element bovenop, of juist onderop de stapel van getekende elementen legt. Een half-bedekte rechthoek kun je daarmee dus weer helemaal zichtbaar maken. 6. Maak een extra tool waarmee je een element kunt aanklikken, waarna je het door te draggen naar een andere plaats kunt slepen. Uiteraard kunnen daarbij achterliggende elementen verschijnen of juist bedekt worden. 7. Als je het window groter of kleiner maakt, reageert het basisprogramma door de bitmap van de tekening groter of kleiner te maken. De getekende elementen blijven echter even groot. Je zou het echter ook zo kunnen maken, dat de getekende elementen meegroeien, respectievelijk krimpen, met de bitmap.

261

Bijlage D

Standaardklassen en -methoden
D.1 package java.lang
() (Object ander) () (String s) (String s) (String s) () (int van) (int van, int tot) (String s) (int pos) (char c) () (int n) () (String s) (double d) () (String s) (int x) class Object String toString boolean equals int hashCode class String boolean equals boolean equalsIgnoreCase int compareTo int length String substring String substring String concat char charAt int indexOf String toUpperCase class Integer c Integer int intValue static int parseInt class Double c Double double doubleValue static double parseDouble class System static void exit static InputStream in static PrintStream out class Math static double pi, e static int abs static double abs static double sqrt, log, exp static double sin, cos, tan static double pow static int min, max static double random static int round class Thread c Thread void start static void sleep interface Runnable void run

(int n) (double d) (double d) (double d) (double grondtal, double exponent) (int a, int b) () (double d) (Runnable r) () (long millisecondes) ()

262

Standaardklassen en -methoden

D.2

package java.util
(String tosplit) () () () (String sep) () (E x) () () () () () (E e) (Collection ? c) (E e) (Collection ? extends E c) (Object o) (Collection ? c) () (int index, E element) (int index) (int index, E element) (int index) (Object o) ()

class Scanner implements Iterator c Scanner String next boolean hasNext int nextInt Scanner useDelimiter class Stack E c Stack void push E pop boolean empty interface Collection E int size boolean isEmpty void clear boolean contains boolean containsAll boolean add boolean addAll boolean remove boolean removeAll Iterator E iterator interface List E extends Collection E void add E get E set E remove int indexOf ListIterator E listIterator interface Set E extends Collection E interface SortedSet E extends Set E E rst E last interface Map K ,V int size boolean isEmpty void clear boolean containsKey boolean containsValue V get V put void putAll V remove Collection V values Set K keySet interface SortedMap K , V extends Map K , K rstKey K lastKey interface Iterator E boolean hasNext E next void remove interface ListIterator E extends Iterator E boolean hasPrevious E previous

() () () () () (Object key) (Object value) (K key) (K key, V value) (Map ? extends K , ? extends V m) (Object key) () () V () () () () () () ()

D.2 package java.util

263

Object

Abstract Collection

Abstract List

Linked List Array List

Collection

List

Abstract Set Sorted Set Abstract Map Sorted Map

Hash Set Tree Set Hash Map Tree Map

Set

Map List Iterator

Iterator

java.util extends

interface

implements

abstract class

class

interface Comparator T int compare class LinkedList E implements List E c LinkedList c LinkedList E getFirst E getLast void addFirst void addLast E removeFirst E removeLast class ArrayList E implements List E c ArrayList c ArrayList c ArrayList void ensureCapacity class HashSet E implements Set E c HashSet c HashSet class TreeSet E implements SortedSet E c TreeSet c TreeSet c TreeSet class HashMap K , V implements Map K , V c HashMap c HashMap class TreeMap K , V implements SortedMap c TreeMap c TreeMap c TreeMap

(T x1, T x2) () (Collection ? extends E c) () () (E e) (E e) () () () (int startcapaciteit) (Collection ? extends E c) (int capaciteit) () (Collection ? extends E c) () (Comparator ? super E c) (SortedSet E s) () (Map K , V m) () (Comparator ? super K c) (SortedMap K , ? extends V m)

264

Standaardklassen en -methoden

D.3

package java.awt

class Graphics void drawString (String s, int x, int y) void drawLine (int x1, int y1, int x2, int y2) void drawRect (int x, int y, int b, int h) void drawOval (int x, int y, int b, int h) void drawArc (int x, int y, int b, int h, int start, int boog) void drawImage (Image im, int x, int y, ImageObserver io) void llRect (int x, int y, int b, int h) void llOval (int x, int y, int b, int h) void llArc (int x, int y, int b, int h, int start, int boog) void setColor (Color c) void setFont (Font f) class Graphics2D extends Graphics void setStroke (Stroke s) void setTransform (AneTransform t) void setPaint (Paint p) void setRenderingHint (Key key, Object value) void draw (Shape s) void ll (Shape s) class Color c Color (int r, int g, int b) c Color (int r, int g, int b, int alpha) c Color (int rgb) int getRed, getGreen () int getBlue, getRGB () Color brighter, darker () static Color white, gray, black, dark gray, light gray static Color red, green, blue, yellow, magenta, cyan, orange, pink class Font c Font (String name, int style, int size) static int bold, italic, plain; // styles class Dimension c Dimension (int w, int h) int width, height int getWidth, getHeight () class BasicStroke implements Stroke c BasicStroke (oat dikte) c BasicStroke (oat dikte, int cap, int join) c BasicStroke (oat d, int c, int j, int m, oat[ ]d, oat f) static int cap butt, cap round, cap square // cap styles static int join bevel, join miter, join round // join styles class GradientPaint implements Paint c GradientPaint (oat, oat, Color, oat, oat, Color) class TexturePaint implements Paint c TexturePaint (BueredImage im, Rectangle2D rect) class Toolkit static Toolkit getDefaultToolkit () Image getImage (String naam) abstract class Image Graphics getGraphics () class BueredImage extends Image c BueredImage (int b, int h, int type) static int type int rgb, type int argb, type byte gray,. . . ; // types void setRGB (int x, int y, int rgb) int getRGB (int x, int y)

D.3 package java.awt class MenuBar c MenuBar () class Menu c Menu (String s) void add (MenuItem m) void addSeparator () class MenuItem c MenuItem (String s) void addActionListener (ActionListener a) interface LayoutManager class BorderLayout implements LayoutManager c BorderLayout () static String north,south,east,west,center; // add-richtingen class FlowLayout implements LayoutManager c FlowLayout () class GridLayout implements LayoutManager c GridLayout (int rijen, int kolommen, int dx, int dy) abstract class Component void addMouseListener (MouseListener m) void addMouseMotionListener (MouseMotionListener m) void addKeyListener (KeyListener k) void setVisible (boolean b) void setSize (int b, int h) void setBounds (int x, int y, int b, int h) void setBackground (Color c) void setFont (Font f) Dimension getSize () int getWidth, getHeight () void paint, update (Graphics g) void repaint () Graphics Graphics () void requestFocus () class Container extends Component void add (Component c) void add (Component c, Object richting) void setLayout (LayoutManager m) class Panel extends Container c Panel () class Applet extends Panel void init () String getParameter (String naam) Image getImage (URL base, String naam) class Window extends Container void addWindowListener (WindowListener w) class Frame extends Window c Frame () c Frame (String titel) void setMenuBar (MenuBar b) void setTitle (String titel) class Dialog extends Window class FileDialog extends Dialog c FileDialog (Frame parent, String title, int typ) static int load, save // types String getFile () String getDirectory ()

265

266

Standaardklassen en -methoden

Component

java.awt java.applet javax.swing


Panel Applet JApplet Abstract Button JMenu Item JToggle Button

Container

Window

Frame

JFrame

JMenu JCheck Box JRadio Button

Dialog

JDialog

JWindow

nog 20 meer
JComboBox JLabel JToolTip JMenuBar JPanel JToolBar Box JScrollBar JSlider

JButton

JComponent CheckBox Choice List Button Canvas Label Scrollbar Text Component

TextArea JTable TextField JList JText Area JText Field JEditor Pane JFormatted TextField JPassword Field JText Pane

Menu Component

MenuBar MenuItem Menu

JTree JText Component Abstract Action Image Icon

java.awt.event

Action Listener

Action Icon

class Button extends Component c Button (String s) void setLabel (String s) String getLabel () void addActionListener (ActionListener a) class Canvas extends Component c Canvas () class Label extends Component c Label (String s) c Label (String s, int alignment) static int left, center, right; // alignments void setText (String s) class Scrollbar extends Component c Scrollbar (int hv, int val, int step, int min, int max) static int horizontal, vertical; // waardes voor hv void addAdjustmentListener (AdjustmentListener a) void setValue (int x) int getValue () class TextComponent extends Component String getText () void setText (String s) void setEditable (boolean b) class TextField extends TextComponent c TextField (int posities) c TextField (String s, int posities) void setEchoChar (char c) void addActionListener (ActionListener a) class TextArea extends TextComponent c TextArea (int regels, int kolommen) void append (String s)

D.4 package javax.swing

267

D.4

package javax.swing

class JApplet extends Applet c JApplet () Container getContentPane () class JFrame extends Frame c JFrame () c JFrame (String titel) Container getContentPane () abstract class JComponent extends Container abstract class AbstractButton extends JComponent void setText (String s) void setIcon (Icon ic) void addActionListener (ActionListener a) class JButton extends AbstractButton c JButton (String s) c JButton (Icon ic) c JButton (String s, Icon ic) class JMenuItem extends AbstractButton c JMenuItem (String s) c JMenuItem (Icon ic) c JMenuItem (String s, Icon ic) class JMenu extends JMenuItem c JMenu (String s) JMenuItem add (JMenuItem i) JMenuItem add (String s) JMenuItem add (Action a) class JMenuBar extends JComponent c JMenuBar () JMenu add (JMenu m) class JComboBox extends JComponent c JComboBox () void addItem (Object o) void addItemListener (ItemListener i) class JToolBar extends JComponent c JToolBar (int orientation) JButton add (Action a) void addSeparator () class JSlider extends JComponent c JSlider (int orientation, int min, int max, int value) void setMajorTickSpacing (int n) void setMinorTickSpacing (int n) interface Action extends ActionListener void putValue (String key, Object value) Object getValue (String key) static String default, name, long description, small icon // keys abstract class AbstractAction implements Action c AbstractAction (String naam) c AbstractAction (String naam, Icon icon) interface Icon int getIconWidth () int getIconHeight () void paintIcon (Component c, Graphics g, int x, int y) class ImageIcon implements Icon c ImageIcon (Image im) c ImageIcon (String lenaam) c ImageIcon (URL location)

268

Standaardklassen en -methoden

D.5

package java.awt.event

interface EventListener interface ActionListener extends EventListener void actionPerformed (ActionEvent e) interface AdjustmentListener extends EventListener void adjustmentValueChanged (AdjustmentEvent e) interface WindowListener extends EventListener void windowClosing (WindowEvent e) void windowOpened (WindowEvent e) void windowClosed (WindowEvent e) void windowActivated (WindowEvent e) void windowDeactivated (WindowEvent e) void windowIconied (WindowEvent e) void windowDeiconied (WindowEvent e) interface MouseListener extends EventListener void mousePressed (MouseEvent e) void mouseReleased (MouseEvent e) void mouseClicked (MouseEvent e) void mouseEntered (MouseEvent e) void mouseExited (MouseEvent e) interface MouseMotionListener extends EventListener void mouseMoved (MouseEvent e) void mouseDragged (MouseEvent e) interface KeyListener extends EventListener void keyPressed (KeyEvent e) void keyReleased (KeyEvent e) void keyTyped (KeyEvent e) class EventObject Object getSource () abstract class AWTEvent extends EventObject class ActionEvent extends AWTEvent class AdjustmentEvent extends AWTEvent int getValue () int getAdjustmentType () static int unit increment, block increment, track,. . . ; // types class ComponentEvent extends AWTEvent class InputEvent extends ComponentEvent int getModiers () static int button1 mask, button2 mask, button3 mask // modiers static int alt mask, ctrl mask, shift mask // modiers class KeyEvent extends InputEvent char getKeyChar () int getKeyCode () static int vk left,vk right,vk up,vk down,vk home,vk end, static int vk page up,vk page down,vk f1,. . . ,vk f24,. . . ; // keycodes class MouseEvent extends InputEvent int getX () int getY ()

D.6

package java.net

class URL c URL (String specicatie) c URL (String protocol, String host, String le) URLConnection openConnection () class URLConnection InputStream getInputStream ()

D.7 package java.io

269

D.7

package java.io

class File c File (String padnaam) c File (String parent, String child) c File (File parent, String child) static String pathSeparator () static char pathSeparatorChar () String getName () File getParentFile () boolean exists () boolean isFile, isDirectory () void delete () long length () File [ ] listFiles, listRoots () class RandomAccessFile c RandomAccessFile (File f, String mode) int read (byte [ ] b) void write (byte [ ] b) long getFilePointer () void seek (long pos) abstract class InputStream int read () int read (byte [ ] b) long skip (long aantal) void close () abstract class OutputStream void write (int b) void write (byte [ ] b) void close () class FileInputStream extends InputStream c FileInputStream (String naam) c FileInputStream (File f) class ByteArrayInputStream extends InputStream c ByteArrayInputStream (byte[ ] b) class FilterInputStream extends InputStream class BueredInputStream extends FilterInputStream c BueredInputStream (InputStream is) class DataInputStream extends FilterInputStream c DataInputStream (InputStream is) int readInt () double readDouble () class FileOutputStream extends OutputStream c FileOutputStream (String naam) c FileOutputStream (File f) class ByteArrayOutputStream extends OutputStream c ByteArrayOutputStream () byte [ ] toByteArray () class FilterOutputStream extends OutputStream class BueredOutputStream extends FilterOutputStream c BueredOutputStream(OutputStream os) class DataOutputStream extends FilterOutputStream c DataOutputStream (OutputStream os) void writeInt (int n) void writeDouble (double d) class PrintStream extends FilterOutputStream void print, println (Object o)

270

Standaardklassen en -methoden

abstract class Reader int read () int read (char [ ] c) abstract class Writer void write (int c) void write (char [ ] c) void write (String s) void close () class InputStreamReader extends Reader c InputStreamReader (InputStream is) c InputStreamReader (InputStream is, String encoding) class FileReader extends InputStreamReader c FileReader (String naam) c FileReader (File f) class StringReader extends Reader c StringReader (String s) class BueredReader extends Reader c BueredReader (Reader r) String readLine () class LineNumberReader extends BueredReader c LineNumberReader (Reader r) int getLineNumber () class FilterReader extends Reader class PushbackReader extends FilterReader c PushbackReader (Reader r) void unread (int c) class OutputStreamWriter extends Writer c OutputStreamWriter (OutputStream os) class FileWriter extends OutputStreamWriter c FileWriter (String naam) c FileWriter (File f) class StringWriter extends Writer c StringWriter () String toString () class BueredWriter extends Writer c BueredWriter (Writer w) class FilterWriter extends Writer class PrintWriter extends Writer c PrintWriter (Writer w) c PrintWriter (OutputStream os) void print (Object o) void print (primitieftype x) void println (Object o) void println (primitieftype x)

D.8

hoofdprogramma
static void main (String [ ] commandlineparameters)

D.9 primitieve types

271

Object

java.io Stream Byte Stream


Input Stream

Direct InputStream

File InputStream ByteArray InputStream

File / String

Random AccessFile Filter InputStream

byte [ ]

Buffered InputStream Data InputStream

InputStream

InputStream

File Output Stream

Direct OutputStream

File OutputStream ByteArray OutputStream

File / String

Filter OutputStream

Buffered OutputStream Data OutputStream PrintStream

OutputStream

OutputStream

Character Stream

Reader

Direct Reader

InputStream Reader

InputStream File Reader File / String

String Reader

String

Indirect Reader

Buffered Reader

Reader LineNumber Reader Reader Reader

Filter Reader Writer

PushBack Reader

Direct Writer

OutputStream Writer

OutputStream File Writer File / String

String Writer

Indirect Writer

Buffered Writer Filter Writer Print Writer

Writer

abstract class

fantasie klasse

class

parameter v.d. constructor

Writer / OutputStream

D.9

primitieve types
bevat niets waarheid unicodesymbool geheel getal geheel getal geheel getal geheel getal oating point oating point default false (char)0 0 0 0 0 0.0 0.0 grootte 0 bits 1 bit 16 bits 8 bits 16 bits 32 bits 64 bits 32 bits 64 bits bereik false, true (char)0 .. (char)65535 128 . . . 127 32768 . . . 32767 2147483648 . . . 2147483647 9223372036854775808 . . . 9223372036854775807 1.4 1045 . . . 3.4028235 10+38 4.9 10324 . . . 1.7976931348623157 10+308

type void boolean char byte short int long oat double

272

Bijlage E

Operatoren
prio 14 14 14 13 13 13 13 12 12 12 11 11 11 10 10 10 9 9 9 9 8 8 7 6 5 4 3 2 1 1 operator () [] . ++ -~ ! * / % + + << >> >>> < > <= >= == != & ^ | && || ? : = op= positie post post in pre/post pre/post pre pre in in in in in in in in in in in in in in in in in in in in mix in in betekenis methode-aanroep array-selectie component-selectie increment decrement bitgewijze niet logische niet vermenigvuldigen delen rest bij deling optellen strings samenvoegen aftrekken bits naar links schuiven bits naar rechts schuiven idem, opvullen met nullen kleiner dan groter dan kleiner of gelijk groter of gelijk gelijk ongelijk bitsgewijze en bitsgewijze exclusieve of bitsgewijze of logische en logische of als - dan - anders toekenning uitrekenen en toekennen zie sectie/blz. 2.5 19 11.2 130 2.5 19 7.3 80 7.3 80 12.6 164 7.2 79 3.3 31 3.3 31 3.3 31 3.3 31 5.2 55 3.3 31 12.6 164 12.6 164 7.2 7.2 7.2 7.2 7.2 7.2 12.6 12.6 12.6 7.2 7.2 3.2 7.3 78 78 78 78 78 78 164 164 164 79 79 28 80

273

Bijlage F

Gereserveerde woorden
soort opdrachten keyword assert break continue do for if return synchronized switch throw try while case catch default else nally boolean byte char double oat int long short void
1*16+. . .

zie sectie/blz. 12.2 148

soort speciale waarden

7.3 8.1 4.3 12.2 8.4 7.1 12.2 8.4 12.2 8.1 7.2 5.1 11.1 4.3 5.1 3.2 5.1 5.1 2.4

80 87 42 147 98 77 147 98 147 87 79 51 127 41 51 30 51 51 18 programmastructuur

onderdelen van opdrachten

elementaire types

modicatie van klassen en methoden

keyword false instanceof new null super this true class enum extends implements import interface package throws abstract nal native private protected public static strictfp transient volatile

zie sectie/blz. 7.2 79 5.3 9.3 10.1 4.1 7.2 2.3 2.3 6.5 2.8 6.5 14.3 13.1 3.5 9.2 2.3 5.3 58 111 121 39 79 15 16 74 21 74 234 171 34 107 16 58

0*16+. . . . . . +0 . . . +1 . . . +2 . . . +3 . . . +4 . . . +5 . . . +6 . . . +7 . . . +8 . . . +9 . . . +10 . . . +11 . . . +12 . . . +13 . . . +14 . . . +15

2*16+. . .

3*16+. . .

4*16+. . .

5*16+. . .

6*16+. . .

7*16+. . .

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

backsp tab newline

return

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

esc

32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47

spatie ! " # $ % & ( ) * + , . /

48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

0 1 2 3 4 5 6 7 8 9 : ; < = > ?

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

@ A B C D E F G H I J K L M N O

80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95

P Q R S T U V W X Y Z [ \ ] ^ _

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111

a b c d e f g h i j k l m n o

112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127

p q r s t u v w x y z { | } ~

274

Bijlage G

Syntax
source file
package klasse

import

naam

naam
*

module

package

package

naam

module
abstract final
klasse

header
public

{ public

member

interface

methode

header
klasse

header

header
implements class

naam

naam
extends

naam

interface

header
interface

naam
extends

naam
,

methode

header
throws

type

type
void

naam

par-decl
,

member
private
protected

static

final

methode

header
public abstract

blok

declaratie
methode

header

Syntax
blok
final

275

declaratie
{ }

opdracht

declaratie type naam


[] , = ;

initialisatie

par-decl type naam


[]

initialisatie expressie
{

initialisatie
,

opdracht label naam


=

expressie
.

; ( ) )

expressie
while if ( (

naam

expressie
,

expressie expressie expr


, ;

opdracht opdracht
else

opdracht opdracht

for

expr

expr
,

declaratie type naam : expr


do

opdracht

while

( (

expressie expressie

) )

switch synchronized

blok

blok
try

blok

catch ( par-decl

blok
finally blok

break continue return throw assert

naam expressie expressie


:

expressie

label naam
case

expressie
:

default

276
expressie constante
prefix-operator

Syntax

expressie expressie
? ( ( type )

infix-operator postfix-operator

expressie
: )

expressie expressie expressie


.

expressie

expressie
this super new

naam
(

expressie
, [

type

expressie type
klasse

expressie ]

<

type-par

>

naam
boolean int double char byte short long float

[]

type-par
super ? extends &

type

type
,

constante getal
true false null symbool

symbool
\ n t \ u r b

hex-cijfer oct-cijfer

symbool cijfer letter leesteken getal


0x

hex-cijfer cijfer
. E L + F D

cijfer

cijfer

277

Index
*, 31 *=, 83 +, 31, 68 ++, 80 +=, 80 -, 31 /, 31 <, 79 <=, 79 =, 28 ==, 79, 90 >, 79 >=, 79 %, 31 &&, 79 ||, 79 !=, 79 aanhalingsteken, 60, 128 enkel, 127 abs, 68 abstract (keyword), 171, 172 abstracte klasse, 171 AbstractList (class), 176 Action (interface), 184 ActionListener (interface), 74, 169 actionPerformed, 74, 85 adapter, 172 add, 70, 72, 143 addActionListener, 73 addAdjustmentListener, 73 addSeparator, 150 addWindowListener, 151 adjustmentValueChanged, 74 afronding, 32 aftrekken, 31 animatie, 109 append, 126 Applet (class), 65, 118 applet, 14 versus application, 148 application, 14, 148 array, 130 als parameter, 134 tweedimensionaal, 163 van objecten, 138 vs. collection, 173 vs. String, 139 ArrayList (class), 173 ascii, 128 assembler, 11 assignment, 28 AudioClip (class), 117 awt, 70, 123 vs. Swing, 181 backslash, 128, 205 BasicStroke (class), 185 Boolean, 78 boolean (type), 79 BorderLayout (class), 143 boxing, 181 break (keyword), 148 broncode, 11 BufferedImage (class), 117, 163 BufferedInputStream (class), 209 BufferedReader (class), 209 Button (class), 70, 117 byte (type), 51 byte stream, 207 ByteArrayInputStream (class), 208 ByteArrayOutputStream (class), 209 bytecode, 12 Canvas (class), 104, 118 case (keyword), 148 cast, 55, 129 catch (keyword), 98 char (type), 127 char33 , 79 character stream, 207 charAt, 117, 127 CheckBox (class), 117 class (keyword), 16 clone, 124 Collection (interface), 176 collection, 173 Color (class), 26, 61 commentaar, 32 Comparable, 180 compiler, 11 computer, 5 concat, 117, 127 console I/O, 214 constante, 59 constructormethode, 58, 105

278 conversie van type, 54 cyaan, 26 DataInputStream (class), 208 declaratie, 29, 50 default (keyword), 148 delen, 31 delete, 117 double (type), 41, 51 drawLine, 25 drawOval, 25 drawRect, 25 drawString, 25 else (keyword), 87 empty, 224 equals, 90, 117, 127 erven, 65, 119 event, 73, 124 event-listener, 73, 123 exception, 98 exists, 117, 205 exit, 151 exp, 68 expressie, 31 extends (keyword), 16, 65, 119, 122 faculteit, 78 false, 79 File (class), 117, 205 FileInputStream (class), 208, 210 FileReader (class), 210 FileWriter (class), 210 fillRect, 25 final (keyword), 34, 122 first, 180 float (type), 51 FlowLayout (class), 142 Font (class), 63 for (keyword), 80, 173, 178 Frame (class), 149 functioneel programmeren, 8 getGraphics, 117 getInputStream, 234 getName, 117 getParameter, 65 getParentFile, 117 getRGB, 117, 164 getText, 85, 126 getValue, 72 getX, 117 Graphics (class), 20, 25, 118 Graphics2D (class), 185 GregorianCalendar (class), 117 grep, 214 GridLayout (class), 142 GUI, 76 licht- en zwaargewicht, 181 HashSet (class), 177 hasNext, 118 herdenitie, 120, 235 herhalen, 77 genest, 83 met een teller, 78 nul keer, 82 oneindig vaak, 82 hi erarchie, 122 if (keyword), 87, 147 imperatief programmeren, 8 implements (keyword), 74, 169 import (keyword), 21 in, 214, 216 init, 70, 72 InputStream (class), 207 InputStreamReader (class), 209 int (type), 30, 51 Integer (class), 68, 181 interface, 76 vs. klasse, 168 interface (keyword), 74 interpreter, 11 isDirectory, 205 isFile, 205 Iterator (interface), 178 JApplet (class), 184 Java, 10 Javascript, 14 JButton (class), 184 JFrame (class), 184 JPanel (class), 184 klasse, 7, 53, 101, 116 abstracte, 171 hi erarchie, 122 Java, 15 vs. interface, 168 klasse-denitie, 16 klasse-header, 16 Label (class), 117 last, 180 layout, 33 layoutmanager, 142 length, 117, 127, 131 LinkedList (class), 174 List (interface), 174 listFiles, 205 log, 68 logische operatoren, 79 long (type), 51

INDEX

INDEX magenta, 26 main, 148, 214 Map (interface), 180 Math (class), 58, 68 membervariabelen, 73, 123 Menu (class), 150 MenuBar (class), 150 MenuItem (class), 150, 165 methode, 7, 56 aanroep, 19 denitie, 36 header, 17 herdenitie, 120 Java, 15 module, 16 new (keyword), 58, 72, 105, 130 newline-character, 128 next, 61, 118 nextInt, 118 null (keyword), 111, 216 NullPointerException (class), 98 NumberFormatException (class), 98 object, 6, 49, 101, 116 object-geori enteerd programmeren, 8 objectverwijzing, 52, 101 opdracht, 5 methode-aanroep, 19 toekenning, 28 openConnection, 234 operator *, 31 *=, 83 +, 31, 68 ++, 80 +=, 80 -, 31 /, 31 <, 79 <=, 79 =, 28 ==, 79, 90 >, 79 >=, 79 %, 31 &&, 79 ||, 79 !=, 79 char33 , 79 ophogen, 80 optellen, 31 out, 214 OutputStream (class), 207 overerven, 65, 119 package, 21 paint, 15, 69, 184 Panel (class), 144 paradigma, 8 parameter, 18, 36 parseInt, 68 Point (class), 117 polymore, 121 pop, 224 pow, 68 println, 210 PrintStream (class), 216 PrintWriter (class), 210, 217 prioriteit, 32 processor, 5 programma, 5 Java, 15 programmeertaal, 6 programmeren functioneel, 8 imperatief, 8 object-geori enteerd, 8 public (keyword), 16 push, 224 random, 61, 63 random access I/O, 205 read, 207 readDouble, 209 Reader (class), 207, 209 readLine, 209 readLine, 216 repaint, 69 resultaat, 56 return (keyword), 42, 56 run, 110 Runnable (interface), 110 Scanner (class), 61, 118 Scrollbar (class), 70, 117 Set (interface), 178 setColor, 25 setEditable, 126 setFont, 63 setLabel, 70 setLayout, 142 setLocation, 117 setLookandFeel, 182 setRGB, 117, 164 setStroke, 185 setText, 126 setValue, 72 short (type), 51 sin, 68 sleep, 110 SortedSet (interface), 180 source code, 11 sqrt, 68

279

280 Stack (class), 224 start, 109 static (keyword), 34, 58, 68 stream-I/O, 205 String (class), 65, 117, 127 subklasse, 119 substring, 117, 127 super (keyword), 121, 235 Swing, 184 switch (keyword), 147 syntax, 16 System (class), 214 tab-character, 128 TextArea (class), 117, 126 TextField (class), 83, 117 this (keyword), 39 Thread (class), 109 toekenningopdracht, 28 toString, 124 true, 79 try (keyword), 98 type, 18 typeconversie, 54 unboxing, 181 unicode, 128 URL (class), 234 useDelimiter, 118 variabele, 6, 28 variabelenaam, 20 vermenigvuldigen, 31 verwijzing, 52 void (keyword), 18 while (keyword), 77 WindowAdapter (class), 172 windowClosing, 151 WindowEvent (class), 151 WindowListener (interface), 151, 172 write, 208 Writer (class), 207, 209

INDEX