0.1 Eenvoudige klassen Het opdelen van software is in de geschiedenis vaak de motivatie geweest voor de evolutie van programmeertalen. Een eerste stap werd gezet met talen zoals Algol en Fortran, die toelaten om monolithische code gestructureerd op te delen in verschillende routines. In de programmeertaal Ada werden packages ingevoerd als structureringstechniek. Het wordt nu mogelijk om gerelateerde routines en datastructuren samen te nemen in één verpakking. Modula en CLU zijn talen die hierop verder bouwen. Deze talen worden objectgebaseerd genoemd. In objectgerichte talen zoals Java, Smalltalk, C++ en Eiffel, wordt code opgedeeld in klassen. Een klasse beschrijft de methodes, die op ieder van haar objecten kan toegepast worden. In dit hoofdstuk worden een aantal elementaire aspecten in de definitie van een klasse overlopen. Als voorbeeld wordt een specificatie en een implementatie van een klasse van bankrekeningen uitgewerkt in Java. In het voorbeeld beperken we ons initieel tot één enkele karakteristiek: de balans van een bankrekening. In de volgende delen krijgen bankrekeningen bijkomende karakteristieken, waarmee andere aspecten in de ontwikkeling van klassen kunnen geïllustreerd worden. Zo zal in het tweede deel van deze tekst de klasse van bankrekeningen uitgebreid worden met een titularis, met een bankkaart, met volmachthouders en met spaarrekeningen. De klassen die in dit hoofdstuk aan bod komen, zijn eenvoudig in twee opzichten: 1. In geen enkele methode komt een complex algoritme aan bod om het gewenste effect te realiseren. Het ontwikkelen van complexe algoritmes is een onderwerp dat buiten dit boek valt. Bepaalde aspecten ervan worden besproken bij het ontwikkelen van iteratieve algoritmes in het tweede deel. 2. In de interne voorstelling van de objecten komen geen complexe gegevensstructuren aan bod. Een partiële studie van meer complexe gegevensstructuren zoals rijen en vectoren, volgt eveneens later in het boek. De opbouw van de klasse van bankrekeningen verloopt in een aantal stappen, die typisch zijn in de ontwikkeling van eender welke klasse. 1. We introduceren een aantal methodes om de balans van een bankrekening te initialiseren, te manipuleren en te inspecteren. In dit hoofdstuk zullen deze bewerkingen
enkel informeel gespecificeerd worden. Deze stap wordt uitvoerig besproken in hoofdstuk 0.1.1, “Specificatie”. 2. We kiezen een geschikte representatie voor de gegevens die in een object moeten bijgehouden worden, in casu de balans van een bankrekening. Verder voorzien we elke methode die in de eerste stap geïntroduceerd werd van een implementatie. Deze stap wordt behandeld in hoofdstukken 0.1.3, “Representatie” en 0.1.4, “Implementatie van methodes”. 3. In een laatste stap zal ingegaan worden op een systematische aanpak voor het uittesten van een klasse. Diverse strategieën zullen daarbij in de loop van het boek van nabij bekeken worden. Het systematisch testen van code is nhet onderwerp van hoofdstuk 0.1.5, “Verificatie”. Zodra de specificatie van een klasse voltooid is, kan ze gebruikt worden in de definitie van andere klassen. Daarom zullen we meteen na de specificatie van de klasse van bankrekeningen tonen hoe methodes kunnen toegepast worden op objecten van een klasse. Het gebruik van een klasse op basis van haar specificatie wordt besproken in hoofdstuk 0.1.2, “Het toepassen van methodes”.
0.1.1 Specificatie De specificatie van een klasse richt zich tot potentiële gebruikers ervan, terwijl de eigenlijke code zich eerder richt op de machine zelf. Mensen kunnen slecht omgaan met grote hoeveelheden gedetailleerde informatie, en het is net dat wat een computer nodig heeft om te weten wat hij moet doen. Specificaties van klassen zullen niet alle details laten zien, maar enkel datgene wat een gebruiker van de klasse moet weten om ermee te kunnen werken. Een goede specificatie zal zo beperkt mogelijk in omvang zijn, terwijl ze toch volledig moet zijn. Dit spanningsveld is één van de grootste uitdagingen bij het ontwerpen van software. De specificatie van een klasse omvat typisch de beschrijving van een aantal methodes die kunnen toegepast worden op objecten van de klasse. Vaak maakt men een onderscheid tussen diverse soorten methodes. In de context van objectgerichte programmeertalen is het zinvol een onderscheid te maken tussen constructoren, mutatoren, inspectoren en terminatoren, hoewel een dergelijke verdeling niet expliciet ondersteund wordt in de meeste objectgerichte programmeertalen. Terminatoren worden pas besproken in hoofdstuk XXXX op pagina X en XXXX op pagina X. Naast methodes die toegepast kunnen worden op objecten van de klasse, kunnen ook k l a s s e n m e t h o d e s ingevoerd worden.
Klassenmethodes worden eerder toegepast op de klasse zelf, dan op individuele objecten ervan. Java biedt slechts een beperkte ondersteuning op het vlak van de specificatie van klassen. Zo beperkt de hoofding van een klasse zich grotendeels tot het invoeren van een geschikte naam. In de hoofding van iedere methode wordt eveneens een geschikte naam ingevoerd. Daarenboven worden in de hoofding van iedere methode de types van de argumenten en van het resultaat omschreven. De hoofding van een methode wordt ook vaak haar signatuur genoemd. Java biedt geen instrumenten om de semantiek van een methode te omschrijven. De taal biedt met andere woorden geen concepten om het nominale effect van een methode te omschrijven, noch om aan te geven onder welke omstandigheden dit nominaal effect gegarandeerd wordt. Bij gebrek aan beter worden deze aspecten genoteerd in commentaar, die de vertaler negeert. Eiffel is een objectgerichte programmeertaal die meer ondersteuning biedt op het vlak van de specificatie van klassen. Zo biedt de taal expliciete concepten om precondities, postcondities en klasseninvarianten neer te schrijven. De omschrijving van de semantiek van een methode zal in dit hoofdstuk beperkt blijven tot informele beschrijvingen. In hoofdstuk Error! Reference source not found. zal een meer systematische aanpak worden voorgesteld, waarin het informele karakter hoe dan ook behouden blijft. Oplossingen voor de verificatie van de specificatieaspecten die niet door de taal ondersteund worden, worden besproken in Error! Reference source not found. en Error! Reference source not found..
0.1.1.1 Syntaxis van specificatie, commentaar en javadoc Op de hoofdingen van klassen en methodes na, biedt Java zelf geen ondersteuning voor specificatie. De taal Java richt zich in hoofdzaak naar de computer, en niet naar de menselijke gebruiker. Vermits Java weinig of geen concepten biedt op het vlak van de specificatie, zullen specificaties in commentaar geschreven worden. In Java-code wordt alles dat omsloten is door /* … */ genegeerd door de computer. Wat je daar tussen schrijft heeft dus een menselijk publiek. Een tweede syntaxis voor commentaar is // …: alles wat // volgt tot het einde van de lijn, is commentaar. Elke programmeertaal gaat anders om met specificatie. Sommige programmeertalen leiden tot een volledige scheiding tussen implementatie en specificatie. Andere talen splitsen het geheel op via andere criteria, die min of meer gelijklopen met de scheiding tussen implementatie en specificatie, en nog andere talen ondersteunen specificatie rechtstreeks met taalconcepten.
In Java worden aspecten van specificatie en implementatie geïntegreerd in één bestand. Dit vereenvoudigt de taak van de ontwikkelaar. Het is immers veel makkelijker dingen te veranderen als ze bij elkaar staan. Wanneer ze los van elkaar staan, groeien specificatie en implementatie vaak uit elkaar. Dit mondt onherroepelijk uit in een situatie waarin ze niet meer gesynchroniseerd zijn, en dus onbruikbaar. Dat specificatie en implementatie bij elkaar staan is een voordeel voor de ontwikkelaar, maar een nadeel voor de gebruiker van de code. Die moet nu immers de tekst die voor hem bedoeld is, zeven uit het volledige bestand. Een goede programmeeromgeving zal een werktuig ter beschikking stellen om uit de volledige beschrijving van een klasse het specificatiegedeelte af te zonderen. Als je met Java werkt, kan je javadoc hiervoor gebruiken. Dit is een programma dat commentaar die tussen /** … */ staat, uit Javacode isoleert en omzet in een HTML-bestand. Met behulp van deze technieken kunnen we in een Java-omgeving verschil maken tussen implementatiecommentaar (omsloten door /* … */ of beginnend met //) en documentatiecommentaar (omsloten door /** … */). De specificatie van een klasse zal geschreven worden in documentatiecommentaar. javadoc javadoc is zo geconcipieerd dat je documentatiecommentaar schrijft bij elementen van de taal. De documentatiecommentaar wordt geschreven vlak voor het element dat hij documenteert. Je kan documentatiecommentaar schrijven voor een klasse, een methode en een variabele (zie paragraaf 0.1.1.3, 0.1.1.4, 0.1.1.5, 0.1.3.1 en 0.1.3.2). In het gegenereerde HTML-bestand zie je dan de signatuur van het besproken element, en daaronder de commentaar die je voor dat element geschreven hebt. Ook worden nog een boel indexen en hyperlinks gegenereerd. De documentatiecommentaar wordt geplaatst tussen /** … */. Verder laat men elke lijn binnen het commentaarblok beginnen met *. De gestandaardiseerde structuur voor documentatiecommentaar wordt gepresenteerd in Voorbeeld 1. /** * Dit is een blok commentaar die zal geïnterpreteerd worden door * javadoc. Zoals je ziet begint elke lijn met een ‘*’. Dat teken * heeft enkel een betere leesbaarheid in het broncodebestand tot * doel, en wordt door javadoc genegeerd. * Je kan structuur aanbrengen in je documentatiecommentaar door * labels te gebruiken: * * @param het ‘param’ label wordt gebruikt om een parameter van * een methode te documenteren * … */ Voorbeeld 1: structuur van documentatiecommentaar
De sterretjes aan het begin van een lijn worden door javadoc genegeerd, en zijn ook niet strikt noodzakelijk. De bedoeling van het formaat is om de commentaar ook in het codebestand zo leesbaar en herkenbaar mogelijk te maken. De documentatiecommentaar die je schrijft kan nog verder gestructureerd worden. Daartoe kan je lijnen in je commentaar beginnen met een label, dat begint met het symbool @ (zie Voorbeeld 1). @param wordt bijvoorbeeld gebruikt om een parameter van een methode te documenteren. javadoc herkent deze labels, en geeft ze een speciale formattering in het gegenereerde HTML-bestand. In hoofdstuk Error! Reference source not found. zal blijken dat we in dit boek een strikte structuur aanbrengen voor specificaties, en spijtig genoeg wordt die niet voldoende ondersteund door javadoc. Er bestaan alternatieven voor javadoc, maar geen enkel hulpmiddel dat wij kennen voldoet. In afwachting gebruiken we in dit boek extra labels die niet door javadoc ondersteund worden.
0.1.1.2
De definitie van een klasse
De definitie van een klasse omvat een hoofding gevolgd door een lichaam. De hoofding van een klasse in Java begint met het sleutelwoord class, gevolgd door de naam van de klasse. Het lichaam van een klasse wordt omsloten door {…}. Het lichaam van een klasse bevat onder andere alle methodes die kunnen toegepast worden op objecten van de klasse, inclusief de constructoren. Vóór de hoofding van de klasse zal typisch documentatiecommentaar geplaatst worden die algemene informatie geeft omtrent de klasse in zijn geheel. Dit wordt geïllustreerd in Voorbeeld 2 hieronder voor de klasse van bankrekeningen. /** * A class for dealing with bank accounts, involving the available * amount of money as their only characteristic. * This characteristic is called the balance of an account. */ public class Account { º } Voorbeeld 2: de hoofding van een klasse van bankrekeningen
In Java zijn namen in het algemeen, en dus ook klassennamen, case sensitive. Dit betekent dat het onderscheid tussen kleine en grote letters betekenisvol is. Iedere i d e n t i f i c a t o r in Java is een sequentie van hoofdletters, kleine letters, cijfers, 1
onderlijningstekens en dollartekens, die niet begint met een cijfer . 1
In principe kan je in Java broncode de volledige Unicode (http://www.unicode.org) karakterset
Om de leesbaarheid van een programma te verhogen is het gebruikelijk een consistente stijl te hanteren voor het benoemen van klassen. In de meeste Java-documenten wordt de zogenaamde titelstijl gebruikt voor klassennamen: ieder woord in de naam van een klasse begint met een hoofdletter; opeenvolgende woorden worden aan mekaar geschreven. Zo zal een klasse voor tennisspelers bijvoorbeeld de naam TennisPlayer krijgen in Java. Andere stijlen zullen dan gebruikt worden om andere elementen uit Java-programma's, zoals variabelen en methodes, te benoemen. Zo kunnen bij het lezen van een programma de ingrediënten ervan onmiddellijk herkend worden aan de manier waarop ze benoemd worden. Naast symbolische constanten, die volledig in hoofdletters worden geschreven, zullen klassennamen de enige identificatoren zijn die met een hoofdletter beginnen. Het sleutelwoord public voor de klasse wordt behandeld in hoofdstuk Error! Reference source not found.. Voorlopig kan je het beschouwen als “verplicht”.
0.1.1.3
Instantiatiemethodes: mutatoren en inspectoren
Instantiatiemethodes zijn methodes die toepasbaar zijn op bestaande objecten van de klasse, ook instantiaties genoemd. Hun specificatie en implementatie gebeurt op het niveau van de klasse waartoe de betrokken objecten behoren. Binnen instantiatiemethodes onderscheiden we twee hoofdsoorten: mutatoren en inspectoren. Een mutator zal in principe de toestand van het betrokken object veranderen. Indien nodig, kan een mutator ook de toestand van andere objecten veranderen waarmee het betrokken object op één of andere manier gerelateerd is, of die als bijkomende argumenten worden meegegeven. Zo zal een mutator om geld over te schrijven van een rekening naar een geassocieerde spaarrekening, niet alleen de balans van de rekening veranderen, maar ook die van de geassocieerde spaarrekening. Deze laatste zal op één of andere manier gerelateerd zijn tot de betrokken rekening. Relaties tussen objecten worden behandeld in het tweede deel van deze tekst. Een mutator om geld over te schrijven van een rekening naar een andere rekening zal op analoge wijze de balans van beide rekeningen veranderen. In dit geval, zal één van beide rekeningen als bijkomend argument worden meegegeven in de toepassing van de mutator. In uitzonderlijke gevallen zal een mutator de toestand van het betrokken object onveranderd laten. In dergelijke gevallen stuurt het betrokken object de toestandsverandering van andere objecten. Voorbeelden van dergelijke mutatoren volgen in het derde deel van de tekst omtrent overerving. gebruiken, en dus karakters als ç, é en ñ in identificatoren gebruiken. In praktijk beperk je je echter best tot ASCII symbolen, omdat de meeste ontwikkelingsomgevingen (nog) geen Unicode aankunnen.
Een mutator zal typisch geen resultaat teruggeven, en vertoont daarom sterke gelijkenissen met een procedure uit imperatieve talen zoals Pascal. Een inspector zal informatie geven omtrent de toestand van het betrokken object. Daarenboven kan het teruggegeven resultaat verder bepaald worden door andere objecten, die gerelateerd zijn met het betrokken object, of die als bijkomende argumenten worden meegegeven. Zo zal een inspector die de afstand berekent tussen twee punten in een vlak de toestand van beide punten inspecteren. De inspector wordt toegepast op één van beide punten, en het andere punt wordt meegegeven als bijkomend argument. Een inspector zal de toestand van alle objecten ongewijzigd laten, en heeft om die reden erg veel gemeen met een functie uit programmeertalen zoals Pascal. Java biedt geen expliciete ondersteuning voor het onderscheiden van mutatoren en inspectoren. De classificatie mutator — inspector is dan ook niet hard. De taal laat bijvoorbeeld toe om methodes te schrijven die zowel de toestand van betrokken objecten verandert als een resultaat teruggeeft. Op zeer speciale uitzonderingen na, raden we het combineren van mutatie en inspectie echter ten stelligste af. Het blijkt dat klassen met zulke methodes veel moeilijker te begrijpen zijn. Anderzijds bestaat er nog veel meer nomenclatuur die methodes opdeelt in soorten en ondersoorten. Hierop gaan we in dit boek echter niet verder in, op een occasionele vermelding na waar zulk een methode toevallig aan bod komt. De definitie van een methode in Java omvat enerzijds een hoofding, en anderzijds een lichaam. De hoofding wordt vaak de signatuur van de methode genoemd. De hoofding van iedere mutator of inspector omvat, naast de naam, een specificatie van een lijst van bijkomende argumenten en een specificatie van het resultaat. De signatuur zal verder aangevuld worden met een documentatiecommentaar, waarin de betekenis van de methode wordt toegelicht. Zoals voor klassen, wordt het lichaam van een methode wordt omsloten door {…}. Het lichaam van een methode bevat de instructies die moeten uitgevoerd worden telkens de methode wordt toegepast op een object. Voorbeeld 3 illustreert de definitie van mutatoren en inspectoren voor de klasse van bankrekeningen. Dit voorbeeld wordt hieronder verder toegelicht, samen met een aantal bijkomende aspecten omtrent de definitie van mutatoren en inspectoren. De betekenis van het sleutelwoord public vóór elke methodedefinitie wordt besproken in paragraaf Error! Reference source not found., “Error! Reference source not found.”, Error! Reference source not found.. public class Account { /** * Deposit
to this account.
*/ public void deposit(long amount) … /** * Withdraw from this account. */ public void withdraw(long amount) … /** * Transfer from this account to <destination>. */ public void transferTo(long amount, Account destination) … /** * Return the balance of this account. */ public long getBalance() … } Voorbeeld 3: definitie van mutatoren en inspectoren voor de klasse van bankrekeningen
0.1.1.3.1
De naam van een instantiatiemethode
De naam van een mutator of een inspector is een identificator, zoals een klassennaam (zie pagina 5). Zoals voor klassen, zal ook voor het benoemen van methodes een consistente stijl gebruikt worden. Meer in het bijzonder zullen de woorden in de identificator voor een mutator of voor een inspector aan mekaar geschreven worden, waarbij het eerste woord begint met een kleine letter, en volgende woorden beginnen met een hoofdletter. Zo wordt in transferTo het eerste woord transfer volledig in kleine letters geschreven, terwijl het tweede woord To begint met een hoofdletter. Het is niet de gewoonte om woorden in een identificator te scheiden met een onderlijningsteken (_), zoals vroeger gebeurde in talen die geen onderscheid maken tussen grote en kleine letters. In ieder geval moet de naam van een mutator of van een inspector verschillen van de naam van de klasse waartoe de instantiatiemethode behoort. Namen van mutatoren en inspectoren binnen dezelfde klasse moeten niet noodzakelijk onderling verschillen. Java ondersteunt het overladen (overloading) van identificatoren. Binnen een bepaalde context kan een zelfde identificator gebruikt worden als naam voor verschillende methodes, op voorwaarde dat hun signaturen onderling verschillen. Zo is het perfect mogelijk een tweede mutator transferTo in te voeren in de klasse van bankrekeningen, op voorwaarde dat de signatuur verschilt van de mutator uit Voorbeeld 3. Dit kan bijvoorbeeld als de bestemming van het over te hevelen bedrag niet een andere bankrekening is, maar een bankkluis of een spaarrekening. Concreet worden de signaturen van twee methodes als verschillend beschouwd indien ze verschillen in het aantal formele
argumenten, of indien er minstens één overeenkomstig argument bestaat, waarvoor het type verschillend is. Er bestaat een afspraak over de naamgeving van eenvoudige mutatoren en inspectoren die we aanraden, maar die niet door de taal wordt verplicht. Eenvoudige inspectoren, die zich beperken tot het opvragen van een eigenschap, hebben als naam getEigenschap, bijvoorbeeld getBalance. Een uitzondering zijn inspectoren die een booleaans resultaat geven: die krijgen de naam is Eigenschap of hasEigenschap, bijvoorbeeld isAdult of hasChildren voor een persoon. Voor eenvoudige mutatoren die simpelweg een waarde toekennen aan een eigenschap wordt set Eigenschap gebruikt, bijvoorbeeld setWidth voor een rechthoek in een tekenprogramma. Deze naamafspraak wordt ook toegepast voor booleaanse eigenschappen (bijvoorbeeld setPermitted(true)). Mutatoren die meer doen dan een waarde aan een eigenschap toekennen krijgen een niet-gestandaardiseerde, toepasselijke naam (bijvoorbeeld transferTo). Deze naamgevingregels zijn afkomstig van JavaBeans, de componenttechnologie die Java complementeert. Ze worden echter ook buiten die context meer en meer toegepast.
0.1.1.3.2
Specificatie van het resultaat van een instantiatiemethode
Het type van het resultaat van een mutator of van een inspector wordt geplaatst vóór de naam van de betrokken methode. Hier zal het onderscheid tussen beide soorten methodes duidelijk worden. — Een mutator veroorzaakt enkel een toestandsverandering in de betrokken objecten, zonder een expliciet resultaat terug te geven. Voor een methode in Java die geen resultaat teruggeeft wordt void gebruikt als type voor het resultaat. De methodes withdraw, deposit en transferTo in Voorbeeld 3 zijn mutatoren. — Een inspector geeft informatie terug omtrent de toestand van de betrokken objecten. Deze informatie kan een waarde zijn van één van de ingebouwde types in Java of een object van een klasse. De methode getBalance uit Voorbeeld 3 is een inspector, die als resultaat de balans van de betrokken rekening zal teruggeven als een waarde van het primitieve type long. — Een primitief type definieert een verzameling primitieve waarden, samen met een aantal bewerkingen om die waarden te manipuleren. Primitieve types worden in hoofdstuk 0.1.3.5 meer in detail bestudeerd. Voor het ogenblik volstaat het te weten dat het type long gehele getallen definieert met bewerkingen zoals optelling, aftrekking en vermenigvuldiging.
Het is sterk aan te raden de scheiding tussen mutatoren en inspectoren strikt aan te houden. Een klasse bevat met andere woorden best geen methode die tegelijkertijd de toestand van het betrokken object of van andere objecten verandert, en informatie omtrent die nieuwe toestand teruggeeft.
0.1.1.3.3
Het impliciete argument voor een instantiatiemethode
Iedere instantiatiemethode zal een impliciet argument hebben van de klasse waartoe de methode behoort. Zo hebben in Voorbeeld 3 de mutatoren withdraw, deposit en t r a n s f e r T o en de inspector g e t B a l a n c e allen een impliciet argument dat overeenstemt met de bankrekening waarop de methode wordt toegepast. Het is het spilobject in de toepassing van de methode. In de documentatiecommentaar van een methode zal het impliciete argument omschreven worden als this ClassName. Zo wordt in de documentatie van de mutator withdraw uit Voorbeeld 3 aangegeven dat het gegeven bedrag zal afgehaald worden van deze bankrekening. Hiermee wordt de bankrekening bedoeld waarop de mutator wordt toegepast. 2
Het bestaan van een impliciet argument is uniek voor objectgerichte talen . Dit concept heeft als gevolg dat de plaats van methodes binnen een geheel van klassen niet volledig vrij is. Een mutator dient gedefinieerd te worden in de klasse van het object dat hij verandert. Indien een mutator consequent de toestand van het impliciete argument ongewijzigd zou laten, is er wellicht een ander object waarop de mutator op een meer zinvolle manier kan toegepast worden. Om analoge redenen zal een inspector minstens de toestand van het impliciete argument gebruiken in het bepalen van de informatie die zal teruggeven worden. Voor primitieve methodes zoals withdraw, deposit en getBalance is deze regel voldoende om te bepalen in welke klasse ze moeten gedefinieerd worden, omdat er geen andere objecten in betrokken zijn. Voor meer complexe methodes, waarin verscheidene objecten betrokken zijn, blijft er een subjectieve marge voor de keuze van de klasse waar de methode zal worden geïntroduceerd. Beschouw als voorbeeld een methode die weergeeft dat een bepaalde wagen wordt aangekocht door een bepaalde persoon. Deze methode kan worden ondergebracht in de klasse van personen, met een persoon als spilobject en met een wagen als bijkomend argument, maar ze kan ook worden gedefinieerd in de klasse van de wagens, met een wagen als spilobject en met een persoon als bijkomend argument. Het introduceren van beide mutatoren is eveneens een oplossing. Of dit aangewezen is of niet is een bron van discussie. Enerzijds zal een gebruiker makkelijker een methode vinden die hij nodig heeft. Anderzijds moeten de ontwikkelaars ervoor zorgen dat, wanneer de software 2
Het concept van impliciet argument komt enkel voor in objectgerichte talen, maar er bestaan ook objectgerichte talen die het concept niet ondersteunen.
evolueert, de twee methodes consistent blijven, en wordt de gebruiker geconfronteerd met een keuze, wat per definitie tijd en moeite kost. Merk op dat de mutator transferTo uit Voorbeeld 3 een speciaal geval is waarin twee objecten van dezelfde klasse betrokken zijn. Er is dan ook geen twijfel dat deze methode geïntroduceerd moet worden in de klasse van de bankrekeningen. De keuze welke van de twee objecten de rol van het direct betrokken object inneemt, is arbitrair. Naast een methode om geld over te schrijven naar een andere rekening, had ook een methode transferFrom kunnen gedefinieerd worden, waarmee geld getransfereerd wordt naar de rekening die als impliciet argument fungeert.
0.1.1.3.4
Specificatie van expliciete argumenten voor een instantiatiemethode
Naast het direct betrokken object, zullen in het algemene geval andere objecten betrokken worden in de toepassing van een methode, en zal daarenboven bijkomende informatie worden meegegeven in de vorm van primitieve waarden. In de context van bijkomende argumenten spreken we van formele argumenten en actuele argumenten. Een formeel argument is de abstracte voorstelling van het argument in de signatuur van de methode en in de code die de methode implementeert. Het is een speciaal soort variabele. Een actueel argument is een uitdrukking, die geschreven wordt bij toepassing van een methode. Een actueel argument beschrijft de waarde die, tijdens de uitvoering, bij de gegeven toepassing van de methode aan het formele argument zal toegekend worden. Zoals zal blijken in hoofdstuk 0.1.3.4 zijn er twee manieren om actuele waarden door te geven: volgens het ene mechanisme zal een kopie van de actuele waarde worden doorgegeven (waardesemantiek of kopiesemantiek), volgens het andere mechanisme zal een referentie naar de actuele waarde worden doorgegeven (referentiesemantiek). In Java wordt voor waarden van primitieve types steeds een kopie doorgegeven. Voor objecten van klassen daarentegen wordt steeds een referentie doorgegeven. In Java worden bijkomende argumenten gespecificeerd in een argumentenlijst die volgt op de naam van de methode. Binnen de argumentenlijst, die omsloten wordt door ronde haakjes, worden opeenvolgende specificaties van formele argumenten gescheiden door komma's. De specificatie van ieder formeel argument omvat een naam voor het argument waaronder het binnen de methode gekend zal zijn, en het type ervan. De naam van een formeel argument is een identificator. Binnen deze tekst zal dezelfde conventie gehanteerd worden voor het benoemen van formele argumenten en lokale variabelen als die voor het benoemen van mutatoren en inspectoren. Opeenvolgende
woorden in de naam van een formeel argument of van een lokale variabele zullen aan mekaar geschreven worden; alle woorden, behalve het eerste woord, zullen beginnen met een hoofdletter. In de documentatiecommentaar van een methode zullen namen van expliciete argumenten omgeven worden door < … >. Voor de specificatie van het type van een formeel argument gelden dezelfde regels als voor de specificatie van het type van het resultaat van een inspector. Een formeel argument kan gebonden worden met een actuele waarde van primitieve types die Java aanbiedt, of het kan gebonden worden met een actueel object van een klasse. In het laatste geval zal de naam van de klasse vermeld worden als type. Voor de mutatoren withdraw en deposit uit Voorbeeld 3 is enkel het bedrag nodig als bijkomend argument. Conform met het type van het resultaat voor het opvragen van de balans, worden hiervoor grote gehele getallen (long) gebruikt. Merk op dat het expliciet argument in de documentatiecommentaar bij deze methodes genoteerd wordt als . Het feit dat bedragen doorgegeven worden in de vorm van grote gehele getallen hoeft geenszins te betekenen dat intern in de objecten van de klasse, de balans ook in die vorm wordt bijgehouden. Het is perfect mogelijk om bijvoorbeeld intern te werken met getallen in vlottende kommavoorstelling. Voor de mutator transferTo zijn twee bijkomende argumenten nodig: het over te hevelen bedrag als een groot geheel getal, en de doelrekening als een (ander) object van de klasse van bankrekeningen. Merk op dat bij de specificatie van het type van het tweede argument, de naam van de klasse gebruikt wordt waartoe het object moet behoren. Voor de inspector getBalance, tenslotte, is geen bijkomende informatie nodig. De argumentenlijst zal daarom leeg zijn. Merk op dat de lege argumentenlijst () wel nodig is. Zonder de lege argumentenlijst wordt de beschrijving niet als een geldige hoofding voor een methode geïnterpreteerd.
0.1.1.4
Constructoren
Naast methodes om bestaande objecten van een klasse te muteren en te inspecteren, is er uiteraard ook de nood om nieuwe objecten aan te maken. Hiervoor biedt een klasse een reeks methodes aan, die constructoren genoemd worden. De naam constructor is misleidend, omdat deze methodes zich niet echt bezig houden met het aanmaken van nieuwe objecten. De rol van een constructor beperkt zich tot de initialisatie van een nieuw object. De eigenlijke constructie van een nieuw object gebeurt met een speciale operator (new), die in hoofdstuk 0.1.1.5 uitvoerig aan bod komt.
Constructoren onderscheiden zich van gewone methodes (mutatoren en inspectoren) door het feit dat ze allen benoemd worden met de naam van de klasse waartoe ze behoren. Verder geeft een constructor, zoals een mutator, geen resultaat terug. In tegenstelling tot mutatoren waar void gebruikt wordt om het ontbreken van een resultaat kenbaar te maken, wordt voor constructoren per definitie aangenomen dat de methode geen resultaat zal teruggeven. De hoofding van een constructor zal bijgevolg enkel bestaan uit de naam van de klasse en uit een lijst van bijkomende argumenten. Zoals bij inspectoren en mutatoren kan deze lijst eventueel leeg zijn. Zoals mutatoren en inspectoren hebben constructoren ook een impliciet argument dat overeenkomt met een object van de klasse. Voor constructoren is dit direct betrokken object, het object dat door de constructor moet geïnitialiseerd worden. public class Account { /** * Initialize this new account with as balance. */ public Account(long initial) … /** * Initialize this new account with a zero balance. */ public Account() … º } Voorbeeld 4: definitie van constructoren voor de klasse van bankrekeningen
In Voorbeeld 4 worden twee constructoren gedefinieerd voor de klasse van bankrekeningen. Een eerste constructor zal de balans van een nieuw aangemaakte bankrekening initialiseren met het meegegeven bedrag. Zoals voor mutatoren en inspectoren is het principe van het overladen ook toepasbaar op constructoren. Elke klasse kan bijgevolg een willekeurig aantal constructoren introduceren, op voorwaarde dat hun signatuur onderling verschilt zoals beschreven in hoofdstuk 0.1.1.3. De definitie van de tweede constructor illustreert het overladen van constructoren. De argumentenlijst voor deze constructor is leeg, en de balans van de betrokken bankrekening zal geïnitialiseerd worden op 0.
0.1.1.5
Klassenmethodes
De instantiatiemethodes beschreven in Voorbeeld 3 zijn methodes die toegepast worden op een object, het impliciete argument. Daarnaast ondersteunt Java ook klassenmethodes. Klassenmethodes hebben de klasse zelf als impliciet argument. We spreken ook van de betrokken klasse. Het niet-gekwalificeerde woord methode wordt gebruikt om over
instantiatie- en klassenmethodes samen te spreken, maar wordt ook gebruikt, indien het verschil door de context duidelijk is, als synoniem voor instantiatiemethode. Ook bij klassenmethodes maken we onderscheid tussen mutatoren en inspectoren. De informatie waarop ze werken is dan echter niet de toestand van een bepaald object, maar van de klasse zelf. Er kan in de meeste objectgerichte talen namelijk ook informatie bijgehouden worden op het niveau van de klasse. Over de informatie die op klassenniveau kan bijgehouden worden in Java wordt in meer detail gepraat in hoofdstuk 0.1.3.2, “Klassenvariabelen”. 3
In bepaalde talen, zoals Smalltalk, zijn klassen zelf ook objecten als alle andere . Andere talen, zoals C++, gaan zo ver niet. Klassen ondersteunen bepaalde begrippen, maar lang niet zoveel als objecten. Java is een speciaal geval. De taal volgt principieel de manier van werken van C++. Door reflectiefunctionaliteit, die beschikbaar is via de standaard klassenbibliotheek, kunnen in Java toch een aantal dingen gedaan worden met klassen als objecten. Dit valt echter volledig buiten het bestek van dit boek. Vaak echter worden klassenmethodes gebruikt om een resultaat te berekenen enkel en alleen op basis van expliciete argumenten. Zulke methodes kunnen niet echt “inspectoren” genoemd worden, omdat het impliciete argument van de klassenmethode, de klasse, niet gebruikt wordt. Voorbeelden van zulke methodes zijn de meer complexe wiskundige operaties die worden aangeboden in de klasse java.lang.Math van de standaard Javaklassenbibliotheek. Vooral omdat elementen van primitieve types in Java geen objecten zijn, worden deze methodes in Java aangeboden als klassenmethodes Objecten die als expliciete argumenten bij een toepassing aan de klassenmethode worden doorgegeven, kunnen ook veranderd worden. In het algemeen wordt dit echter als een weinig doorzichtige stijl van werken beschouwd. Meestal is het veel beter om de methode dan als instantiatiemethode in te voeren van de klasse van één van de objecten waarvan de toestand veranderd wordt. De naamgeving, het specificeren van het resultaat en de specificatie van de expliciete argumenten volgen dezelfde regels als bij instantiatiemethodes (zie 0.1.1.3). Klassenmethodes worden in Java onderscheiden van instantiatiemethodes door het sleutelwoord static vooraan in de signatuur. De bank die onze voorbeeldcode gebruikt, wenst de balans van de bankrekeningen te beperken met een minimum. Volgens de bank zijn die limieten algemeen, onafhankelijk van een specifieke rekening, en liggen ze voor eens en voor altijd vast. Daarom is het 3
In Smalltalk zijn klassen instantiaties van een klasse MetaClass, een metaklasse. De metaklasse-klasse MetaClass is een instantiatie van zichzelf.
mogelijk een klassenmethode in te voeren om de ondergrens voor rekeningen terug te 4
geven . Naast een ondergrens, wordt ook een bovengrens ingevoerd. Het bereik van gehele types in Java is immers niet onbeperkt. Opnieuw gaan we er van uit dat de bovengrens voor alle bankrekeningen gelijk is, zodat ook hier een klassenmethode kan ingevoerd worden. Deze uitbreidingen worden uitgewerkt in Voorbeeld 5. De methode getLowerLimit heeft het static-sleutelwoord, en geeft de laagst mogelijke waarde terug voor de balans van eender welke rekening. De statische methode getUpperLimit geeft de hoogst mogelijke waarde terug voor de balans van eender welke rekening. De beschrijving van de klasse zelf is uitgebreid om met de beperking op de balans rekening te houden. In de hoofding van de klasse wordt daartoe algemene informatie gegeven omtrent de balans van een bankrekening en de limieten die er op van toepassing zijn. In hoofdstuk Error! Reference source not found. zullen dergelijke algemene beperkingen geformaliseerd worden als klasseninvarianten. Naast bijkomende informatie in de hoofding van de klasse van bankrekeningen, worden ook de beschrijvingen van de mutatoren en de constructoren uitgebreid. De aanpak, die gevolgd wordt in de definitie van de mutatoren en van de constructoren, wordt verder uitgediept in hoofdstuk 0.1.1.7, “Totale methodes”. /** * A class for dealing with bank accounts, involving the available * amount of money as their only characteristic. * This characteristic is called the balance of an account. * * The lower limit and the upper limit for the balance of accounts * are characteristics of the class itself. The lower limit is * always negative; the upper limit is always positive. * * The balance of a bank account is always greater than or equal * to the lower limit and less than or equal to the upper limit. * Notice that zero is always an acceptable value for the balance * of an account. */ public class Account { /** * Return the lower limit of the balance of accounts. */ static public long getLowerLimit() … /** * Return the upper limit of the balance of accounts. */ static public long getUpperLimit() …
4
In het volledig systeem zullen bankrekeningen gerelateerd (zie Error! Reference source not found. “XXXREF”) zijn met een object dat de bank voorstelt. In dat geval is het te overwegen om de grenzen als instantiatiekarakteristiek van de bank te specificeren, in plaats van als klassenkarakteristiek.
/** * Initialize this new account with a zero balance. */ public Account() … /** * Initialize this new account with as balance, * if it is not smaller than the lower limit and not larger * than the upper limit. */ public Account(long initial) … /** * Deposit to this account, if the given amount is * positive, and if the resulting balance would not be larger * than the upper limit. */ public void deposit(long amount) … /** * Withdraw * positive * than the */ public void
from this account, if the given amount is and if the resulting balance would not be smaller lower limit. withdraw(long amount) …
/** * Transfer from this account to <destination>, if the * given amount is positive, if <destination> is effective, if * the resulting balance of this account would not be smaller than * the lower limit, and if the resulting balance of <destination> * would not be larger than the upper limit. */ public void transferTo(long amount, Account destination) … /** * Return the balance of this account. */ public long getBalance() … } Voorbeeld 5: definitie van klassenmethodes voor de klasse van bankrekening
0.1.1.6
Instantiatiemethodes versus klassenmethodes
In het algemeen is een zekere reservatie ten opzichte van het invoeren van klassenmethodes aangewezen. Het is een bekend fenomeen dat mensen die nog niet goed vertrouwd zijn met de principes van het objectgericht programmeren te veel terugvallen op deze manier van werken, die dicht staat bij het klassieke procedureel programmeren. Dat is spijtig, want de belangrijkste objectgerichte technieken, zoals dynamische binding (zie hoofdstuk Error! Reference source not found., “Error! Reference source not found.”,
op pagina Error! Bookmark not defined.) werken enkel met instantiatiemethodes, en zijn niet van toepassing op klassenmethodes. We zullen dan ook de klassenmethodes die in het voorbeeld hierboven ingevoerd werd ter illustratie, onmiddellijk vervangen door instantiatiemethodes. Het is mogelijk, er zijn verschillende voordelen aan verbonden, en de nadelen verbleken tegenover die voordelen. Het is mogelijk omdat de informatie die beschikbaar is op klassenniveau ook beschikbaar is op objectniveau. Het feit dat de informatie hetzelfde is voor alle objecten van een klasse, verhindert ons niet die informatie te bekomen via een object van die klasse. Er zijn twee grote voordelen. Ten eerste verkrijgen we op deze manier alle faciliteiten die instantiatiemethodes bieden, die we missen bij klassenmethodes. Ten tweede wordt de software stabieler voor toekomstige aanpassingen: indien de bank plots wel de limieten wenst te kunnen variëren per rekening, is dit eenvoudig mogelijk door de definitie van de instantiatiemethodes te veranderen. Het zal duidelijk zijn dat de veranderingen veel ingrijpender zullen zijn indien de informatie initieel enkel op klassenniveau beschikbaar was. Het blijkt trouwens dat variaties in informatie, die initieel algemeen lijkt te zijn, erg vaak voorkomen. Het enige nadeel is dat uitvoering met gebruik van instantiatiemethodes iets trager zal zijn, net vanwege de extra faciliteiten die zulke methodes bieden. In de overgrote meerderheid van de projecten is dit verschil echter irrelevant. De vervanging van de klassenmethodes getLowerLimit en getUpperLimit door instantiatiemethodes met dezelfde naam, wordt doorgevoerd in Voorbeeld 6. De klassendocumentatie wordt aangepast, zodat ze in het midden laat of de limiet een eigenschap is van de objecten of van de klasse. Deze kennis is niet relevant voor de gebruiker van de klasse, en de ambiguïteit laat de implementator een grotere vrijheid. /** * A class for dealing with bank accounts, involving the available * amount of money (the balance of the account), and a lower limit * and an upper limit for the balance. * * The lower limit for the balance of an account is always negative; * the upper limit for its balance is always positive. * * The balance of a bank account is always greater than or equal * to the lower limit for its balance and less than or equal to * the upper limit for its balance. Notice that zero is always an * acceptable value for the balance of an account. */ public class Account { /** * Initialize this new account with a zero balance. The lower * limit and the upper limit for the balance of this new account * are initialized according to the policy of the bank.
*/ public Account() … /** * Initialize this new account with as balance. The * lower limit and the upper limit for the balance of this new * account are initialized according to the policy of the bank. * If is not within this range, the balance of this * new account is initialized to zero. */ public Account(long initial) … /** * Deposit to this account, if the given amount is * positive, and if the resulting balance would not be larger * than the upper limit for the balance of this account. */ public void deposit(long amount) … /** * Withdraw * positive * than the */ public void
from this account, if the given amount is and if the resulting balance would not be smaller lower limit for the balance of this account. withdraw(long amount) …
/** * Transfer from this account to <destination>, if the * given amount is positive, if <destination> is effective, if * the resulting balance of this account would not be smaller than * the lower limit for its balance, and if the resulting balance * of <destination> would not be larger than the upper limit for * its balance. */ public void transferTo(long amount, Account destination) … /** * Return the balance of this account. */ public long getBalance() … /** * Return the lower limit for the balance of this account. */ public long getLowerLimit() … /** * Return the upper limit for the balance of this account. */ public long getUpperLimit() … } Voorbeeld 6: finale versie van de specificatie van de klasse van bankrekeningen
0.1.1.7
Totale methodes
De specificatie van de klasse van bankrekeningen, zoals ze in Voorbeeld 6 werd uitgewerkt, introduceert een stel totale methodes. We bedoelen hiermee dat het resultaat of het effect van elke methode gedefinieerd is voor ieder mogelijk stel actuele argumenten. Naar analogie met totale functies uit de wiskunde, worden dergelijke methodes totale methodes genoemd. In de ontwikkeling van de specificatie van een methode is het niet alleen belangrijk het effect van de methode te omschrijven onder normale omstandigheden. De specificatie van een methode is slechts volledig als ze het gedrag ervan onder abnormale omstandigheden ook beschrijft. Dergelijke abnormale omstandigheden worden vaak ook randgevallen genoemd. Randgevallen zijn typisch omstandigheden waaronder het normale effect van een methode niet kan gerealiseerd worden. De specificatie van de methode zal dan aangeven hoe de methode zich in dergelijke gevallen gedraagt. Naast het schrijven van goede specificaties, is het detecteren van alle mogelijke randgevallen in de specificatie van iedere methode, een andere grote uitdaging bij het ontwikkelen van software. In hoofdstukken XXX en YYY zullen alternatieve manieren besproken worden om op een adequate manier om te gaan met mogelijke randgevallen. In het ene geval zullen methodes partieel blijven en worden uitzonderlijke gevallen behandeld met precondities, terwijl in het andere geval signalen zullen geproduceerd worden in de vorm van uitzonderingen. In de specificatie van de mutator withdraw uit de klasse van bankrekeningen worden twee randgevallen onderscheiden. Op de eerste plaats wordt rekening gehouden met het geval waarin het af te halen bedrag negatief is. Volgens de specificatie zal de toestand van de bankrekening bij een afhaling van een negatief bedrag onveranderd blijven. Omdat dit de balans zou verhogen, wordt de gebruiker in dat geval verplicht gebruik te maken van de mutator deposit. Op de tweede plaats houdt de specificatie van de mutator withdraw rekening met het geval waarin de resulterende balans onder de laagst mogelijk limiet voor de balans zou komen te liggen. Ook in dat geval zal de toestand van de betrokken bankrekening onveranderd blijven. De specificatie van de mutator transferTo is op het vlak van randgevallen een stuk complexer. In het normale geval zal het gegeven bedrag afgehaald worden van de bankrekening waarop de methode wordt toegepast, en gestort worden op de bankrekening die als expliciet argument wordt meegegeven. De methode zal op de eerste plaats geen effect hebben als het over te schrijven bedrag niet positief is. Verder moet de bankrekening waarnaar het geld wordt overgeschreven een effectieve rekening zijn. In hoofdstuk 0.1.3.4, “Semantiek van variabelen”, zal aangegeven worden dat variabelen van een klassentype een referentie bevatten naar een object van de betrokken klassen. De geregistreerde
referentie kan niet effectief (null) zijn. Een overschrijving heeft tenslotte ook geen effect als de balans van de bankrekening waarop de methode wordt toegepast te laag is, of als de balans van de doelrekening te hoog is. De notie van totale methodes heeft uiteraard niet alleen betrekking op mutatoren en constructoren. Ook voor inspectoren kan gestreefd worden naar een definitie, waarin het resultaat onder alle omstandigheden gedefinieerd is. De inspectoren getBalance, getLowerLimit en getUpperLimit voldoen om triviale redenen aan deze vereiste. Voor inspectoren is een totale versie niet altijd aangewezen. Beschouw als voorbeeld de definitie van een inspector die het verschil teruggeeft tussen de bankrekening waarop de inspector wordt toegepast en een andere bankrekening, die als expliciet argument wordt meegegeven. Een totale versie zal ook een resultaat moeten bepalen indien de andere bankrekening niet effectief is. Een zinvolle waarde voor het verschil is in dat geval ver te zoeken. Zo zou de waarde 0 verkeerdelijk suggereren dat de rekening waarop de methode wordt toegepast, dezelfde balans heeft als de null-referentie. De technieken, die in hoofdstukken XXX (Contractueel) en YYY (Defensief) besproken worden, zijn voor de specificatie van deze inspector wellicht meer aangewezen.
0.1.2
Het toepassen van methodes
Zodra de specificatie van een klasse is afgerond, is alle informatie beschikbaar om er gebruik van te maken. De specificatie richt zich immers op de modale gebruikers van de klasse, en introduceert die informatie, en alleen die informatie die nodig is om objecten van de klasse aan te maken, en vervolgens te manipuleren door het toepassen van de gespecificeerde methodes. Uiteraard zal een methode pas effectief bruikbaar zijn zodra de implementatie van de klasse voltooid is. In hoofdstuk 0.1.2.1 wordt de notatie toegelicht, die in Java voorhanden is om objecten aan te maken. Vervolgens wordt in hoofdstuk 0.1.2.2 aangegeven hoe mutatoren en inspectoren kunnen toegepast worden op bestaande objecten en klassen. De notatie voor het toepassen van instantiatiemethodes in Java verschilt van de notatie voor het toepassen van klassenmethodes. In hoofdstuk 0.1.2.3 wordt aangegeven hoe klassenmethodes in Java kunnen toegepast worden. Een programma kan uiteraard geen zinvol werk verrichten zonder communicatie met de eindgebruikers. In hoofdstuk 0.1.2.4 wordt in detail ingegaan op de zogenaamde Application Program Interface. Tenslotte wordt in hoofdstuk 0.1.2.5 uitgelegd hoe de notie van een hoofdprogramma wordt ondersteund in Java.
0.1.2.1
Het aanmaken van nieuwe objecten
Nieuwe objecten van een klasse worden aangemaakt met behulp van de operator new. De evaluatie van de uitdrukking new!ClassName(a1,!a2,!…,!an) verloopt in de volgende stappen: 1. Er wordt geheugen gereserveerd voor het bewaren van de interne toestand van een nieuw object van de betrokken klasse. Hoe de interne toestand kan worden gedefinieerd wordt besproken in hoofdstuk 0.1.3, “Representatie”. Voor het aanmaken van een nieuw object van de klasse van bankrekeningen zal geheugen gereserveerd worden voor het bewaren van de balans van de betrokken rekening. De evaluatie van de uitdrukking zal falen indien geen geheugen meer beschikbaar is. 2. Een constructor, waarvan de formele argumentenlijst overeenstemt met de actuele argumentenlijst uit de constructie-uitdrukking, wordt uitgevoerd. Meer in detail wordt gezocht naar een constructor waarvan ieder van de formele argumenten fi in type overeenstemt met het actuele argument a i. De Java-vertaler zal de uitdrukking niet aanvaarden, indien een dergelijke constructor niet voorhanden is. Bij het aanmaken van een nieuwe bankrekening zal bijgevolg, volgens de definitie van de constructoren in Voorbeeld 6, ofwel een geheel getal, ofwel geen enkel actueel argument worden meegegeven. Voor alle andere uitdrukkingen om bankrekeningen aan te maken zal geen constructor gevonden worden in de definitie van de klasse. Dergelijke constructies zullen bijgevolg tijdens de vertaling van het programma verworpen worden. Merk op dat actuele argumenten omgeven worden door haakjes en gescheiden worden door komma's. 3. Een referentie (verwijzing) naar het nieuwe object wordt teruggeven als resultaat van de uitdrukking. Het manipuleren van referenties naar objecten wordt uitvoerig behandeld in het deel omtrent relaties tussen objecten. Voor het ogenblik volstaat het te weten dat een referentie naar een object kan toegekend worden aan een variabele. Deze procedure zal verfijnd worden in het licht van de declaratie van variabelen in hoofdstuk 0.1.4.3, “Initialisatie van variabelen” en, in het licht van overerving in hoofdstuk Error! Reference source not found., “Error! Reference source not found.”, op pagina Error! Bookmark not defined.. De constructie van nieuwe objecten van een klasse wordt in Voorbeeld 7 geïllustreerd voor bankrekeningen. Het codefragment begint met het declareren van variabelen die kunnen refereren naar objecten van de klasse van bankrekeningen. Vervolgens worden twee bankrekeningen aangemaakt, waarvoor expliciet een initieel bedrag wordt meegegeven. Deze bankrekeningen zullen bijgevolg worden geïnitialiseerd met behulp van de eerste constructor uit Voorbeeld 6. De variabelen myAccount en yourAccount zullen
verwijzen naar de eerste, respectievelijk de tweede bankrekening. Tenslotte wordt een derde bankrekening aangemaakt, zonder opgave van een initieel bedrag. Deze bankrekening zal bijgevolg geïnitialiseerd worden door de argumentenloze constructor uit Voorbeeld 6. De variabele hisAccount zal wijzen naar deze derde rekening. De semantiek van de declaratie van variabelen en van toekenningen, alsook de notatie voor het weergeven van grote gehele getallen (de L achter de getallen) worden in hoofdstuk 0.1.3.4, respectievelijk in hoofdstuk 0.1.3.5 uitvoerig toegelicht. Account myAccount; Account yourAccount; Account hisAccount; myAccount = new Account(1000L); yourAccount = new Account(30000L); hisAccount = new Account(); Voorbeeld 7: constructie van nieuwe bankrekeningen
0.1.2.2
Het toepassen van mutatoren en inspectoren
Van zodra objecten van een klasse zijn aangemaakt, kunnen de aangeboden instantiatiemethodes (mutatoren en inspectoren) erop worden toegepast. In Java, zoals in de meeste objectgerichte programmeertalen, wordt het toepassen van een methode op een bepaald object, geëxpliciteerd door dat object aan te geven voor de betrokken methode. In het meest algemene geval kan het object waarop een methode wordt toegepast berekend worden vanuit eender welke uitdrukking. Typisch zal deze uitdrukking zich beperken tot een variabele waarin een referentie naar het betrokken object bewaard wordt. De uitdrukking om het object te identificeren wordt van de toe te passen methode gescheiden door een punt. Na de naam van de methode volgt dan een lijst van actuele argumenten, die in aantal en in type moeten overeenkomen met de formele argumenten van een methode met de opgegeven naam uit de klasse, waartoe het object behoort. Het toepassen van mutatoren en inspectoren wordt in Voorbeeld 8 geïllustreerd voor objecten van de klasse van bankrekeningen. Eerst wordt het afhalen en het storten van geld op een welbepaalde bankrekening geïllustreerd. Daarna wordt de balans opgevraagd en toegekend aan een variabele voor het bewaren van grote gehele getallen. Merk op dat, in Java, variabelen op eender welk punt in het programma kunnen ingevoerd worden, en meteen kunnen voorzien worden van een initiële waarde. Dit gebeurt in het voorbeeld met myAccount en myBudget. De initialisatie van yourAccount wordt uitgesteld tot later in het voorbeeld. Ook de declaratie (invoering) van deze variabele mag uitgesteld worden tot net voor het punt waar de variabele nodig is. Tenslotte wordt het overschrijven van geld geïllustreerd door de mutator transferTo toe te passen op de bankrekening waarnaar
gerefereerd wordt door de variabele myAccount. In deze toepassing moeten het over te schrijven bedrag en de doelrekening als actuele argumenten worden meegegeven. Account myAccount = new Account(1000L); Account yourAccount; myAccount.deposit(10000L); myAccount.withdraw(5000L); long myBudget = myAccount.getBalance(); yourAccount = new Account(); myAccount.transferTo(myBudget, yourAccount); Voorbeeld 8: het toepassen van mutatoren en inspectoren op bankrekeningen
Merk op dat Java een getypeerde taal is. Voor elke variabele en voor elk formeel argument wordt in de declaratie de klasse aangegeven van de objecten waarnaar kan verwezen worden door de variabele of het formeel argument, of het primitieve type van primitieve waarden die erin kunnen bewaard worden. Zo kunnen de variabelen myAccount en yourAccount in Voorbeeld 8 enkel verwijzen naar objecten van de klasse van bankrekeningen. Om analoge redenen kunnen in de variabele myBudget enkel grote gehele getallen gestockeerd worden. Het spreekt voor zich dat het toepassen van een ongekende methode op een object tijdens de vertaling van het programma zal verworpen worden, zoals het toepassen van een methode marry op een object van de klasse van bankrekeningen. Ook zal er een foutenboodschap gegeven worden, indien de formele argumenten van geen enkele methode in de betrokken klasse in overeenstemming kunnen gebracht worden met de actuele argumenten. Om die reden zal bijvoorbeeld de instructie myAccount.transferTo! (yourAccount,!900) niet aanvaard worden. Tenslotte zal tijdens de uitvoering van het programma een fout gesignaleerd worden, indien de evaluatie van de uitdrukking voor het object waarop een methode wordt toegepast, niet resulteert in een verwijzing naar een effectief object van de klasse.
0.1.2.3
Het toepassen van klassenmethodes
Klassenmethodes worden niet toegepast op objecten, maar op klassen. De oproep ervan wordt geïllustreerd in Voorbeeld 9 met de klassenmethodes uit Voorbeeld 5. In plaats van een object voor de naam van de methode, wordt de klassennaam gebruikt om aan te geven dat de betrokken methode wordt toegepast op de klasse zelf, eerder dan op één van haar objecten. Zoals bij instantiatiemethodes wordt de klassennaam van de klassenmethode gescheiden door een punt.
Toepassing van een klassenmethode op een object, zoals gedemonstreerd met een oproep op anAccount, is syntactisch ook toegelaten in Java. Hier is het object enkel een alias voor de hoofdklasse in de methodeoproep. Het zal duidelijk zijn dat deze notatie verwarrend kan zijn. long bankUpperLimit = Account.getUpperLimit(); Account anAccount = new Account(); long bankLowerLimit = anAccount.getLowerLimit(); Voorbeeld 9: het toepassen van klassenmethodes
0.1.2.4
De standaard Java Application Program Interface
Aan een programmeertaal alleen heb je niet genoeg. Je kan er zeer complexe dingen mee doen, maar niemand zal er iets van merken als je geen manier hebt om resultaten naar buiten te brengen, bijvoorbeeld door ze op het scherm te schrijven of in een bestand op schijf. Klassiek is er hiervoor een minimale ondersteuning in de taal. Uitgebreide ondersteuning behoort tot de taak van het besturingssysteem. Binnen een programma kunnen deze diensten van het besturingssysteem aangesproken worden via de Application Program
Interface (A P I ). De API is de specificatie van de methodes die het
besturingssysteem aanbiedt in een hogere programmeertaal. Een programma in die programmeertaal kan deze API-methodes dan oproepen zoals alle andere methodes. De uitvoering van dergelijke methodes gebeurt echter in het besturingssysteem. De meeste besturingssystemen hebben op dit moment een API in C. Deze is direct bruikbaar in C- en C++-programma's. Werk je echter in een taal waarvoor het besturingssysteem geen API aanbiedt, dan wordt het een stuk complexer om de faciliteiten te gebruiken, die het besturingssysteem aanbiedt. In het algemeen dien je modules te schrijven die de oproepen vertalen tussen de geplogenheden van de ene en de andere hogere programmeertaal. De API’s van de meeste besturingssystemen zijn niet objectgericht. Het is dan ook steeds een uitdaging om de faciliteiten van het besturingssysteem op een propere manier te gebruiken in een objectgericht programma. Er zijn de laatste twee decennia vele pogingen geweest om objectgerichte API’s te ontwikkelen, meestal als schil rond de bestaande API’s. De meeste zijn echter mislukt of doodgebloed, en geen enkel product is doorgebroken op de markt. Noemenswaard zijn de inspanningen van Talingent, een samenwerkingsproject van onder andere IBM en Apple, en OpenStep van NeXT. Talingent is nooit afgewerkt, maar vormt de basis voor delen van de Java-API. OpenStep blijkt het enige geslaagde experiment, maar is niet bekend bij het grote publiek. Een belangrijke reden daarvoor is de gekozen programmeertaal: ObjectiveC, een concurrent van C++.
Een nog groter probleem is de draagbaarheid van programma's. Programmeertalen zijn meestal sterk gestandaardiseerd, maar de API’s van besturingssystemen verschillen als dag en nacht. Wanneer je een programma op verschillende platformen wil gebruiken, moet alle code die gebruik maakt van de ondersteuning van het besturingssysteem, voor elk besturingssysteem apart geschreven en onderhouden worden. Dit beperkt zich niet tot het oproepen van een methode met een andere naam. Om vensters op het scherm te krijgen, bijvoorbeeld, worden compleet verschillende structuren en protocollen gebruikt door verschillende besturingssystemen. Java is de eerste omgeving die al deze problemen met redelijk succes aanpakt. De JavaAPI is een verzameling van klassen die de meeste diensten aanbieden, die besturingssystemen ondersteunen. Deze API is een deel van Java, in tegenstelling tot de klassieke aanpak waar de API een deel is van het besturingssysteem. De Java-API is identiek op alle platformen. De implementatie van de klassen die behoren tot de Java-API, verschilt van platform tot platform, om op alle platformen een gelijkaardig effect te verkrijgen. Er wordt ondersteuning geboden voor invoer en uitvoer via een commandolijn, via een grafische gebruikersinterface of via een webbrowser, voor netwerkconnectiviteit, voor het werken met bestanden op schijf, voor parallellisme, enzovoorts. Daarnaast biedt de standaard Java-API ook nog klassen voor structuren die veel gebruikt worden in een objectgericht programma. Over de kwaliteit van de Java-API kan gediscussieerd worden. De structuur van de klassen en van de methodes is niet steeds optimaal, en voldoet niet altijd aan de normen van goed objectgericht programmeren, zoals die in dit boek worden aangebracht. De implementatie is soms nogal ad hoc, met alle gevolgen inherent hieraan. De grafische gebruikersinterface is op sommige platformen ronduit lelijk, of niet conform de geplogenheden van dat platform. Vooral de documentatie van de API klassen is vaak onvoldoende. De laatste 3 problemen worden verbeterd in de opeenvolgende versies, en perfectie is niet des mensens. Het eerste probleem is echter moeilijker te verbeteren, daar veranderingen in structuur bestaande programma’s kunnen schaden. Ook hier wordt voorzichtig aan gewerkt. Dat er een objectgerichte, draagbare, eenvormige API bestaat, is echter zo belangrijk dat we problemen die er hier en daar zijn, erbij willen nemen. Het feit dat de API objectgericht is maakt het leren ervan veel makkelijker dan met een klassieke procedurele API het geval was. Omdat de API identiek is voor alle mogelijke platformen, moet een bedrijf niet langer een verschillende versie van al haar producten ontwikkelen en onderhouden voor ieder platform, en het moet ook niet voor alle platformen APIspecialisten in huis halen (en houden). Naar onze mening is het bestaan van een eenvormige API oneindig veel belangrijker voor de draagbaarheid van Java dan de virtuele machine.
De Java-API wordt aangeboden in een aantal pakketten. De groepering van klassen in pakketten wordt besproken in hoofdstuk Error! Reference source not found.. De structurering van API-klassen wordt in hoofdstuk Error! Reference source not found. op pagina Error! Bookmark not defined. in meer detail behandeld. Hieronder zullen we de 2 aspecten van de standaard Java-API bespreken die we in de volgende hoofdstukken nodig hebben: de ondersteuning voor invoer en uitvoer via de commandolijn en de ondersteuning voor het werken met tekst. Deze ondersteuning wordt geboden door de klassen System en String, die beiden deel uitmaken van het pakket java.lang. Verder in het boek zullen andere aspecten uit de Java-API besproken worden waar ze relevant zijn. In dit boek wordt niet ingegaan op de grafische gebruikersinterface noch op de netwerkcapaciteiten. Voor een uitgebreide studie van de standaard Java-API verwijzen we naar gespecialiseerde werken.
0.1.2.4.1
De klasse String
Strings zijn sequenties van karakters. Anders dan in andere talen zijn strings echte objecten in Java. De klasse String is één van de voorgedefinieerde klassen in de standaard Java-klassenbibliotheek. Objecten van deze klasse zijn niet wijzigbaar: de klasse biedt geen mutatoren aan. De methodes in de klasse kunnen gebruikt worden om strings te vergelijken, te doorzoeken, en nieuwe strings aan te maken op basis van bestaande strings. Java biedt ook een klasse aan met wijzigbare strings, StringBuffer. Java biedt een speciale notatie biedt om strings in broncode te noteren. Iedere sequentie van symbolen die omsloten worden door dubbele aanhalingstekens ("…") impliceert de creatie van een string-object, waarvan de betrokken symbolen de niet-wijzigbare inhoud zijn. Zo zal de uitdrukking "abc" resulteren in een referentie naar een nieuw aangemaakt string-object, waarvan het eerste symbool a is, gevolgd door het symbool b en afgesloten met het symbool c. Sommige vertalers voor Java proberen het aanmaken van identieke strings te vermijden. Indien in een programmatekst tweemaal dezelfde string genoteerd wordt, zullen dergelijke vertalers slechts één nieuw object van de klasse van strings aanmaken. Op beide plaatsen in de programmatekst zal dan gerefereerd worden naar hetzelfde object. Java ondersteunt ook een mechanisme om een tekstuele representatie te verkrijgen van eender welk object. Alhoewel we die methode niet gespecificeerd hebben, heeft de klasse van bankrekeningen ook een inspector public String toString(). Deze methode wordt geërfd van de klasse Object. Hierop wordt in detail ingegaan in hoofdstuk Error! Reference source not found., “Error! Reference source not found.”, op pagina Error! Bookmark not defined.. Standaard geeft deze methode een string-object terug dat begint met de volledige naam van de klasse, een @-teken, en een hexadecimaal getal (het getal is
de hashwaarde van het object; ook hiervoor is er een standaard methode in de klasse O b j e c t ). Zo zal a n A c c o u n t . t o S t r i n g ( ) iets teruggeven van de vorm Account@3b445b44. Het is mogelijk om zelf een ander resultaat te programmeren. Een afwijking op het feit dat strings objecten zijn zoals alle andere, is de taalondersteuning voor concatenatie. Naast instantiatiemethodes in de klasse String ondersteunt de taal ook het +-symbool als concatenatie-operator. Het resultaat van de expressie "this
is
"!+
anAccount.toString() is "this
is
Account@3b445b44". In bovenstaande expressie mag de oproep van toString weggelaten worden: "this is "!+ anAccount zal hetzelfde resultaat geven. toString is immers een bijzondere methode, die automatisch opgeroepen wordt waar een conversie van een object naar een string nodig is. Meer functionaliteit om met strings om te gaan wordt aangeboden als methodes in de voorgedefinieerde klasse String. Zo definieert de klasse een inspector length om het aantal tekens in een string op te vragen.
0.1.2.4.2
Invoer en uitvoer in Java
Eenvoudige invoer en uitvoer wordt klassiek ondersteund wordt door het besturingssysteem. Het meest algemene systeem, wat normaal datgene is dat door een taal wordt ondersteund, is afkomstig van Unix. In een Unix-omgeving heeft men 3 standaard stromen (streams): de standaard invoerstroom, de standaard uitvoerstroom en de standaard foutenstroom. Men spreekt van stromen om aan te duiden dat het over een lineaire stroom van karakters gaat, in principe zonder verdere structuur. Een programma kan invoer inlezen van de standaard invoerstroom, kan uitvoer schrijven naar de standaard uitvoerstroom, en kan foutenboodschappen schrijven naar de standaard foutenstroom. In de gebruikersomgeving worden de 3 stromen standaard gemapt op de commandolijninterface (command line interface) waarin het programma wordt opgestart. De gebruiker kan echter bij het opstarten de stromen ergens anders naartoe sturen, zoals een bestand of een netwerkconnectie. Of het programma zijn invoer interactief van de gebruiker krijgt, of uit een bestand leest, is daarmee irrelevant geworden voor de programmeur. Een meer geavanceerde gebruikersinterface, met vensters en invoervelden, met spraak of spraakherkenning, vereist andere technieken. In omgevingen zonder commandolijninterface kunnen deze technieken niet zonder meer gebruikt worden. Klassiek is het niet mogelijk invoer te krijgen zonder gebruik te maken van andere technieken. Meestal wordt door de omgeving wel een oplossing geboden voor de standaard uitvoerstroom en de standaard foutenstroom. Een voorbeeld is een webbrowser waarin een Java-applet draait. Er is geen ondersteuning voor invoer langs de
standaard invoerstroom, maar indien er iets wordt uitgeschreven op de standaard uitvoerstroom, dan zal de webbrowser een extra venster tonen met de resultaten. In Java werd het objectgerichte paradigma consequent doorgetrokken. De invoerstroom, uitvoerstroom- en foutenstroommedia zijn objecten die aangeboden worden door de standaard klassenbibliotheek, en invoer en uitvoer wordt gerealiseerd door het uitvoeren van instantiatiemethodes op deze objecten. Deze objecten maken deel uit van de voorgedefinieerde klasse System, die een vreemd allegaartje van publieke variabelen en methodes herbergt. De klasse System is één van de vele voorgedefinieerde klassen in Java. 5
— De publieke klassenvariabele in in de klasse System refereert naar een object dat de standaard invoerstroom modelleert. Klassenvariabelen worden besproken in hoofdstuk 0.1.3.2. De standaard invoerstroom is de normale stroom waarmee een gebruiker op een interactieve manier gegevens kan doorspelen naar een programma. Deze variabele refereert naar een object van de voorgedefinieerde klasse FileInputStream, die een aantal methodes aanbiedt om gegevens in te lezen in diverse formaten. — De publieke klassenvariabele out in de klasse System refereert naar een object dat de standaard uitvoerstroom modelleert. De standaard uitvoerstroom is de normale stroom waarlangs een programma haar resultaten kenbaar maakt aan de gebruikers. Deze variabele refereert naar object van de voorgedefinieerde klasse PrintStream, die een aantal methodes aanbiedt om gegevens in tekstuele vorm uit te schrijven. — De publieke klassenvariabele err in de klasse System refereert naar een object dat de standaard foutenstroom modelleert. Het object waarnaar de variabele err refereert, is ook een object van PrintStream. Ieder programma in Java kent zonder meer de klassen System, PrintStream en InputStream, zonder dat daarvoor speciale moeite moet gedaan worden. Hoe precies aangegeven wordt welke klassen gekend zijn in een specifieke applicatie komt in het tweede deel van de tekst uitvoerig aan bod. De klassen FileInputStream en PrintStream bieden elk een stel methodes om objecten en primitieve waarden in te lezen, respectievelijk uit te schrijven. In dit hoofdstuk beperken we ons tot een bespreking van twee methodes binnen de klasse PrintStream. Met behulp van de methode print kan een gegeven object of een gegeven primitieve waarde zonder meer in tekstuele vorm worden uitgeschreven. Met behulp van de methode
5
Merk op dat in de voorgedefinieerde klassen rond Java de regels die binnen deze tekst vooropgesteld worden, niet altijd gerespecteerd worden. Zo zou volgens deze tekst eerder een publieke inspector moeten voorzien worden binnen de klasse System, die een referentie aflevert naar het object dat de standaardinvoerstroom voorstelt.
println wordt na het uitschrijven van de tekstuele vorm overgegaan naar een nieuwe lijn. De tekstuele vorm van een object wordt bekomen door de methode toString er op toe te passen. Zoals hoger opgemerkt, zal deze methode automatisch toegepast worden indien een object in een String dient omgezet te worden. De tekstuele vorm van een primitieve waarde is taalgedefinieerd. Java voorziet in de taal geen syntaxis om de tekstuele vorm van primitieve waarden te schikken via één of andere formattering. In andere talen kan men bijvoorbeeld aangeven dat een geheel getal rechts gealinieerd moet uitgeschreven worden in een veld van 10 tekens breed. Zulks is wel mogelijk door andere klassen uit de standaard klassenbibliotheek te gebruiken. Voorbeeld 10 geeft een aantal voorbeelden van het gebruik van de standaard uitvoerstroom. In de eerste instructie wordt een groot geheel getal in tekstuele vorm uitgeschreven. In de tweede instructie wordt de gegeven string letterlijk uitgeschreven op de standaard uitvoerstroom. De laatste twee instructies schrijven de toestand van het object myAccount in tekstuele vorm uit. De laatste instructie is equivalent aan zijn voorganger, omdat Java automatisch de methode toString zal toepassen op objecten, in contexten waar een tekstuele voorstelling vereist is. Account myAccount = new Account(1000L); System.out.println(myAccount.getBalance()); System.out.println("My message"); System.out.println(myAccount.toString()); System.out.println(myAccount); Voorbeeld 10: het uitschrijven van informatie op de standaard uitvoerstroom
0.1.2.5
De Java-hoofdmethode
Een Java-programma is een collectie van klassen, zoals de klasse van bankrekeningen, waarbij iedere klasse een aantal methodes aanbiedt. Er is bijgevolg niet een speciaal concept dat overeenstemt met de notie van een hoofdprogramma uit procedurele talen zoals Pascal of C. De codefragmenten uit Voorbeeld 7, Voorbeeld 8 en Voorbeeld 9 maken bijgevolg op hun beurt deel van het lichaam van een methode van één of andere klasse. De uitvoering van een Java-programma start door een speciale methode, de hoofdmethode, toe te passen op de klasse die een dergelijke methode aanbiedt. Voorbeeld 11 illustreert de notie van een hoofdmethode in Java, aan de hand van het klassieke Hello Worldprogramma, dat de boodschap “Hello
World” uitschrijft op de standaard
uitvoerstroom. Na het uitschrijven van deze boodschap stopt de uitvoering van het programma.
public class HelloWorld { public static void main(String args[]) { System.out.println(“Hello World”); } } Voorbeeld 11: het Hello World programma als illustratie van de hoofdmethode in Java
De hoofdmethode is een klassenmethode. Elke klasse kan maximum één hoofdmethode bevatten. Om een werkend programma te hebben moet er minstens één klasse zijn met een 6
hoofdmethode in het project . De regels waaraan de hoofdmethode moet voldoen zijn als volgt: — De naam van de hoofdmethode moet main zijn. Omdat binnen identificatoren in Java hoofdletters onderscheiden worden van kleine letters, is een correcte spelling van de naam van de hoofdmethode belangrijk. Indien bijvoorbeeld een methode met naam Main zou geïntroduceerd worden, zou dit uiteraard niet als een fout gesignaleerd worden door de vertaler. Bij het opstarten van het systeem zal echter gezocht worden naar een methode met naam main. Een methode met naam Main zal niet herkend worden als hoofdmethode. — De hoofdmethode moet gedefinieerd worden als een klassenmethode. Vóór het opstarten van het programma kan er immers nog geen object zijn waarop een methode kan toegepast worden. — De hoofdmethode moet voorzien zijn van precies één expliciet argument in de vorm van een rij van objecten van de klasse String. Deze argumenten laten toe om informatie door te spelen bij de uitvoering van het programma. Merk op dat in Java een rij van objecten wordt aangegeven door de naam te laten volgen door niet nader gespecificeerde dimensies ([]). Rijen zullen in het tweede deel van deze tekst meer in detail bekeken worden. — De hoofdmethode mag geen expliciet resultaat teruggeven: het resultaattype moet void zijn. Indien een methode main voorzien wordt van andere argumenten, of een expliciet resultaat teruggeeft, zal ze niet als hoofdmethode aanvaard worden. — Het sleutelwoord public is eveneens verplicht. Java-programma’s worden uitgevoerd binnen een Java-virtuele-machine. Javaprogramma’s worden door de Java-vertaler omgezet tot een intermediaire code, die bytecode genoemd wordt. De bytecode-instructies worden vervolgens één voor één uitgevoerd door de Java-virtuele-machine.
6
Een alternatief voor programma’s met een hoofdmethode is in Java het gebruik van een applet (zie http://www.cs.kuleuven.ac.be/ObjectgerichtProgrammerenMetJava/).
De manier waarop Java-programma’s worden opgestart verschilt dan ook van het opstarten van programma’s geschreven in programmeertalen die machine-uitvoerbare code produceren. Voorbeeld 12 laat zien hoe Java-programma’s worden uitgevoerd op machines die een command line interface hebben, zoals Unix-varianten of het DOS-window in Microsoft™-producten. De Java-virtuele-machine wordt opgestart (java) en krijgt als parameter de naam van een klasse mee. De virtuele machine zal nu binnen de klasse met die naam zoeken naar een hoofdmethode, en die toepassen. Als de klasse in kwestie niet bestaat, of ze heeft geen hoofdmethode, dan wordt een foutenboodschap getoond. %java HelloWorld Hello World % Voorbeeld 12: opstarten van een Java-programma via een command line interface
Op de commandolijn kan je argumenten meegeven aan de applicatie. Deze argumenten worden gescheiden door spaties. De hoofdmethode krijgt de actuele waarden van de argumenten binnen in de rij args, die net zoveel elementen heeft als er actuele argumenten waren op de commandolijn. Als er geen argumenten worden opgegeven in de commandolijn, is de rij args dus nul elementen groot. public class HelloWorld2 { public static void main(String args[]) { System.out.println(“Hello World from” + args[0] + “ and “ + args[1]); } } Voorbeeld 13: het Hello World programma met argumenten %java HelloWorld2 eric jan Hello World from eric and jan % Voorbeeld 14: opstarten van het Hello World programma met 2 commandolijnargumenten
Bij de meeste virtuele machines kan je de manier van werken beïnvloeden door commandolijnargumenten tussen het commando java en de naam van de klasse. Deze opties vallen echter buiten het bestek van dit boek. Besturingssystemen zonder command line interface hebben andere manieren om een Java-programma op te starten. Op Apple™ Macintosh™ wordt met JBindery een bestandje gemaakt dat zich gedraagt als een klassieke applicatie. In dat bestandje worden alle argumenten die je normaal via de commandolijn opgeeft, bewaard. Soortgelijke dingen zijn ook onder Windows™ mogelijk.
0.1.3Representatie De specificatie van een klasse, zoals ontwikkeld in hoofdstuk 0.1.1, introduceert de methodes die nodig zijn voor het aanmaken en het manipuleren van objecten van die klasse. Uiteraard moeten alle methodes voorzien worden van een effectieve implementatie, om het gespecificeerde effect te bekomen. Daarenboven zal het nodig zijn om voor ieder object informatie bij te houden omtrent zijn huidige toestand. In dit hoofdstuk wordt ingegaan op het aspect van de representatie van objecten van een klasse, i.e. het bepalen van een geschikte voorstelling voor de interne toestand van ieder van haar objecten. In hoofdstuk 0.1.4 zal vervolgens aangegeven worden hoe de methodes die aangeboden worden door een klasse kunnen gerealiseerd worden in functie van die representatie. Het kiezen van een geschikte representatie voor de toestand van een object is een belangrijk aspect omdat de specificatie vrijheid laat aan de implementator. De lengte van een persoon wordt uitgedrukt door een positief reëel getal in de reële wereld, maar reële getallen worden niet ondersteund door een computer. Het is immers onmogelijk om bijvoorbeeld ’ met oneindige precisie voor te stellen. In een computer worden floating point getallen gebruikt, maar een floating point-type is slechts een deelverzameling van de rationale getallen, met een veranderlijke dekking over het interval dat ze bestrijken. Er zijn bijvoorbeeld veel meer elementen beschikbaar tussen 0 en 1 dan tussen 2001 en 2002. De implementator dient de representatie zo te kiezen dat de interne voorstelling de specificatie mogelijk maakt. Daarbij wordt hij gehinderd door vereisten omtrent performantie en geheugengebruik. Zo ondersteunt Java floating point getallen van 32-bit (float) en floating point getallen van 64-bit (double). 64-bit getallen hebben een grotere precisie en bestrijken een groter interval, maar gebruiken 2 keer zoveel geheugen. Een tweede aspect, zeer belangrijk bij fysische grootheden, is de keuze van de eenheid. De lengte van een persoon is niet zomaar een getal, maar een gedimensioneerd getal. Je drukt de lengte van een persoon uit in meter of millimeter, of voet, of een andere lengtemaat. De interne voorstelling hoeft niet dezelfde maat te gebruiken als de specificatie. Als de lengte van een persoon volgens de specificatie wordt uitgedrukt in meter, met 2 cijfers na de komma, met een minimum van 0 meter en een maximum van 2,5 meter, dan heeft het zin om die lengte intern bij te houden in centimeters. Dat is immers een geheel getal tussen 0 en 250, en met gehele getallen kan sneller en preciezer gerekend worden dan met 32-bit floating point getallen. Het zal duidelijk zijn dat deze keuzes goed gedocumenteerd moeten worden.
Ieder object van iedere klasse zal typisch voorzien worden van een toestand, die zal gemanipuleerd worden bij iedere methode die op het object wordt toegepast. Zo zal de toestand van iedere bankrekening een balans omvatten, die geïnitialiseerd wordt bij de constructie van die bankrekening, die aangepast wordt bij het toepassen van mutatoren, en die opgevraagd wordt bij het toepassen van inspectoren. De toestand van een object wordt bijgehouden in een stuk geheugen dat toegewezen wordt aan dat object bij de constructie ervan. De toestandsruimte voor de betrokken objecten zal op het niveau van de klasse worden gedefinieerd door een aantal instantiatievariabelen (instance variables) in te voeren. Naast instantiatievariabelen kunnen ook klassenvariabelen worden ingevoerd, naar analogie met klassenmethodes. Ze beschrijven niet de toestand van een specifiek object van de klasse, maar bevatten informatie over de klasse als geheel. In een variabele kunnen waarden van primitieve, ingebouwde types worden bewaard, of referenties naar objecten. Dit hoofdstuk zal in hoofdzaak beperkt worden tot het declareren van variabele van primitieve types, en meer in het bijzonder van gehele types. Andere primitieve types worden in deze tekst slechts sporadisch gebruikt. Referenties naar objecten worden in het tweede deel van deze tekst uitvoerig besproken.
0.1.3.1
Instantiatievariabelen
De declaratie van een instantiatievariabele omvat het type gevolgd door een naam voor de variabele. Het type van een instantiatievariabele is ofwel één van de ingebouwde, primitieve types van Java, ofwel een klasse. De naam van een instantiatievariabele is een identificator, i.e., een sequentie van hoofdletters, kleine letters, cijfers, onderlijningstekens en dollartekens, die niet begint met een cijfer. In de meeste documenten wordt voorgeschreven om voor de naam van een instantiatievariabele dezelfde stijlconventies te hanteren als voor de namen van mutatoren en inspectoren. In dit document wordt hiervan afgeweken, om instantiatievariabelen beter te kunnen onderscheiden van methodes. In deze tekst zal de naam van iedere instantiatievariabele beginnen met een dollarteken. Opeenvolgende woorden in de naam van een instantiatievariabele worden aan mekaar geschreven: het eerste woord begint met een kleine letter, volgende woorden beginnen met een hoofdletter. Een instantiatievariabele beschrijft een stuk van de totale informatie, die intern in ieder object van de betrokken klasse zal worden bijgehouden. Indien in de definitie van een klasse drie instantiatievariabelen geïntroduceerd worden, zal in het geheugen dat toegewezen wordt aan ieder object van die klasse, ruimte voorzien worden voor deze drie
instantiatievariabelen. Een instantiatievariabele bepaalt daarenboven hoe de informatie precies intern in ieder object van de klasse wordt bijgehouden. In de declaratie van iedere instantiatievariabele wordt immers aangegeven welk soort gegevens (type) erin kunnen bewaard worden. Voorbeeld 15 illustreert de declaratie van een instantiatievariabele voor de klasse van bankrekeningen. Om de huidige balans van iedere rekening bij te houden, wordt een instantiatievariabele $balance van het primitieve type long geïntroduceerd. In het geheugen dat toegewezen worden aan ieder object van de klasse van bankrekeningen zal ruimte voorzien worden voor het bewaren van de balans van de betrokken bankrekening. Volgens de declaratie van de instantiatievariabele zal dit gegeven bewaard worden inde vorm van een groot geheel getal. Merk op dat de declaratie van een instantiatievariabele aangevuld wordt met een documentatiecommentaar, waarin de inhoud van de variabele bondig wordt omschreven. De declaratie van instantiatievariabelen vormt een integraal onderdeel van de definitie van de klasse. In tegenstelling tot bijvoorbeeld C++, wordt in Java de specificatie van een klasse niet fysisch gescheiden van de implementatie. Een volledige definitie van de klasse van bankrekeningen is terug te vinden in Voorbeeld 30. De betekenis van het sleutelwoord private wordt uitgelegd, in hoofdstuk Error! Reference source not found., “Error! Reference source not found.”, op pagina Error! Bookmark not defined.. public class Account { … /** * The balance of this account. */ private long $balance; } Voorbeeld 15: declaratie van een instantiatievariabele voor de klasse van bankrekeningen.
0.1.3.2
Klassenvariabelen
De declaratie van klassenvariabelen volgt dezelfde regels als de declaratie van instantiatievariabelen. Naar analogie met klassenmethodes wordt het onderscheid gemaakt door het sleutelwoord static in het begin van de declaratie. We zullen consequent een naamgeving gebruiken zoals voor instantiatievariabelen, maar als eerste karakter een onderlijningsteken gebruiken (_) in plaats van het teken $. In tegenstelling tot instantiatievariabelen, worden klassenvariabelen gedeeld door alle objecten van de betrokken klasse. Wanneer een klasse met andere woorden twee klassenvariabelen introduceert, zal ergens geheugen gereserveerd worden voor deze twee
variabelen. Dit geheugen zal toegewezen worden bij het opstarten van het programma, en zal door alle objecten van de betrokken klasse gedeeld worden. Zoals voor instantiatievariabelen, zal het type van iedere klassenvariabele bepalen welk soort gegevens erin kunnen bewaard worden. Ter illustratie worden in Voorbeeld 16 klassenvariabelen ingevoerd om de ondergrens, respectievelijk de bovengrens voor de balans van rekeningen bij te houden. Dat kan, omdat de bank ons gezegd heeft dat de eigenschap voor alle bankrekeningen dezelfde is. Zoals onmiddellijk zal blijken, zal niet verder gewerkt worden met deze klassenvariabelen. public class Account { … /** * The lower limit for the balance of each account. */ static private long _lowerLimit; /** * The upper limit for the balance of each account. */ static private long _upperLimit; … } Voorbeeld 16: declaratie van klassenvariabelen voor de klasse van bankrekeningen
0.1.3.3
Symbolische constanten en niet-wijzigbare variabelen
Het gebruik van symbolische namen voor constanten dateert reeds van de eerste definitie van de taal Pascal. Symbolische namen voor constanten verhogen op de eerste plaats de leesbaarheid van de broncode. Zo is een programma waarin op diverse plaatsen een constante 100 gebruikt wordt, veel minder toegankelijk dan datzelfde programma waarin de symbolische naam MaxAantalVakken ingevoerd en consequent gebruikt wordt. Daarnaast is een programma waarin symbolische namen voor constante waarden ingevoerd worden veel eenvoudiger aan te passen, indien de constante vergroot of verkleind moet worden. Met symbolische namen moet enkel een verandering aangebracht worden in de definitie van de constante zelf. In het andere geval moeten alle voorkomens van de (letterlijke) constante worden opgespoord en aangepast. Uiteraard is dit een potentiële bron voor fouten in het gewijzigde programma. Java ondersteunt niet rechtstreeks het concept symbolische constanten. Java biedt wel de mogelijkheid om klassen- of instantiatievariabelen als niet-wijzigbaar te declareren (sleutelwoord final). De waarde van niet-wijzigbare variabelen moet exact één keer
gezet worden tijdens de initialisatie. Nadien kan er geen toekenning meer aan gebeuren, en is enkel inspectie mogelijk. De notatie voor het definiëren van een niet-wijzigbare klassenvariabele wordt geïllustreerd in Voorbeeld 17. Omdat voor de ondergrens en voor de bovengrens van de balans van bankrekeningen gezegd werd dat ze voor eens en voor altijd vastligt, en dus niet moeten veranderen tijdens de uitvoering van het programma, besluiten we om deze limieten als niet-wijzigbare klassenvariabelen in te voeren (final static). Voor de betekenis van het sleutelwoord private verwijzen we opnieuw naar hoofdstuk Error! Reference source not found., “Error! Reference source not found.”. public class Account { º /** * The lower limit for the balance of each account. */ final static private long LOWER = 0L; /** * The upper limit for the balance of each account. */ final static private long UPPER = Long.MAX_VALUE; … } Voorbeeld 17: definitie van constanten voor de grenzen van de balans
Niet-wijzigbare klassenvariabelen zijn grotendeels equivalent met symbolische constanten zoals ze gekend zijn in andere talen. Niet-wijzigbare instantiatievariabelen worden gebruikt om eigenschappen van objecten te stockeren die gezet moeten worden tijdens constructie, en daarna niet meer mogen wijzigen. Voor bankrekeningen zou de openingsdatum kunnen ingevoerd worden als een niet-wijzigbare instantiatievariabele. Het zal duidelijk zijn dat de levensduur van niet-wijzigbare instantiatievariabelen beperkt is door de levensduur van het object zelf. Het niet-wijzigbaar zijn van variabelen slaat op de onmiddellijke inhoud ervan. Wanneer de niet-wijzigbare variabele van een primitief type is, kan de waarde van de nietwijzigbare variabele na initialisatie nooit veranderen. Wanneer het type van de nietwijzigbare variabele een klasse is, dan bevat de niet-wijzigbare variabele een referentie naar een object. Het is deze referentie die na initialisatie nooit meer kan veranderen. Het object waarnaar de niet-wijzigbare variabele verwijst, kan eventueel mutatoren aanbieden, en die kunnen zonder probleem op dat object worden toegepast. Het object zelf is dus wel wijzigbaar. System.in, System.out en System.err zijn voorbeelden van nietwijzigbare (klassen)variabelen die refereren naar een object.
De naam van een niet-wijzigbare variabele is een identificator, net zoals die van een normale variabele. Als algemene conventie wordt in Java voorgesteld de namen van nietwijzigbare klassenvariabelen (symbolische constanten) volledig in hoofdletters weer te geven, waarbij verschillende woorden gescheiden worden door een _-teken. In deze tekst wordt deze conventie integraal gevolgd. Zo worden in Voorbeeld 17 de constanten LOWER en UPPER geïntroduceerd, die de laagst mogelijke, respectievelijk hoogst mogelijke waarde voor de balans van bankrekeningen symboliseren. Niet-wijzigbare instantiatievariabelen worden beschouwd als gewone instantiatievariabelen, en krijgen dus een naam die begint met $. Initialisatie van variabelen wordt in meer detail besproken in 0.1.4.3, “Initialisatie van variabelen”. Één mogelijkheid is onmiddellijk bij declaratie een waarde toe te kennen aan de niet-wijzigbare variabele. In Voorbeeld 17 wordt de ondergrens geïnitialiseerd met de letterlijke constante 0
als l o n g . De bovengrens wordt geïnitialiseerd met de
voorgedefinieerde constante Long.MAX_VALUE. De definitie van deze constante maakt deel uit van de voorgedefinieerde klasse Long uit de standaard klassenbibliotheek. Het sleutelwoord final wordt ook nog gebruikt bij klassen, methodes en argumenten. Daar heeft het echter een totaal andere betekenis. Finale klassen en methodes worden besproken
in
hoofdstuk
REF
FINALE_KLASSEN_EN_METHODES,
“REF_FINALE_KLASSEN_EN_METHODES”,
en
finale
argumenten
in
REF_FINALE_ARGUMENTEN, “REF_FINALE_ARGUMENTEN”.
0.1.3.4
Semantiek van variabelen
De precieze semantiek van de declaratie van een instantiatievariabele of van een klassenvariabele is gelijkaardig aan de declaratie van andere (lokale) variabelen en aan de declaratie van formele argumenten. In deze paragraaf zal worden aangegeven dat variabelen van een primitief type een waardesemantiek of kopiesemantiek hebben, terwijl variabelen van een klassentype een referentiesemantiek hebben. Deze verschillen zullen zich niet alleen uiten in de waarden die kunnen bewaard worden in dergelijke variabelen. De verschillen zullen zich met name ook manifesteren in de betekenis van de toekenning en van de vergelijkingsoperatoren.
0.1.3.4.1
Waardesemantiek
Voor een variabele met waardesemantiek wordt geheugen gereserveerd voor het rechtstreeks bewaren van een waarde van het gespecificeerde type. In Java, wordt waardesemantiek toegepast op iedere variabele en ieder argument van een primitief type.
Voor de instantiatievariabele $balance uit Voorbeeld 15, zal in iedere bankrekening geheugen voorzien worden voor het direct bewaren van een waarde van het type long. Voor het formeel argument amount in de mutator withdraw uit Voorbeeld 6, zal, idem dito, bij iedere toepassing van de methode geheugen voorzien worden voor het bewaren van een waarde van het type long. In Voorbeeld 18 worden drie variabelen gedeclareerd waarin telkens een waarde van het type i n t direct kan geregistreerd worden. In dit voorbeeld wordt het soort variabele (instantiatievariabele, klassenvariabele, lokale variabele of argument) in het midden gelaten. Waardesemantiek heeft onmiddellijke gevolgen voor de precieze betekenis van de toekenningen en vergelijkingen waarin dit soort variabelen betrokken zijn. Bij toekenning zal een kopie van de waarde, die resulteert uit de evaluatie van de uitdrukking aan de rechterkant van de toekenningsoperator (=), worden geregistreerd als nieuwe inhoud van de variabele aan de linkerkant. In de eerste toekenning van Voorbeeld 18 wordt op die manier een kopie van het geheel getal 5 geregistreerd in de locatie toegewezen aan de variabele x. Merk op dat toekenning in Java, een uitdrukking is. Het resultaat van een toekenningsuitdrukking is (een kopie van) de waarde die wordt toegekend. Dit maakt het onder andere mogelijk om (kopies van) dezelfde waarde toe te kennen aan verschillende variabelen in één enkele instructie. In de tweede toekenning van Voorbeeld 18 wordt op die manier een kopie van de huidige waarde van de variabele x achtereenvolgens toegekend aan de variabelen y en z. Het staat buiten kijf dat deze stijl vaak de leesbaarheid vermindert, en af te raden is. Bij het toepassen van methodes zal, bij het binden van actuele waarden aan formele argumenten, eveneens een toekenning gebeuren. Bij het binden van het actuele argument myBudget uit Voorbeeld 8 aan het formele argument amount, zal bijgevolg een kopie van de huidige inhoud van de variabele myBudget worden geregistreerd in de locatie die toegewezen wordt aan het formele argument amount. Deze manier van bindingassociatie tussen actuele en formele argumenten wordt waardeparameterbinding (call by value) genoemd. Hetzelfde principe geldt bij de waarde die de inspector getBalance teruggeeft. Bij vergelijking met behulp van de operatoren == en != zullen de waarden die geregistreerd zijn in de betrokken variabelen vergeleken worden met mekaar. In Voorbeeld 18 wordt op die manier nagegaan of de waarden geregistreerd in de variabelen x en y gelijk zijn. Het resultaat van deze vergelijking kan onder andere gebruikt worden als controleuitdrukking in een conditionele opdracht. int x, y, z; x = 5; z = y = x;
if (x == y) … Voorbeeld 18: declaratie, toekenning en vergelijking van variabelen met waardesemantiek
0.1.3.4.2
Referentiesemantiek
Voor een variabele met referentiesemantiek wordt geheugen gereserveerd voor het rechtstreeks bewaren van een referentie, een wijzer (pointer) naar een waarde van het gespecificeerde type. In Java, wordt referentiesemantiek toegepast op iedere variabele van een klassentype. Dit betekent dat dergelijke variabelen steeds zullen wijzen naar objecten van de betrokken klasse. Voor het formeel argument destination in de mutator transferTo uit Voorbeeld 6, zal bij iedere toepassing van de methode geheugen voorzien worden voor het bewaren van een referentie naar een object van de klasse van bankrekeningen. In Voorbeeld 19 worden drie variabelen gedeclareerd waarin telkens een referentie naar een object van de klasse van bankrekeningen kan geregistreerd worden. In dit voorbeeld wordt het soort variabele (instantiatievariabele, klassenvariabele, lokale variabele of argument) in het midden gelaten. Referentiesemantiek heeft onmiddellijke gevolgen voor de betekenis van toekenningen en vergelijkingen waarin dit soort variabelen betrokken zijn. Bij toekenning zal een referentie naar het object, dat resulteert uit de evaluatie van de uitdrukking aan de rechterkant van de toekenningsoperator, worden geregistreerd als nieuwe inhoud van de variabele aan de linkerkant. In de eerste toekenning van Voorbeeld 19 wordt op die manier een referentie naar een nieuw object van de klasse van bankrekeningen geregistreerd in de locatie toegewezen aan de variabele myAccount. In feite resulteert de uitdrukking aan de rechterkant van de toekenningsoperator in een referentie. Een kopie van die referentie wordt toegekend aan de betrokken variabele. In de tweede toekenning wordt een kopie van de referentie, geregistreerd in de variabele myAccount achtereenvolgens toegekend aan de variabelen yourAccount en hisAccount. Bij het toepassen van methodes zal, bij het binden van actuele waarden aan formele argumenten, eveneens een toekenning gebeuren. Bij het binden van het actuele argument yourAccount uit Voorbeeld 8 aan het formele argument destination, zal bijgevolg een kopie van de referentie in de variabele yourAccount worden geregistreerd in de locatie die toegewezen wordt aan het formele argument destination. Voor de referentie zelf geldt bijgevolg waardeparameterbinding. Het gerefereerde object zelf kan echter via die referentie naar willekeur worden aangepast.
Bij vergelijking met behulp van de operatoren == en != zullen de referenties die geregistreerd zijn in de betrokken variabelen vergeleken worden met mekaar. In Voorbeeld 19 wordt op die manier nagegaan of de referenties geregistreerd in de variabelen myAccount en yourAccount gelijk zijn. Merk op dat de gerefereerde objecten zelf onderling niet vergeleken worden. In het voorbeeld zal de vergelijking enkel waar zijn als beide variabelen refereren naar dezelfde bankrekening, en onwaar als ze refereren naar verschillende bankrekeningen, met gelijke (of ongelijke) balansen. Om de gerefereerde objecten zelf te vergelijken wordt de methode equals aangeboden in Java. Dit is opnieuw een methode die door elk object ondersteund wordt, zoals de methode toString. De standaard implementatie vergelijkt de referenties met ==, zodat de twee laatste lijnen van het voorbeeld in principe hetzelfde effect hebben. Het is echter mogelijk voor een bepaalde klasse een andere implementatie te voorzien voor de methode equals, bijvoorbeeld door de toestand van de betrokken objecten te vergelijken. Deze methode wordt in meer detail besproken in hoofdstuk COMPLEXE_WAARDEN_REF, “COMPLEXE_WAARDEN_REF”. Account myAccount, yourAccount, hisAccount; myAccount = new Account (2000); hisAccount = yourAccount = myAccount; if (yourAccount == myAccount) … if (yourAccount.equals(myAccount)) … Voorbeeld 19: declaratie, toekenning en vergelijking van variabelen met referentiesemantiek
0.1.3.4.3
Referenties in Java ten opzichte van expliciete wijzers in andere programmeertalen
In vergelijking met klassieke procedurele talen, zoals Pascal of C, biedt Java geen concepten om referenties of wijzers expliciet te manipuleren. In de definitie van de taal wordt eens en voor altijd vastgelegd welke gegevens rechtstreeks via gewone variabelen gemanipuleerd kunnen worden, en welke onrechtstreeks via wijzervariabelen. Dit beperkt uiteraard de mogelijkheden in de ontwikkeling van programma’s, maar vereenvoudigt meteen ook de taak van de programmeur in aanzienlijke mate. Het verlies aan flexibiliteit is trouwens de reden waarom in C++, in tegenstelling tot Java, wijzers nog steeds expliciet aanwezig zijn. De verschillen tussen Pascal en Java op het vlak van waarde- en referentiesemantiek worden geïllustreerd in Voorbeeld 20. — Het record-type Account komt structureel overeen met de klasse van bankrekeningen in Java. Meer in het bijzonder komt het veld balance uit het record-type overeen met de instantiatievariabele $balance uit de klasse van bankrekeningen.
— De variabelen myAccount en myBudget uit het Pascal-fragment stemmen volledig overeen met de Java-declaraties uit Voorbeeld 8, met uitzondering van de verschillen tussen het type i n t e g e r in Pascal en het type l o n g in Java. De variabele myAccount heeft in beide talen referentiesemantiek; de variabele myBudget heeft in beide talen waardesemantiek. — De variabele directAccount uit het Pascal-fragment heeft geen tegenhanger in Java. Het is met andere woorden onmogelijk om in Java een variabele te declareren waarvan de directe inhoud een object van een klasse is. — De variabele refInteger heeft wel een tegenhanger in Java. In de standaard klassenbibliotheek worden schilklassen (wrapper classes) aangeboden, waarvan de objecten een referentiële schil vormen rond waarden van de primitieve types. Aan deze klassen is niets mysterieus: we zouden ze ook zelf kunnen schrijven. Ze hebben één instantiatievariabele van het primitieve type in kwestie, en ze bieden gewoon de mogelijkheid via referenties met waarden van primitieve types te werken, en extra functionaliteit via instantiatie- en klassenmethodes. Ook interessante constanten worden via deze schilklassen als niet-wijzigbare klassenvariabelen aangeboden. Het gebruik van deze schilklassen valt grotendeels buiten de context van deze tekst. type Account = record balance : integer; end; RfAccount = ↑Account; var myAccount : RfAccount; myBudget : integer; directAccount : Account; refInteger : ↑integer; Voorbeeld 20: waarde- en referentiesemantiek in Pascal
0.1.3.5
Primitieve types in Java
Java biedt een aantal primitieve, voorgedefinieerde types voor het werken met elementaire waarden zoals gehele getallen en symbolen. In dit hoofdstuk bespreken we kort een aantal dingen omtrent primitieve types, voldoende om te kunnen programmeren. Een aantal technische details bespreken we hier niet omdat ze niet direct relevant zijn voor het hoofdthema van dit boek, de principes van het objectgericht programmeren. Ze zijn echter wel relevant voor de productieprogrammeur, omdat een aantal zaken die te maken hebben met geheugengebruik en performantie niet-intuïtief zijn. Voor deze details verwijzen we naar XXX DE WEBSITE XXX.
Java voorziet primitieve types voor booleaanse logica, gehele getallen, getallen in vlottende-komma-voorstelling (floating point-getallen), en symbolen (karakters). Voor elk van de types zijn een aantal bewerkingen in de taal aanwezig, via operatoren. In tegenstelling tot andere talen, specificeert de Java-taal het formaat van de types. In andere talen is het formaat van de types afhankelijk van de processor. Het effect hiervan is dat een berekening exact hetzelfde resultaat zal geven, onafhankelijk van de architectuur waarop de berekening wordt uitgevoerd. Een nadeel is dat de uitvoeringstijd van platform tot platform kan verschillen, en de code niet geoptimiseerd is voor het platform waar de code op draait. Deze nadelen betalen we echter graag als prijs voor de betere draagbaarheid van de code. Waarden hebben, in tegenstelling tot objecten, geen levensduur. Een waarde wordt niet op een bepaald moment aangemaakt om na verloop van tijd weer te verdwijnen. Verder hebben waarden geen variabele toestand. Het introduceren van mutatoren om de toestand van waarden te veranderen is daarom niet aan de orde. Van de andere kant hebben waarden wel een bepaalde voorstelling hebben en kunnen er een aantal functionaliteiten op toegepast worden. De notie van een abstract datatype is een overkoepelende concept, dat zowel toepasbaar is op verzamelingen van waarden als op verzamelingen van objecten. Sommige programmeertalen, zoals C++ en Eiffel, bieden concepten om naast klassen van variabele objecten ook eigen types van waarden te introduceren. In Java kunnen enkel klassen worden aangemaakt, en is de verzameling types van waarden vast en taalgedefinieerd.
Dit
wordt
in
meer
detail
besproken
in
hoofdstuk
COMPLEXE_WAARDEN_REF, “COMPLEXE_WAARDEN_REF”, op pagina COMPLEXE_WAARDEN_REF.
0.1.3.5.1
Aangeboden types
Java ondersteunt vier primitieve types voor gehele getallen, maar slechts twee zijn relevant: int en long. Om technische redenen raden we het gebruik van de andere gehele types, short en b y t e, af. Het type int beschrijft het interval [-2 147 483 648, 2 147 483 647], het type l o n g het interval [-9 223 372 036 854 775 808, 9 223 372 036 854 775 807]. De grenzen zijn in de taal beschikbaar als niet-wijzigbare klassenvariabelen van de schilklassen (bijvoorbeeld Integer.MAX_VALUE en Long.MIN_VALUE) Ook voor letterlijke constanten (getallen in broncode) kan men aangeven of het gaat om een int of een long. int-getallen worden voorgesteld door een niet geannoteerd getal (12345), long-getallen door een getal gevolgd door de letter l of L (12345L). Omdat in
sommige lettertypes geen onderscheid wordt gemaakt tussen de glyph voor 1 en l, raden we sterk aan steeds de hoofdletter L te gebruiken. Er bestaat geen suffix voor int-getallen. Java ondersteunt twee vlottende-komma-types; float en double. Waarden van het type double hebben een precisie die dubbel zo groot is als waarden van het type float. Het grootste positieve getal dat door een float-waarde kan worden voorgesteld is 3,40282347e+38, het kleinste positief getal is 1,40239846e-45. Het grootste positieve getal dat door een double-waarde kan worden voorgesteld is 1,79769313486231570e+308, het kleinste is 4,94065645841246544e-324. Om in letterlijke constanten aan te geven dat het over een float-waarde gaat, schrijft men f of F na de getallen. Om aan te geven dat het over een double-waarde gaat, schrijft men d of D na de getallen. De default is double. Het booleaanse primitieve type b o o l e a n heeft slechts 2 elementen in zijn waardeverzameling. Ze worden uitgedrukt door de sleutelwoorden true en false. 7
De elementen van het type char zijn alle Unicode-karakters . In broncode kunnen ze geschreven worden als karakters tussen enkele aanhalingstekens (‘a’).
0.1.3.5.2
Bewerkingen en vergelijkingen
De normale rekenkundige bewerkingen (som: +, verschil: -, product: *, quotiënt: /, modulo: %) worden in de taal ondersteund voor alle getaltypes. De operator – kan ook gebruikt worden als unaire operator om de negatieve waarden van de operand te bekomen. Booleaanse waarden kunnen gecombineerd worden met && (conditionele logische conjunctie — (conditional and), || (conditionele logische disjunctie — (conditional or) en ^ (exclusieve disjunctie — xor). ! is de operator voor de booleaanse negatie. Karakters en string-objecten kunnen geconcateneerd worden met de +-operator: ‘a’!+!’b’ geeft als resultaat “ab”. De standaard vergelijkingoperatoren (gelijkheid: ==, ongelijkheid: !=) zijn toepasbaar op alle primitieve types (en op objectreferenties). Voor getallen en karakters worden ook de bekende orderelaties >, <, >=, <= ondersteund. Meer complexe rekenkundige methodes zijn te vinden als klassenmethodes in de klasse java.lang.Math in de standaard klassenbibliotheek. Andere methodes worden nog aangeboden in de schilklassen, zoals java.lang.Integer en java.lang.Long in de standaard klassenbibliotheek.
7
Zie http://www.unicode.org/
Voor alle binaire bewerkingen bestaat er ook een toekenningsoperator die de bewerking en de toekenning in één kortere expressie combineert (+=, -=, *=, /=, %=, &&=, |=, ^=). Het linkerlid van een dergelijke gecombineerde operator moet steeds een variabele te zijn. Het effect van bijvoorbeeld x!+=!a is equivalent met x!=!x!+!a. De operatoren ++ (increment) en - -) (decrement) zijn verkorte notaties voor een toekennende optelling, respectievelijk aftrekking met als rechteroperand de constante 1. Dit betekent dat semantisch gezien de uitdrukkingen x!=!x!+!1, x!+=!1, ++x en x++ volledig equivalent zijn. Omdat het verkorte notaties zijn voor toekennende optellingen en aftrekkingen, moet de enige operand een variabele zijn. Met andere woorden de operand van de operatoren + + en - - moet kunnen voorkomen aan de linkerkant van een toekenning. Beide operatoren kunnen zowel in prefix als in postfix worden toegepast op de operand. In prefix wordt de inhoud van de variabele eerst verhoogd (verlaagd) en daarna verder gebruikt in de uitdrukking. In de toekenning z=++x zal eerst x verhoogd worden met 1 en daarna toegekend worden aan z. De variabelen z en x zullen dus na uitvoering van de toekenning dezelfde inhoud hebben. In postfix wordt de huidige inhoud van de variabele gebruikt als waarde van de operand. Daarna wordt de inhoud van de variabele verhoogd (verlaagd) met 1. In de toekenning y!=!x++ wordt eerst de inhoud van x toegekend aan y, waarna x wordt opgehoogd met 1. De inhoud van de variabele y zal bijgevolg 1 kleiner zijn dan de inhoud van x. Voor de conjunctie en de disjunctie bestaat een enkele versie (& en |) en een dubbele versie (&& en ||). De dubbele versies worden respectievelijk conditionele logische conjunctie en conditionele logische disjunctie genoemd, de enkele versies simpelweg logische conjunctie en logische disjunctie. Het verschil tussen de twee versies is dat bij de conditionele versies eerst de eerste operand wordt geëvalueerd. De tweede operand zal slechts geëvalueerd worden als het nog nodig is. Als bij de conditionele conjunctie de eerste operand evalueert naar false, dan weten we al dat het resultaat van de bewerking false zal zijn, en is het niet meer nodig om de tweede operand nog te evalueren. Bij de conditionele disjunctie zal de tweede operand enkel geëvalueerd worden als de eerste operand evalueert naar false. De conditionele versies van de booleaanse operatoren zijn zeer handig in code die rekening moet houden met bijzondere omstandigheden. Een goed voorbeeld is het delen door nul. In Voorbeeld 21 is het de bedoeling dat iets wordt gedaan indien de verhouding van t ten opzichte van d kleiner is dan een constante. De eerste versie voldoet niet, omdat d nul kan zijn. In dat geval zou de expressie falen met een uitzondering. We beslissen dat indien d nul is, de gevraagde code niet moet uitgevoerd te worden (delen door nul zou
oneindig geven, wat zeker niet kleiner is dan de grensconstante). De tweede versie werkt wel: de deling wordt nu enkel uitgevoerd indien d niet nul is. De gevraagde code wordt slechts uitgevoerd als aan beide criteria voldaan is. We raden het gebruik van de enkele logische operatoren & en | af. Zelfs indien er geen onmiddellijk verschil is, heeft het zin de conditionele operatoren te gebruiken. Ze zorgen immers voor snelheidswinst. In dit boek raden we consequent af dingen op te offeren voor meer performantie, maar indien de alternatieven conceptueel evenwaardig zijn, zou het natuurlijk dom zijn de technisch minder goede optie te kiezen. … if ((t / d) < LIMIT) do something … … if ((d != 0) && ((t / d) < LIMIT)) do something … Voorbeeld 21: gebruik van de conditionele booleaanse operatoren
Een uitgebreide lijst van alle operatoren die in Java beschikbaar zijn, vind je in REF.
0.1.3.5.3
Overloop, onderloop en delen door 0
Rekenkundige bewerkingen op getallen worden uitgevoerd modulo het bereik van die getallen. Dit betekent dat een berekening die buiten het bereik van het getaltype leidt, gewoon doorgaat zonder dat enige foutmelding wordt gegeven. Indien bijvoorbeeld bij een int-berekening bij Integer.MAX_VALUE 1 wordt opgeteld, is het resultaat 2 147 483 648, i.e. I n t e g e r . M I N _ V A L U E , en niet 2 147 483 648. Integer.MIN_VALUE!– Integer.MAX_VALUE heeft als resultaat 1. Een ander leuk effect is dat bijvoorbeeld –Long.MIN_VALUE hetzelfde int-getal voorstelt als Long.MIN_VALUE. Men onderscheidt overloop (overflow — het bereik van het type wordt aan één van de bovengrenzen overschreden) en onderloop (underflow — het bereik van het type wordt aan één van de ondergrenzen overschreden). Bij gehele types kan enkel overloop optreden, als de waarden te sterk negatief of te sterk positief zijn. Onderloop kan zich enkel voordoen bij de types float en double in berekeningen waarvan het resultaat te dicht bij 0 ligt. In Java zal in dergelijke gevallen 0 als resultaat worden afgeleverd, zonder de gebruiker hiervan op de hoogte te stellen. Dat Java overloop en onderloop als nominaal gedrag van de rekenkundige operaties 8
aanziet, en geen waarschuwing geeft, is onbegrijpelijk . Deze beslissing staat haaks op de 8
Het verlies van de Arianne 503 op 4 juni 1996 was het gevolg van het niet-gooien van een overloopuitzondering. De code voor de Arianne is geschreven in Ada, een taal die standaard wel
globale teneur van de taal, die stabiliteit, eenvoud en eenduidigheid steeds boven technische criteria stelt. Het gevolg is dat de programmeur steeds op zijn hoede moet zijn voor overloop en onderloop, en bij risicoberekeningen steeds eigenhandig foutdetectiecode moet schrijven. Er zullen wel foutmeldingen gegenereerd worden bij deling van een geheel getal door 0 (/) en bij het bepalen van de rest bij deling van een geheel getal door 0 (%). Deze foutmelding zal komen in de vorm van een uitzondering (exception), in dit geval een ArithmeticException. In hoofdstuk Error! Reference source not found. zal aangegeven worden hoe met dergelijke uitzonderingen kan omgegaan worden in Java. Een floating point-getal door 0 delen is toegelaten. Dit geeft de waarde Infinity indien het deeltal positief is, en -Infinity indien het deeltal negatief is. Infinity en -Infinity zijn elementen van het type float en double zoals 0 en 1. Er is geen notatie voorzien om deze waarden letterlijk in code te vermelden. Je kan over deze waarden spreken door ze te omschrijven, bijvoorbeeld als 1f/0f, -1f/0f, 1d/0d of -1d/0d.
0.1.4
Implementatie van methodes
Zodra de representatie van de objecten van een klasse is vastgelegd, kunnen de gespecificeerde constructoren, mutatoren en inspectoren voorzien worden van een implementatie. De meeste instantiatiemethodes bewerken immers de interne toestand van het betrokken object. Daarnaast kan een methode de toestand van andere objecten manipuleren, die via formele argumenten of via instantiatievariabelen of klassenvariabelen vanuit het betrokken object bereikbaar zijn. De implementatie van de diverse methodes van een klasse maakt integraal deel uit van de definitie van die klasse. Specificatie en implementatie zijn bijgevolg geïntegreerd in één enkele beschrijving. De volledige definitie van de klasse van bankrekeningen wordt weergegeven in Voorbeeld 30 op pagina 65. Een aantal elementen uit de implementatie van de diverse methodes worden hieronder toegelicht. Het lichaam van een methode volgt onmiddellijk op de hoofding, en wordt omsloten door {…}. waarschuwt bij overloop en onderloop. Bij een optimisatie werd voor de berekening in kwestie de overlooptest echter handmatig uitgeschakeld om performantieredenen. De code werd oorspronkelijk geschreven voor de Arianne IV, en voor die raket was aangetoond dat de overloop nooit kan optreden. De code werd zonder meer, in geoptimiseerde versie, voor de Arianne V gebruikt, en bij deze raket kon de overloop wel optreden. Zie http://www.esrin.esa.it/htdocs/tidc/Press/Press96/ariane5rep.html.
0.1.4.1
Het impliciete argument
In de specificatie van constructoren, mutatoren en inspectoren werd aangegeven dat er steeds een object van de klasse impliciet betrokken is in iedere instantiatiemethode. Dit object werd het (direct) betrokken object of impliciet argument genoemd, en wordt bij toepassing van de methode in prefix geplaatst. Uiteraard moet dit object in de implementatie van de methode bereikbaar zijn. Java biedt zowel een expliciete als een impliciete manier om toegang te krijgen tot het direct betrokken object. Het betrokken object is binnen het lichaam van iedere instantiatiemethode bereikbaar via een speciale variabele this. Dit is een variabele met referentiesemantiek, die verwijst naar het direct betrokken object. Wanneer binnen het lichaam van een methode een (andere) methode f moet worden toegepast op het betrokken object, dan kan dit bijgevolg genoteerd worden als this.f(º). Analoog kan toegang tot een instantiatievariabele $x van het betrokken object genoteerd worden als this.$x. De variabele this kan niet van waarde veranderen. Toekenningen aan deze variabele zijn bijgevolg uit den boze. Het expliciet gebruik van de variabele this is eerder beperkt in de implementatie van de methodes binnen een klasse. Als algemene regel geldt immers dat toepassingen van nietgekwalificeerde methodes automatisch betrekking hebben op het direct betrokken object. Een niet-gekwalificeerde toepassing van een methode is een toepassing waarin niet expliciet wordt aangegeven op welk object de methode wordt toegepast. De nietgekwalificeerde toepassing van een methode f(…) in het lichaam van een (andere) methode, is bijgevolg equivalent aan this.f(…). Een gelijkaardige regel is van toepassing op instantiatievariabelen. Een niet-gekwalificeerde toegang tot een instantiatievariabele, heeft automatisch betrekking op de instantiatievariabele van het betrokken object. De niet-gekwalificeerde toegang tot de instantiatievariabele $x is bijgevolg equivalent aan this.$x. Impliciete manipulatie van het direct betrokken object wordt geïllustreerd in de implementatie van de mutatoren deposit en withdraw. Omdat de instantiatievariabele $balance aan de linkerkant van beide toekenningen niet gekwalificeerd is, hebben ze beiden betrekking op de balans van de direct betrokken bankrekening. Bij de implementatie van de methode transferTo werd, louter ter illustratie, de expliciete vorm gebruikt om een methode toe te passen op het betrokken object. In het lichaam wordt eerst een afhaling toegepast op de direct betrokken rekening, gevolgd door een storting op de doelrekening. De implementatie van deze methode wordt verder besproken in hoofdstuk 0.1.4.5
In klassenmethodes is het impliciet argument de klasse zelf. Bijgevolg is er in dergelijke methodes geen sprake van een direct betrokken object, en is de variabele this niet gedefinieerd binnen het lichaam van klassenmethodes. In Voorbeeld 22 worden de inspectoren g e t L o w e r L i m i t en g e t U p p e r L i m i t geïmplementeerd als klassenmethodes. Niet-geannoteerde methodes en variabelen binnen het lichaam van een klassenmethode, zoals LOWER in het lichaam van de inspector getLowerLimit, verwijzen naar klassenmethodes of naar klassenvariabelen van dezelfde klasse. Ter illustratie werd, in het lichaam van de inspector getUpperLimit, de klassenvariabele UPPER expliciet geannoteerd met de klassennaam. public class Account { … /** * Return the lower limit for the balance of accounts. */ static public long getLowerLimit() { return LOWER; } /** * Return the upper limit for the balance of accounts. */ static public long getUpperLimit() { return Account.UPPER; } … } Voorbeeld 22: implementatie van klassenmethodes
In
Voorbeeld 30 worden de instantiatiemethodes g e t L o w e r L i m i t en
getUpperLimit voorzien van een implementatie. Ook hier kan de klassenvariabele LOWER geannoteerd of niet-geannoteerd gebruikt worden. In de implementatie van instantiatiemethodes zijn immers ook de klassenvariabelen en klassenmethodes beschikbaar. Equivalente implementaties van de inspector getLowerLimit worden getoond in Voorbeeld 23. In een eerste versie wordt de klassenvariabele geannoteerd met de klassennaam, in een tweede versie met this. Deze mogelijkheden werden ook al voorgesteld in Voorbeeld 9. Klassenmethodes kunnen op dezelfde manieren gebruikt worden. public class Account { … /** * Return the lower limit for the balance of this account. */ public long getLowerLimit() { return Account.LOWER; } … }
public class Account { … /** * Return the lower limit for the balance of this account. */ public long getLowerLimit() { return this.LOWER; } … } Voorbeeld 23: gebruik van klassenvariabelen in instantiatiemethodes
0.1.4.2
De terugkeeropdracht
Bij de implementatie van inspectoren moet, naast het consulteren van de toestand van het direct betrokken object en van de indirect betrokken objecten, informatie worden teruggegeven. In Java wordt de terugkeerwaarde vastgelegd in een terugkeeropdracht (return instruction). De algemene vorm van een terugkeeropdracht omvat het sleutelwoord return gevolgd door een uitdrukking die de waarde bepaalt die aan de oproeper zal worden teruggegeven. Het type van de uitdrukking moet conform zijn met het type van het resultaat dat door de betrokken methode wordt teruggeven. De terugkeeropdracht mag overal in het lichaam van de methode staan. Een methode wordt onmiddellijk beëindigd wanneer de uitvoering een terugkeeropdracht tegenkomt. In het lichaam van een methode kunnen meerdere terugkeeropdrachten staan, bijvoorbeeld in verschillende takken van conditionele structuren. Natuurlijk wordt enkel de eerste terugkeeropdracht die de uitvoering tegenkomt, uitgevoerd. Merk op dat het op deze manier mogelijk is code te schrijven die nooit uitgevoerd kan worden, omdat er eerder, op alle uitvoeringspaden, een terugkeeropdracht staat. Voor methodes die geen expliciet resultaat teruggeven (void), mag de uitdrukking in de terugkeeropdracht worden weggelaten. Op het einde van het lichaam van dergelijke methodes wordt impliciet een terugkeeropdracht zonder terugkeerwaarde geplaatst. Een terugkeeropdracht zonder waarde (return;) is toegelaten, en kan gebruikt worden om de uitvoering op andere punten dan het lexicale einde van de methode te beëindigen. Voor methodes die wel een waarde teruggeven, moet de uitvoering van het lichaam van de methode steeds eindigen met een expliciete terugkeeropdracht. De implementatie van inspectoren wordt in Voorbeeld 30 geïllustreerd. De inspector getBalance, die de huidige balans van de betrokken rekening moet teruggeven, wordt gerealiseerd door de waarde van de instantiatievariabele $balance terug te geven. Bij het doorgeven van terugkeerwaarden geldt, net zoals bij de associatie van actuele argumenten
aan formele parameters, het principe van toekenning. Indien bijgevolg een primitieve waarde wordt teruggegeven, zal een kopie van die waarde worden doorgegeven. Indien een referentie naar een object wordt teruggegeven, wordt analoog een kopie van die referentie doorgegeven. De oproeper heeft dan de mogelijkheid het gerefereerde object, respectievelijk de gekopieerde waarde naar willekeur te bewerken.
0.1.4.3
Initialisatie van variabelen
De initialisatie van variabelen is een klassiek probleem dat zich stelt in alle imperatieve programmeertalen. Sommige talen zoals C en C++ hechten geen belang aan een goede initiële waarde voor variabelen. Het enige wat in dergelijke talen gebeurt is dat de variabele bij declaratie een stuk van het geheugen krijgt toegewezen. Als de variabele geïnspecteerd wordt vóór er door het programma een waarde aan werd toegekend, dan blijkt dat de waarde van de variabele willekeurig is. Ze bevat bij deze talen de bitconfiguratie die toevallig op die geheugenlocatie is achtergelaten door een vorige gebruiker. Dat kan het programma zelf zijn, het besturingssysteem of zelfs een ander programma. Meestal leidt deze situatie tot uitvoeringsfouten. Er is bijvoorbeeld geen enkele garantie dat het toevallige bitpatroon voldoet aan de structuur die van variabelen van het type in kwestie verwacht wordt. De effecten bij het gebruik van deze waarden zijn onvoorspelbaar. Deze fouten blijken ook extreem moeilijk te analyseren, omdat de inhoud van de variabele toevallig is, en dus soms wel en soms niet tot problemen zal leiden. Om dit probleem op te lossen zijn er principieel twee strategieën mogelijk. Ofwel verplicht de taal de programmeur elke variabele te initialiseren voor ze geïnspecteerd wordt, ofwel initialiseert de taal elke variabele automatisch met een aanvaardbare defaultwaarde. Java gebruikt beide strategieën, op verschillende plaatsen.
0.1.4.3.1
Impliciete initialisatie van instantiatievariabelen en klassenvariabelen
Instantiatievariabelen en klassenvariabelen worden automatisch door de taal geïnitialiseerd indien de programmeur geen expliciete initialisatie voorziet. Instantiatievariabelen en klassenvariabelen van numerieke primitieve types hebben 0 als defaultwaarde. Instantiatievariabelen en klassenvariabelen van het primitieve type b o o l e a n zijn zonder expliciete initialisatie f a l s e . Instantiatievariabelen en klassenvariabelen van het type char worden zonder expliciete initialisatie op het NULkarakter (het symbool met interne code 0x0000) gezet. Instantiatievariabelen en klassenvariabelen die een klassentype hebben worden automatisch op null geïnitialiseerd (ze verwijzen naar geen object).
0.1.4.3.2
Expliciete initialisatie van instantiatievariabelen en van klassenvariabelen
Er zijn in Java twee mogelijkheden om instantiatievariabelen expliciet te initialiseren, en twee vergelijkbare mogelijkheden voor het initialiseren van klassenvariabelen. Ten eerste kan men een initiële waarde toekennen aan een instantiatievariabele of aan een klassenvariabele op de plaats van de declaratie (initialisatie-bij-declaratie). Dit gebeurt in Voorbeeld 30 met de klassenvariabele LOWER. Merk op dat deze expliciete initialisatie in dit geval overbodig is, omdat LOWER expliciet op de defaultwaarde wordt geïnitialiseerd. Ten tweede is het mogelijk om code los in de klasse te schrijven, zonder dat ze bij een methode hoort. Deze code wordt uitgevoerd tijdens initialisatie en wordt initialisatiecode genoemd. Indien de initialisatiecode geannoteerd is met het sleutelwoord static, wordt de code uitgevoerd tijdens de initialisatie van de klasse. Indien de code niet geannoteerd is, wordt ze uitgevoerd tijdens de initialisatie van ieder nieuw object. Deze laatste variant wordt erg weinig gebruikt. … final static private long LOWER = 7L; … … final static private long LOWER; static { LOWER = 7L; } … … private long $balance = 7L; … … private long $balance; { $balance = 7L; } … Voorbeeld 24: alternatieven voor initialisatie van variabelen
In Voorbeeld 24 worden de alternatieven om instantiatievariabelen en klassenvariabelen op 7 te initialiseren op een rijtje gezet. De eerste twee code fragmenten initialiseren een niet-wijzigbare klassenvariabele LOWER. Deze initialisatie wordt uitgevoerd wanneer de klasse wordt ingeladen. De eerste versie toont initialisatie-bij-declaratie, de tweede versie toont klasseninitialisatiecode. De twee laatste code fragmenten initialiseren een instantiatievariabele $balance op 7. In het eerste fragment wordt initialisatie-bijdeclaratie getoond; in het tweede instantiatieinitialisatiecode.
De initialisatiecode in een klasse wordt uitgevoerd in lexicale volgorde. Bij het aanmaken van een nieuw object wordt eerst de instantiatie-initialisatie-code (met inbegrip van de initialisatie-bij-declaratie) uitgevoerd, en daarna het lichaam van de constructor in kwestie. Initialisatie-bij-declaratie is eigenlijk een speciaal geval van initialisatiecode. De klassen-initialisatie-code wordt uitgevoerd bij het inladen van de klasse door de virtuele machine. In Java is het immers zo dat, de eerste keer dat een klasse gebruikt wordt, de virtuele machine ze zal zoeken en pas dan zal inladen. Bij het inladen wordt de klasse geïnitialiseerd door het uitvoeren van de klasseninitialisatiecode en de defaultinitialisaties. 9
Daarna wordt ze beschikbaar gesteld voor gebruik door het programma . De klasse blijft gedurende de ganse levensduur van de virtuele machine ingeladen. Er is geen mogelijkheid om een ingeladen klasse terug te ontladen.
0.1.4.3.3
Verdere initialisatie van instantiatievariabelen in constructoren
In hoofdstuk 0.1.2.1 werden de stappen besproken, die genomen worden bij het aanmaken van nieuwe objecten. In het licht van de initialisatie van instantiatievariabelen, moet dit constructieprotocol uitgebreid worden met een extra stap. Concreet verloopt de evaluatie van de uitdrukking new!ClassName(a1,!a2,!…,!an) in de volgende stappen: 1. Er wordt geheugen gereserveerd voor het bewaren van de interne toestand van een nieuw object van de betrokken klasse. De evaluatie van de uitdrukking zal falen indien geen geheugen meer beschikbaar is. 2. De instantiatie-initialisatie-code in de betrokken klasse wordt uitgevoerd, met inbegrip van de initialisatie-bij-declaratie. De initialisaties worden in lexicale volgorde uitgevoerd, met andere woorden in de volgorde waarin ze voorkomen in de definitie van de betrokken klasse. Deze extra stap in het aanmaken van een nieuw object zal er voor zorgen dat iedere instantiatievariabele voorzien wordt van een initiële waarde. De initialisatie gebeurt ofwel door een expliciete initialisatie in de declaratie van de betrokken variabele, ofwel door een expliciete initialisatie in de initialisatiecode, ofwel door een impliciete initialisatie. 3. Een constructor, waarvan de formele argumentenlijst overeenstemt met de actuele argumentenlijst uit de constructie-uitdrukking, wordt uitgevoerd. Meer in detail wordt gezocht naar een constructor waarvan ieder van de formele argumenten f i in type overeenstemt met het actuele argument a i. De Java-vertaler zal de uitdrukking niet aanvaarden, indien een dergelijke constructor niet voorhanden is. 9
Deze procedure kan veranderd worden door zelf een andere ClassLoader klasse te schrijven. Dat is echter een onderwerp dat voor dit boek te ver gaat.
4. Een referentie (verwijzing) naar het nieuwe object wordt teruggeven als resultaat van de uitdrukking. Deze referentie naar het aangemaakte object kan vervolgens toegekend worden aan een variabele. De taak van een constructor van een klasse bestaat er essentieel in de toestand van nieuwe objecten verder te initialiseren. In vele gevallen zullen de voorafgaande initialisaties van de diverse instantiatievariabelen niet voldoen. Zo kan het zijn dat de verzuimwaarde voor bepaalde instantiatievariabelen geen geldige betekenis hebben voor objecten van bepaalde klassen. Verder zal de initialisatie van nieuwe objecten vaak gebeuren op basis van actuele argumenten, die bij de constructie worden meegegeven. De implementatie van constructoren wordt geïllustreerd in Voorbeeld 30. Voor de eerste constructor wordt de instantiatievariabele $balance geïnitialiseerd op de meegegeven waarde, als die waarde binnen de vooropgestelde grenzen ligt. Merk op dat ook hier de expliciete vorm van toegang tot instantiatievariabelen mogelijk is. De initialisatie van de instantiatievariable binnen het lichaam van deze constructor had dus ook kunnen geschreven worden als this.$balance!=!initial;. Het lichaam van de tweede constructor is leeg. Omdat de instantiatievariabele $balance per definitie geïnitialiseerd wordt volgens defaultregels, zal bij constructie van een nieuwe bankrekening met behulp van deze constructor de balans automatisch op 0 staan. De tweede constructor uit Voorbeeld 30, die de defaultconstructor genoemd wordt omdat hij geen argumenten heeft, heeft een speciale betekenis in Java. Iedere klasse in Java wordt automatisch voorzien van een defaultconstructor met een lege implementatie. Deze defaultconstructor zal met andere woorden bij het aanmaken van nieuwe objecten van de klasse de expliciete of impliciete initialisatie van iedere instantiatievariabele ongewijzigd laten. Deze automatische constructor vervalt echter van zodra een klasse minstens één constructor expliciet introduceert. We noemen hem dan ook de default-defaultconstructor: we verzuimen (1) expliciet constructoren in te voeren, en dan krijgen we een constructor die argumenten verzuimt (2). Voor de klasse van bankrekeningen kunnen bijgevolg de volgende gevallen onderscheiden worden: — Als in de klasse van bankrekeningen geen enkele constructor gedefinieerd wordt, geldt de default-defaultconstructor. De balans van iedere rekening zal op 0 geïnitialiseerd worden, voor zover geen expliciete initialisatie ingevoerd wordt voor de instantiatievariabele $balance. — Als in de klasse van bankrekeningen minstens één andere constructor gedefinieerd wordt, vervalt de default-defaultconstructor als een manier om bankrekeningen aan te
maken. In de klasse van bankrekeningen kan bijvoorbeeld een constructor worden ingevoerd, waarbij een initiële waarde voor de balans als argument wordt meegegeven. — Naast andere constructoren, kan in de klasse van bankrekeningen een defaultconstructor expliciet gedefinieerd worden. Als de balans van dergelijke rekeningen op 0 geïnitialiseerd moet worden, zal het lichaam van deze constructor leeg zijn op voorwaarde dat geen expliciete initialisatie van de instantiatievariabele $balance gebruikt wordt. De constructor moet in dit geval echter expliciet worden ingevoerd, omdat de klasse van bankrekeningen daarnaast nog andere constructoren introduceert.
0.1.4.3.4
Initialisatie van lokale variabelen en van formele argumenten
In Java moeten lokale variabelen en formele argumenten voorzien worden van een initiële waarde, vooraleer ze kunnen geïnspecteerd worden. De Java-vertaler weigert met name code waarin een lokale variabele wordt geïnspecteerd, indien hij er niet zeker van kan zijn dat er voordien een waarde aan de variabele werd toegekend. De vertaler is hier vrij strikt. Er zijn gevallen waar het voor ons duidelijk is dat de variabele zeker een waarde toegewezen gekregen heeft, maar die te complex zijn voor de vertaler om te herkennen. Dit komt voor indien de toekenning gebeurt in één tak van een conditionele structuur (meestal try … catch …, zie Error! Reference source not found.), en niet in de andere, waarbij wij zien dat die ene tak zeker uitgevoerd zal worden, maar de vertaler niet. De initiële waarde voor een lokale variabele kan meteen worden vastgelegd in de declaratie zelf. Dit wordt in Voorbeeld 25 geïllustreerd voor de gehele variabele x. De eerste waarde voor een lokale variabele moet echter niet noodzakelijk in de declaratie zelf worden vastgelegd. De initiële waarde voor een lokale variabele kan bijvoorbeeld ook vastgelegd worden in een toekenning. Zo wordt in Voorbeeld 25 de initiële waarde voor de variabele y pas bepaald in een toekenning verderop in de code. Java verbiedt enkel inspecties van de variabele y in instructies tussen zijn declaratie en de toekenning van de initiële waarde. public void someMethod() { int x=10; int y; … // No inspections of the variable y in this part y = x; … } Voorbeeld 25: initialisatie van lokale variabelen
Voor de formele argumenten van methodes stelt zich het initialisatieprobleem niet. Zij hebben steeds een waarde, via de parameterbinding bij iedere toepassing van de methode.
0.1.4.4
De selectieopdracht
In programmeercursussen die het programmeren aanleren via niet-objectgerichte talen volgt men sinds dertig jaar een zelfde, geslaagde didactiek. Men stelt dat programmeren draait rond vier hoofdconcepten: sequentie, selectie, iteratie en subroutines. Sequentie wijst erop dat een computer programma’s uitvoert door commando’s in volgorde uit te voeren. Selectie maakt het mogelijk dat de computer sommige dingen doet en andere niet, gegeven de omstandigheden. Selectiestructuren in de programmatekst maken alternatieve uitvoeringspaden mogelijk. Iteratie zorgt ervoor dat je niet alles wat de computer moet doen, letterlijk moet opschrijven. Met iteratieve structuren wordt het mogelijk de computer een zelfde programmatekst zo vaak als nodig te laten uitvoeren, eventueel met variaties die afhangen van de situatie tijdens een bepaalde iteratiestap. Subroutines maken het mogelijk gelijkende code te generaliseren tot één programmatekst. Deze technieken blijven gelden in het objectgericht programmeren, maar ze verliezen hun status aan concepten als inkapseling, objectstructuur en objectinteractie, overerving, polymorfisme en dynamische binding. Men kan zeggen dat de nadruk verschuift van het temporele karakter van programmeren naar het ruimtelijke. Sequentie wordt minder belangrijk omdat routines in een objectgericht programma klassiek veel korter worden dan in een programma geschreven in een procedurele taal. Een methode met meer dan tien lijnen code is al uitzonderlijk. Selectiestructuren worden zoveel mogelijk vervangen door stabielere dynamische binding. Het vervangen van iteraties door ingekapselde verzamelingstructuren is iets wat pas recent begint door te breken. Het gebruiken van subroutines blijft erg belangrijk, maar wordt een bijproduct van inkapseling van data en gedrag in klassen. Sequentie, conditie en iteratie blijven echter de hoofdbouwstenen van programmerenin-het-klein, binnen een methode. Tot nu toe hebben we in het boek enkel over methodeoproepen en sequentie gepraat. In dit hoofdstuk zullen we selectiestructuren in Java bespreken. Iteratieve structuren worden besproken in hoofdstuk Error! Reference source not found., “Error! Reference source not found.”, op pagina Error! Bookmark not defined..
0.1.4.4.1
Enkelvoudige en dubbele selectieopdrachten
Java biedt een enkelvoudige selectiestructuur in de vorm van een if-opdracht, en een dubbele selectiestructuur in de vorm van een if-else-opdracht. In beide vormen gebeurt de
selectie op basis van een booleaanse uitdrukking. Deze controlerende uitdrukking moet steeds tussen haakjes staan. Een selectieopdracht omvat verder een then-opdracht, die uitgevoerd wordt indien de evaluatie van de controlerende uitdrukking resulteert in de booleaanse waarde true. In een if-else-opdracht wordt verder een else-opdracht voorzien, die uitgevoerd wordt als de evaluatie van de controlerende uitdrukking resulteert in de booleaanse waarde false. Bij een if-opdracht zal onmiddellijk worden overgegaan naar de volgende opdracht, indien de evaluatie van de controlerende uitdrukking resulteert in de booleaanse waarde false. De then-opdracht en de else-opdracht kunnen een enkelvoudige instructie zijn of een blok (een lijst van instructies en declaraties omgeven door {…}). Voorbeeld 26 geeft aan hoe op basis van een resultaat op 20, de graad voor een bepaald examen kan bepaald worden. Let op de indentatie van de code. Geneste if-else-opdrachten worden typisch niet geïndenteerd, omdat hierdoor een al te diepe indentatie zou bekomen worden, waardoor erg weinig ruimte overblijft op de lijn. Indentatie blijft echter grotendeels een geval van persoonlijke smaak. Belangrijk is dat een bepaalde stijl consequent doorheen een volledig programma wordt toegepast. // Assume the value of result is in the range 0..20. if (result >= 18) System.out.println("AAA"); else if (result >= 16) System.out.println("AA"); else if (result >= 14) System.out.println("A"); else if (result >= 12) System.out.println("B"); else System.out.println("M"); Voorbeeld 26: voorbeeld van if-opdracht en if-else-opdracht
Bij het nesten van if-else-opdrachten en if-opdrachten kan het onduidelijk zijn met welke if-opdracht een bepaalde else-opdracht moet geassocieerd worden. Dit probleem wordt vaak het dangling else-probleem genoemd. In het eerste deel van Voorbeeld 27 zijn principieel 2 interpretaties mogelijk. Een if-opdracht met een geneste if-else-opdracht, of een if-else-opdracht met een geneste if-opdracht. Merk op dat het eerste en het tweede codefragment volledig equivalent zijn. De indentatie is totaal irrelevant voor de manier waarop de vertaler de code interpreteert. Java stelt dat een else-opdracht steeds met de if-opdracht geassocieerd wordt die er het dichtst bij staat. In het voorbeeld hoort de else-opdracht dus niet bij de buitenste if-opdracht zoals wellicht door de indentatie van het eerste voorbeeld gesuggereerd wordt, maar zal de else-opdracht geassocieerd wordt met de meest naar binnen gelegen if-opdracht. Om de else-opdracht daadwerkelijk te associëren met de buitenste if-opdracht, moet de geneste if-
opdracht omsloten worden in een samengestelde opdracht, zoals geïllustreerd in het derde codefragment van Voorbeeld 27. Het blijkt dat zulke verwarring opnieuw een belangrijke bron is van programmeerfouten, die dan ook nog eens door mensen zeer moeilijk te vinden zijn. Daarom raden we aan steeds de then- en de optionele else-tak door {…}-symbolen te omsluiten, ook als deze takken slechts één opdracht omvatten. Het blijkt immers dat later vaak nog instructies worden toegevoegd, en dat dan de accolades vaak vergeten worden. De twee laatste codefragmenten tonen de twee alternatieven in de aangeraden versie. if (x > 5) if (y > 5) System.out.println("x & y are > 5"); else System.out.println("x is <= 5"); if (x > 5) if (y > 5) System.out.println("x & y are > 5"); else System.out.println("y is <= 5"); if (x > 5) { if (y > 5) System.out.println("x & y are > 5"); } else System.out.println("x is <= 5"); if (x > 5) { if (y > 5) { System.out.println("x & y are > 5"); } else { System.out.println("y is <= 5"); } } if (x > 5) { if (y > 5) { System.out.println("x & y are > 5"); } } else { System.out.println("x is <= 5"); } } Voorbeeld 27: het dangling-else-probleem in geneste if-opdrachten
In Voorbeeld 30 wordt de selectiestructuur gebruikt om ervoor te zorgen dat de mutatoren en de constructor met een argument de balans van de rekeningen niet buiten de
opgelegde grenzen zouden brengen. Indien dit wel zo zou zijn, gebeurt er eenvoudig niets. In hoofdstukken Error! Reference source not found. en Error! Reference source not found. worden alternatieven aangeboden om met deze uitzonderlijke omstandigheden rekening te houden.
0.1.4.4.2 Meervoudige selectieopdrachten Naast enkelvoudige selectieopdrachten ondersteunt Java ook m e e r v o u d i g e selectieopdrachten in de vorm van een switch-opdracht. In een enkelvoudige selectieopdracht wordt, op basis van een logische uitdrukking, gekozen tussen de uitvoering van opdrachten in de then-tak of in de else-tak. In een meervoudige selectieopdracht kunnen een willekeurig aantal takken voorzien worden waarmee de uitvoering kan voortgezet worden. De keuze van een tak gebeurt in dergelijke gevallen op basis van een gehele waarde. In Voorbeeld 26 werd een stuk code uitgewerkt waarmee de graad die bij een examen behaald werd, uitgeschreven wordt op de standaard uitvoerstroom, op basis van een resultaat op 20. Geneste if-else-opdrachten werden gebruikt om de uit te schrijven tekst te bepalen. Voorbeeld 28 toont hoe hetzelfde effect bereikt kan worden met behulp van een switch-opdracht. // Assume the value of result is in the range 0..20. switch (result) { case 20: case 19: case 18: System.out.println("AAA"); break; case 17: case 16: System.out.println("AA"); break; case 15: case 14: System.out.println("A"); break; case 13: case 12: System.out.println("B"); break; default: System.out.println("M"); } Voorbeeld 28: voorbeeld van een switch-opdracht
De switch-opdracht in Java omvat een controlerende uitdrukking aangevuld met de definitie van een aantal takken waarmee de uitvoering kan verder gezet worden. Eén van deze takken kan een defaulttak zijn.
— Het type van de controlerende uitdrukking moet enumereerbaar zijn. In Java betekent dit dat enkel uitdrukkingen van het type char, byte, short, int of long toegelaten zijn. Programmeertalen zoals Pascal en Ada ondersteunen zelfgedefinieerde enumeratietypes, en in die talen kan in meervoudige selectieopdrachten ook van deze types gebruik gemaakt worden. De controlerende uitdrukking volgt op het sleutelwoord switch en moet steeds tussen haakjes staan. — De definitie van een tak uit een meervoudige selectieopdracht begint in Java steeds met het sleutelwoord case, gevolgd door de waarde waarvoor de betrokken tak moet geselecteerd worden. Voor deze waarden kunnen enkel constanten gebruikt worden. Het type van iedere waarde moet overeenstemmen met het type van de controlerende uitdrukking. De definitie van een tak kan al dan niet afgesloten worden met een opdracht die moet uitgevoerd worden indien de betrokken tak geselecteerd wordt tijdens de uitvoering van de switch-opdracht. De volgorde waarin takken binnen een switchopdracht gedefinieerd worden, is totaal willekeurig. Wel kunnen er nooit twee takken zijn met dezelfde waarde. — Naast takken waarvoor een expliciete waarde wordt opgegeven, kan een switchopdracht in Java voorzien worden van een defaulttak. De definitie van deze tak begint met het sleutelwoord default, en wordt onmiddellijk gevolgd door een opdracht die bij selectie van deze tak moet uitgevoerd worden. Binnen een switch-opdracht kan hoogstens één defaulttak voorzien worden. De uitvoering van een switch-opdracht begint met de evaluatie van de controlerende uitdrukking. De resulterende waarde bepaalt het punt waar verder zal gegaan worden met de uitvoering van het programma: — Indien de resulterende waarde overeenstemt met de waarde, die in één van de takken expliciet werd opgegeven, wordt de uitvoering verder gezet vanaf die tak. Beschouw in Voorbeeld 28 het geval waar result de waarde 15 heeft. De uitvoering zal in dit geval verder gezet worden vanaf case 15:. — Indien de resulterende waarde niet overeenstemt met een waarde, die in één van de takken expliciet werd opgegeven, zal de uitvoering verder gezet worden vanaf de defaulttak, voor zover deze aanwezig is. Beschouw in Voorbeeld 28 het geval waar result de waarde 6 heeft. De uitvoering zal in dit geval verder gezet worden vanaf de defaulttak. — Indien de resulterende waarde niet overeenstemt met een waarde, die in één van de takken expliciet werd opgegeven, en er geen defaulttak voorzien is, zal de uitvoering verder gezet worden met de opdracht die volgt op de switch-opdracht. Vanaf het punt waarnaar gesprongen werd, gaat de uitvoering gewoon verder tot het einde van de switch-opdracht. De hoofdingen van andere takken, die de uitvoering
onderweg nog tegenkomt, worden genegeerd. De opdrachten, die deel uitmaken van deze volgende takken, worden wél uitgevoerd. Een break-opdracht kan gebruikt worden om voortijdig een switch-opdracht te verlaten. Beschouw in Voorbeeld 28 het geval waar result de waarde 15 heeft. De uitvoering wordt verder gezet vanaf de tak, die expliciet voorzien werd voor het behandelen van deze waarde. Met deze tak is geen opdracht geassocieerd, zodat onmiddellijk verder gegaan wordt met het uitvoeren van de opdracht uit de volgende tak (case 14:). “A” wordt vervolgens uitgeschreven op de standaard uitvoerstroom. Daarna wordt, tengevolge van de break-opdracht, de switch-opdracht verlaten. Zonder de break-opdracht zou verder gegaan worden met het uitvoeren van de opdracht in de volgende tak (case 13:), … In het onderhavig geval geven we de voorkeur aan de versie met de geneste if-elseopdrachten. Die versie is makkelijker omdat de orde belangrijk is in het beslissingsproces. De meervoudige selectieopdracht is enkel zinnig in gevallen waar de waarden gebruikt worden als toevallige etiket voor niet-geordende concepten. In talen die het ondersteunen zou daarvoor een expliciet enumeratietype worden ingevoerd. In objectgerichte talen is de nood voor zulke enumeratietypes veel kleiner dan in procedurele talen. In procedurele talen worden zulke types meestal gebruikt in variantrecord-structuren om types te onderscheiden. Een record-structuur Vehicle heeft dan een veld van het type kind. Dat type is dan gedefinieerd als de verzameling {car, truck, bicycle, boat, …}. Elementen van die verzameling kunnen meestal enkel vergeleken worden. In een objectgerichte taal worden zulke structuren gebouwd met verschillende klassen in een overervingsstructuur. De selectiestructuren die gedrag variëren aan de hand van de waarde van het kind-veld in het voorbeeld zijn vaak meervoudige selectieopdrachten. Het zijn net deze praktijken die procedurele software zo onstabiel maken. In een objectgerichte taal wordt de gedragsvariatie bekomen door dezelfde methode een verschillend effect te geven in de verschillende klassen Vehicle, Car, Truck, Bicycle, Boat… Selectie gebeurt dan door middel van dynamische binding (zie XXXX). De conclusie is dat in een objectgerichte taal de meervoudige selectieopdracht niet vaak gebruikt wordt. Hoe, in de beperkte gevallen waar het nog zinnig is, in Java met enumeratietypes wordt omgegaan, wordt besproken in REFERENTIEREFERENTIE.
0.1.4.5
Implementatie van complexe methodes
De meer complexe methodes uit de klasse der bankrekeningen zijn de methodes om geld te storten op een rekening en af te halen van een rekening. De meest complexe
methode is zonder twijfel de methode om geld over te schrijven van de ene bankrekening naar een andere bankrekening. In de specificatie van deze methode werden een vrij groot aantal randgevallen omschreven. In de specificatie van de methode transferTo, zoals ze uitgewerkt werd in Voorbeeld 6, werd aangegeven dat de toestand van beide bankrekeningen onveranderd blijft in ieder van de randgevallen. In de implementatie moet uiteraard rekening gehouden worden met al deze mogelijke randgevallen. Een eerste mogelijke implementatie van de methode transferTo wordt geïllustreerd in Voorbeeld 29. In de implementatie worden de instantiatievariabelen van de betrokken rekeningen direct gemanipuleerd. Zowel de controles als de veranderingen aan de balansen van beide rekeningen worden gerealiseerd door direct in te werken op de betrokken instantiatievariabelen. Bij de controle op de resulterende balans van beide rekeningen, wordt daarenboven rechtstreeks gebruik gemaakt van de interne constanten LOWER en UPPER. Het lichaam van de methode transferTo is min of meer een combinatie van de lichamen van de methodes withdraw en deposit, zoals ze uitgewerkt werden in Voorbeeld 30. Merk op hoe controles op het overlopen van de balansen van beide rekeningen werden uitgewerkt. Voor de direct betrokken rekening moet nagegaan worden of de huidige balans verminderd met het over te schrijven bedrag, niet beneden de kleinst mogelijke waarde voor de balans komt te liggen. In deze controle mag zelf geen overloop voorkomen. Om die reden wordt in de controle nagegaan of de huidige balans niet kleiner is dan de kleinst mogelijke waarde voor de balans vermeerderd met het over te schrijven bedrag. Omdat we weten dat de kleinst mogelijke waarde niet positief kan zijn, en omdat het over te schijven bedrag op het punt van de controle gegarandeerd positief is, kan het resultaat van de optelling nooit buiten het bereik van het type long komen te liggen. Een gelijkaardige redenering geldt voor de controle omtrent de aanpassing aan de balans van de doelrekening. public void transferTo(long amount, Account destination) { if ( (amount > 0) && (destination != null) ) { if ( ($balance >= LOWER + amount) && (destination.$balance <= UPPER – amount) ) { this.$balance -= amount; destination.$balance += amount; } } } Voorbeeld 29: directe implementatie van de methode transferTo
In hoofdstuk 0.1.4.1 werd reeds aangegeven dat het effect van de methode transferTo bekomen wordt door een afhaling toe te passen op de direct betrokken rekening, gevolgd door een storting op de doelrekening. In de implementatie van de
methode transferTo wordt daarom beter gebruik gemaakt van de methodes deposit en withdraw. Een dergelijke implementatie werd uitgewerkt in Voorbeeld 30. Omdat de methodes withdraw en deposit gedefinieerd werden als totale methodes, moeten op één of andere manier de randgevallen onderscheiden worden van het normale geval. In de implementatie van de mutator transferTo uit Voorbeeld 30, worden alle controles doorgevoerd in het begin van het lichaam van de methode. Merk op dat ook in de uitwerking van deze controles, niet langer direct ingewerkt wordt op de instantiatievariabele $balance, noch op de constanten UPPER en LOWER. Hierdoor wordt de implementatie van de methode veel minder direct afhankelijk van de representatie. Veranderingen aan de manier waarop informatie omtrent bankrekeningen intern wordt bijgehouden, hebben niet langer een invloed op de implementatie van de methode transferTo. Directe implementatie van alle methodes van een klasse is over het algemeen af te raden. Het is haast altijd een betere strategie om complexere methodes te implementeren in functie van eenvoudigere, primitievere methodes. Het objectgericht programmeren streeft immers naar een maximaal hergebruik van reeds ontwikkelde software. Omdat een overschrijving principieel een combinatie is van een afhaling en een storting, wordt ze best op die manier gerealiseerd. Het objectgericht programmeren streeft daarenboven naar een maximale aanpasbaarheid. Zo is het mogelijk dat de methode voor het afhalen van geld later moet uitgebreid worden met het aanrekenen van kosten ten laste van de klant. Een directe implementatie van een de methode transferTo zou dan op dezelfde manier moeten aangepast worden, terwijl in de implementatie van Voorbeeld 30 de aanpassing enkel moet doorgevoerd worden in het lichaam van de methode withdraw zelf. De aangepaste versie wordt dan meteen ook gebruikt in transferTo. Een implementatie van complexere methodes in functie van primitieve methodes is aangewezen wanneer herbruikbaarheid en aanpasbaarheid belangrijke criteria zijn. Enkel in zeer uitzonderlijke omstandigheden, waarin (tijds)efficiëntie ten koste van alles moet worden nagestreefd, kan een directe implementatie van complexere methodes overwogen worden. Bij een directe implementatie zal immers de overhead bij het toepassen van methodes vermeden worden. De richtlijn in verband met de implementatie van complexere methodes zal verder nog aangescherpt worden. In het derde deel van deze tekst zal blijken dat het realiseren van complexe methodes in termen van meer primitieve methodes ook grote voordelen biedt bij overerving. De implementatie van de methode t r a n s f e r T o uit Voorbeeld 30 is eerder kunstmatig, en zeker geen schoolvoorbeeld van goede objectgerichte code. De belangrijkst
reden hiervoor ligt in de beslissing om de methodes deposit en withdraw te definiëren als totale methodes. Op die manier wordt het moeilijk een onderscheid te maken tussen het normale geval, waarin de transactie effectief doorgevoerd wordt, en de randgevallen, waarin de transactie geweigerd wordt. In hoofdstuk CONTRACT_REF, " CONTRACT_REF" en in hoofdstuk ROBUST_REF, " CONTRACT_REF", zullen versies worden uitgewerkt, die op dat vlak beter scoren. Het feit dat totale versies voor methodes uit de klasse van bankrekeningen niet de meest aangewezen strategie zijn, betekent hoegenaamd niet dat deze aanpak in alle gevallen moet vermeden worden. Zo is een totale versie van een methode om een element te verwijderen uit een gegevensstructuur zoals een lijst of een vector, perfect aanvaardbaar. De methode zal zodanig gedefinieerd worden dat ze zowel kan toegepast worden op structuren die het element bevatten, als op structuren die het element niet bevatten. In het laatste geval zal de methode geen toestandsverandering te weeg brengen.
0.1.4.6
Volledige definitie van de klasse van bankrekeningen
In Voorbeeld 30 wordt de volledige definitie van de klasse van bankrekeningen getoond, zoals ze tot hiertoe werd uitgewerkt. Merk op dat aspecten van specificatie en van implementatie verweven zitten in de totale definitie. /** * A class for dealing with bank accounts, involving the available * amount of money (the balance of the account), and a lower limit * and an upper limit for the balance. * * The lower limit for the balance of an account is always negative; * the upper limit for its balance is always positive. * * The balance of a bank account is always greater than or equal * to the lower limit for its balance and less than or equal to * the upper limit for its balance. Notice that zero is always an * acceptable value for the balance of an account. */ public class Account { /** * Initialize this new account with as balance. The * lower limit and the upper limit for the balance of this new * account are initialized according to the policy of the bank. * If is not within this range, the balance of this * new account is initialized to zero. */ public Account(long initial) { if ( (initial >= getLowerLimit()) && (initial <= getUpperLimit()) ) { $balance = initial;
} } /** * Initialize this new account with a zero balance. The lower * limit and the upper limit for the balance of this new account * are initialized according to the policy of the bank. */ public Account() { // NOP } /** * Deposit to this account, if the given amount is * positive, and if the resulting balance would not be larger * than the upper limit for the balance of this account. */ public void deposit(long amount) { if (amount > 0) { if ($balance <= getUpperLimit() - amount) { $balance += amount; } } } /** * Withdraw from this account, if the given amount is * positive and if the resulting balance would not be smaller * than the lower limit for the balance of this account. */ public void withdraw(long amount) { if (amount > 0) { if ($balance >= getLowerLimit() + amount) { $balance -= amount; } } } /** * Transfer from this account to <destination>, if the * given amount is positive, if <destination> is effective, if * the resulting balance of this account would not be smaller than * the lower limit for its balance, and if the resulting balance * of <destination> would not be larger than the upper limit for * its balance. */ public void transferTo(long amount, Account destination) { if ( (amount > 0) && (destination != null) ) { if ( (this.getBalance() >= this.getLowerLimit() + amount) && (destination.getBalance() <= destination.getUpperLimit() – amount) ) { this.withdraw(amount); destination.deposit(amount); } } }
/** * Return the balance of this account. */ public long getBalance() { return $balance; } /** * Return the lower limit for the balance of this account. */ public long getLowerLimit() { return LOWER; } /** * Return the upper limit for the balance of this account. */ public long getUpperLimit() { return UPPER; } /** * The lower limit for the balance of each account. */ final static private long LOWER = 0L; /** * The upper limit for the balance of each account. */ final static private long UPPER = Long.MAX_VALUE; /** * The balance of this account. */ private long $balance; } Voorbeeld 30: specificatie en implementatie van de klasse van bankrekeningen
Gebruikersdocumentatie die afgeleid wordt uit deze programmatekst door een programma als javadoc zou er kunnen uitzien als in Voorbeeld 31. De vorm die hier wordt voorgesteld, lijkt slechts van ver op HTML pagina’s die gegenereerd worden door javadoc. Er is veel mogelijk met deze techniek, en we vinden dat javadoc absoluut niet ver genoeg gaat. De bedoeling van het voorbeeld is echter enkel om aan te lezer te tonen welke informatie de gebruiker heeft, en welke niet. De gebruiker zou zonder enige ambiguïteit met de klasse van bankrekeningen moeten kunnen werken op basis van deze informatie. Al wat weggelaten is, behoort tot het domein van de implementatie van de klasse.
public class Account A class for dealing with bank accounts, involving the available amount of money (the balance of the account), and a lower limit and an upper limit for the balance. The lower limit for the balance of an account is always negative; the upper limit for its balance is always positive. The balance of a bank account is always greater than or equal to the lower limit for its balance and less than or equal to * the upper limit for its balance. Notice that zero is always an acceptable value for the balance of an account.
Constructors •
Account(long initial) Initialize this new account with initial as balance. The lower limit and the upper limit for the balance of this new account are initialized according to the policy of the bank. If initial is not within this range, the balance of this new account is initialized to zero.
•
Account() Initialize this new account with a zero balance. The lower limit and the upper limit for the balance of this new account are initialized according to the policy of the bank.
Instance methodes •
public void deposit(long amount) Deposit amount to this account, if the given amount is positive, and if the resulting balance would not be larger than the upper limit for the balance of this account.
•
public void withdraw(long amount) Withdraw amount from this account, if the given amount is positive and if the resulting balance would not be smaller than the lower limit for the balance of this account.
•
public void transferTo(long amount, Account destination) Transfer amount from this account to destination, if the given amount is positive, if destination is effective, if the resulting balance of this account would not be smaller than the lower limit for its balance, and if the resulting balance of destination would not be larger than the upper limit for its balance.
•
public long getBalance() Return the balance of this account.
•
public long getLowerLimit() Return the lower limit for the balance of this account.
•
public long getUpperLimit() Return the upper limit for the balance of this account. Voorbeeld 31: een mogelijke vorm voor de gebruikersdocumentatie voor de klasse van bankrekeningen
0.1.5
Verificatie
Zodra de specificatie en de implementatie van een klasse voltooid zijn, kan de correctheid van de klasse geverifieerd worden. Los van de concrete strategie, is de doelstelling van verificatie na te gaan of de implementatie van een klasse precies datgene realiseert wat in de specificatie werd aangegeven. In concrete verificatiestrategieën onderscheidt men methodes voor statische verificatie en methodes voor dynamische verificatie. In statische verificatie zal men, enkel en alleen op basis van de definitie van de klasse, proberen aan te tonen dat de implementatie conform is met de specificatie. In statische verificatie zal bijgevolg geen uitvoering plaatsvinden van de methodes die aangeboden worden door de betrokken klasse. Een eenvoudig voorbeeld van statische verificatie is een kritische studie van de definitie van de klasse, bij voorkeur door een andere software ingenieur dan degene die de definitie heeft uitgewerkt (code-inspectie). Een meer extreme strategie is het formeel bewijzen dat de implementatie realiseert wat in de specificatie wordt vooropgesteld. Deze aanpak vergt uiteraard een formele definitie van de specificatie van een klasse, gekoppeld aan een formele definitie van de gehanteerde programmeertaal. In hoofdstuk Error! Reference source not found., “Error! Reference source not found.”, zal iets dieper worden ingegaan op technieken en methodes voor het statisch verifiëren van definities van klassen, of onderdelen ervan. De geïnteresseerde lezer wordt verwezen naar de literatuur in verband met het gestructureerd programmeren. In dynamische verificatie zal men, op basis van resultaten die bekomen worden door het concreet gebruik van de klasse, proberen aan te tonen dat de specificatie overeenstemt met de implementatie. In dynamische verificatie wordt gewerkt met de vertaalde, uitvoerbare vorm van een klasse enerzijds, en met de definitie van de klasse anderzijds. Dynamische verificatie wordt vaak het testen van een stuk software genoemd. Het heeft als eigenschap dat het enkel het bestaan van fouten kan aantonen. Met behulp van dynamische verificatie zal men nooit kunnen concluderen dat een stuk software volledig vrij is van fouten. Het testen van software vergt een weldoordacht plan, een weldoordachte strategie om de diverse methodes zo systematisch mogelijk uit te proberen. In dit hoofdstuk zal de zwarte-doos-teststrategie (black box testing) geïntroduceerd worden, als één van de mogelijke aanpakken in de context van dynamische verificatie van software. Een andere strategie voor dynamische verificatie, met name de witte-doosteststrategie wordt in het tweede deel van deze tekst uitvoerig behandeld.
0.1.5.1
De zwarte-doos-teststrategie
Onder de strategieën voor het testen van software systemen, of onderdelen ervan, is de zwarte-doos-teststrategie wellicht één van de eenvoudigste technieken. Binnen deze strategie wordt een softwaresysteem, of een onderdeel ervan beschouwd als een zwarte doos, waarvan de interne werking niet gekend is. De doos wordt uitgetest door na te gaan of de geteste eenheid naar behoren reageert op invoer die aangeboden worden. Deze strategie wordt veelvuldig gebruikt in de elektronica voor het uittesten van chips. Vanuit diverse signalen aan de invoerpoorten van de chip, zal gemeten worden of de juiste signalen verschijnen aan de uitvoerpoorten. Binnen de context van de objectgerichte software ontwikkeling, is deze strategie toepasbaar op het testen van klassen. Wanneer een klasse benaderd wordt als een zwarte doos, zal enkel en alleen haar specificatie gebruikt worden. Details omtrent de implementatie van de klasse worden niet gebruikt in de testen. Het testplan wordt opgesteld enkel en alleen op basis van de specificatie van de diverse methodes binnen de klasse. Dit betekent dat het testplan opgesteld kan worden zodra de specificatie van de betrokken klasse vast ligt, nog voor de implementatie klaar is. Indien in de specificatie van een methode geen speciale gevallen onderscheiden worden, zal het volstaan de methode uit te proberen met één enkel stel actuele argumenten. Merk op dat niets verhindert om in de implementatie van de methode dit onderscheid alsnog te maken. Fouten in één van beide paden kunnen dan over het hoofd gezien worden in een zwarte-doos-test. In de specificatie van de mutatoren withdraw, deposit en transferTo, en van de constructor met een argument uit de klasse van bankrekeningen wordt een onderscheid gemaakt tussen diverse gevallen. Dit betekent dat we meerdere testgevallen moeten onderscheiden. Zo wordt in de specificatie van de mutator withdraw onderscheid gemaakt tussen het normale geval, het geval waarin het af te halen bedrag niet positief is, en het geval waarin de resulterende balans beneden de laagst mogelijke waarde komt te liggen. Voor ieder van deze gevallen zal volgens het principe van de zwarte-doosteststrategie een test worden uitgewerkt.
0.1.5.1.1
Zwarte-doos-test: naïeve versie
Een eerste mogelijk testprogramma wordt geïllustreerd in Voorbeeld 32. In deze naïeve versie wordt na het toepassen van een constructor of een mutator telkens de toestand van de betrokken bankrekening(en) uitgeschreven. Na het toepassen van een inspector wordt de resulterende informatie uitgeschreven. Het testprogramma wordt ontwikkeld als een
hoofdmethode, die deel uitmaakt van een afzonderlijke klasse. Het spreekt voor zich dat de klasse van bankrekeningen zelf niet zal bevuild worden met testcode. Het principe van zwarte-doos testen wordt eerst en vooral geïllustreerd aan de hand van de defaultconstructor. Omdat in de specificatie van deze constructor geen onderscheid gemaakt wordt tussen verschillende gevallen, volstaat één enkele test. Na de constructie van een nieuwe bankrekening met behulp van de defaultconstructor, wordt de toestand van de nieuwe bankrekening uitgeschreven op de standaard uitvoerstroom. Voor het testen van inspectoren, wordt de betrokken inspector toegepast op een bestaand object. Vervolgens wordt het resultaat van de inspector uitgeschreven op de standaard uitvoerstroom. In Voorbeeld 32 wordt op die manier een test uitgewerkt voor de inspector getBalance. Gelijkaardige testen kunnen uitgewerkt worden voor de inspectoren getLowerLimit en getUpperLimit. Voor het testen van mutatoren wordt de betrokken inspector toegepast op bestaande objecten. Vervolgens zal de toestand van alle betrokken objecten uitgeschreven worden op de standaard uitvoerstroom. In Voorbeeld 32 worden drie testen uitgewerkt voor de mutator w i t h d r a w , omdat in de specificatie van deze methode drie gevallen kunnen onderscheiden worden. In de eerste test wordt de goede werking van het normale geval gecontroleerd, waarin de afhaling effectief wordt doorgevoerd. In een tweede test wordt ingegaan op het geval waarin het af te halen bedrag negatief is. In dit geval moet de toestand van de betrokken rekening onveranderd blijven. In een laatste test wordt het geval beschouwd waarin de resulterende balans beneden de laagst mogelijke waarde zou komen. Ook in dat geval moet de toestand van de betrokken bankrekening onveranderd blijven. Gelijkaardige testen kunnen uitwerkt worden voor de mutatoren d e p o s i t en transferTo. public class NaiveTest { static public void main(String argv[]) { // Default constructor (single case) Account myAccount = new Account(); System.out.println(myAccount); // Inspector getBalance myAccount = new Account(1000); System.out.println(myAccount.getBalance()); // Mutator withdraw // - normal case: acceptable amount myAccount = new Account(10000); myAccount.withdraw(5000); System.out.println(myAccount); // - illegal case: negative amount myAccount.withdraw(-3000); System.out.println(myAccount);
// - illegal case: resulting balance below lower limit myAccount.withdraw(10000); System.out.println(myAccount); … } } Voorbeeld 32: naïeve zwarte-doos-test voor de klasse van bankrekeningen
Het grote nadeel van deze aanpak is het gebruik van de defaultimplementatie van de transformatie van een bankrekening naar een tekstuele voorstelling met behulp van de methode toString. Deze voorstelling geeft weinig of geen informatie omtrent de toestand van de betrokken bankrekening. Zoals beschreven werd in hoofdstuk 0.1.2.4.1, schrijft de defaultimplementatie van toString enkel uit dat het object behoort tot de klasse van bankrekeningen, samen met de hash-waarde van de betrokken bankrekening. De defaultimplementatie moet immers algemeen genoeg zijn om toepasbaar te zijn op eender welk object. Het is bijgevolg niet realistisch te verwachten dat bij het uitschrijven van de toestand van een bankrekening informatie zal gegeven worden over de balans ervan. Indien de methode toString voorzien wordt van een eigen implementatie in de klasse van bankrekeningen, kan uiteraard wel rekening gehouden worden met de specifieke kenmerken van de objecten van deze klasse. In hoofdstuk 0.1.5.1.2 zal aangegeven worden dat ook in dat geval de concrete uitwerking van deze zwarte-doos-test uit Voorbeeld 32 verre van optimaal is.
0.1.5.1.2
Zwarte-doos-test: professionele versie
In plaats van de toestand van objecten uit te schrijven waarop constructoren en mutatoren worden toegepast, bestaat een betere strategie erin na iedere constructie en na iedere mutatie, de toestand van het betrokken object te inspecteren, of althans dat deel dat in de geteste methode aan bod komt. Deze aanpak wordt geïllustreerd in Voorbeeld 33. In plaats van de resultaten van de inspecties uit te schrijven op de standaard uitvoerstroom, worden de resulterende waarden softwarematig vergeleken worden met de verwachte waarden. Bij de naïeve versie moet deze vergelijking handmatig door de tester gedaan worden. Men moet er rekening mee houden dat de definitie van een klasse nooit volledig kan afgesloten worden. Een softwaresysteem zal voortdurend evolueren ten gevolge van aanpassingen en uitbreidingen die gevraagd worden door de gebruikers van een systeem. Na iedere wijziging aan een klasse zal de definitie opnieuw worden uitgetest. Met de aanpak uit Voorbeeld 33 is het niet nodig om telkens opnieuw te controleren of de geïnspecteerde toestanden correct zijn.
/** * Black-box test for the class of accounts. */ public class BlackBoxTest { static public void main(String[] argv) { int nbErrors = 0; long myExpectedBalance; long yourExpectedBalance; // Default constructor: single case { Account myAccount = new Account(); if (myAccount.getBalance() != 0L) ++nbErrors; if (myAccount.getLowerLimit() > 0L) ++nbErrors; if (myAccount.getUpperLimit() <= 0L) ++nbErrors; } // Constructor with initial amount // - Normal case: acceptable initial amount { Account myAccount = new Account(1L); if (myAccount.getBalance() != 1L) ++nbErrors; if (myAccount.getLowerLimit() > 0L) ++nbErrors; if (myAccount.getUpperLimit() <= 0L) ++nbErrors; } // - Illegal case: initial balance below lower limit { Account myAccount = new Account(Long.MIN_VALUE); if (myAccount.getLowerLimit() != Long.MIN_VALUE) { if (myAccount.getBalance() != 0) ++nbErrors; if (myAccount.getLowerLimit() > 0) ++nbErrors; if (myAccount.getUpperLimit() <= 0L) ++nbErrors; } } // - Illegal case: initial balance above upper limit { Account myAccount = new Account(Long.MAX_VALUE); if (myAccount.getUpperLimit() != Long.MAX_VALUE) { if (myAccount.getBalance() != 0) ++nbErrors; if (myAccount.getLowerLimit() > 0) ++nbErrors; if (myAccount.getUpperLimit() <= 0L) ++nbErrors; } } // Mutator deposit // - normal case: acceptable amount { Account myAccount = new Account(1); long amount = maximumDeposit(myAccount) / 2; long expectedBalance = myAccount.getBalance() + amount; myAccount.deposit(amount); if (myAccount.getBalance() != expectedBalance) ++nbErrors; } // - illegal case: negative amount { Account myAccount = new Account(1); myAccount.deposit(-100);
if (myAccount.getBalance() != 1) ++nbErrors; } // - illegal case: resulting balance above upper limit { Account myAccount = new Account(1); if (myAccount.getUpperLimit() < Long.MAX_VALUE) { myAccount.deposit(Long.MAX_VALUE); if (myAccount.getBalance() != 1) ++nbErrors; } } // Mutator withdraw // - normal case: acceptable amount { Account myAccount = new Account(1); myAccount.deposit(maximumDeposit(myAccount)/2); long amount = maximumWithdraw(myAccount) / 4; long expectedBalance = myAccount.getBalance() - amount; myAccount.withdraw(amount); if (myAccount.getBalance() != expectedBalance) ++nbErrors; } // - illegal case: negative amount { Account myAccount = new Account(1); myAccount.withdraw(-100); if (myAccount.getBalance() != 1) ++nbErrors; } // - illegal case: resulting balance below lower limit { Account myAccount = new Account(); if (myAccount.getLowerLimit() > Long.MIN_VALUE+1) { myAccount.withdraw(Long.MAX_VALUE); if (myAccount.getBalance() != 0) ++nbErrors; } } // Mutator transferTo // - normal case: acceptable amount // - normal case: acceptable amount { Account myAccount = new Account(1); myAccount.deposit(maximumDeposit(myAccount)/2); Account yourAccount = new Account(1); long amount = Math.min(maximumWithdraw(myAccount)/4, maximumDeposit(yourAccount)); long myExpectBalance = myAccount.getBalance() - amount; long yourExpectBalance = yourAccount.getBalance() + amount; myAccount.transferTo(amount,yourAccount); if (myAccount.getBalance() != myExpectBalance) ++nbErrors; if (yourAccount.getBalance() != yourExpectBalance) ++nbErrors; } // - illegal case: negative amount { Account myAccount = new Account(1); Account yourAccount = new Account(1); myAccount.transferTo(-100,yourAccount); if (myAccount.getBalance() != 1) ++nbErrors; if (yourAccount.getBalance() != 1) ++nbErrors;
} // - illegal case: non-effective destination account { Account myAccount = new Account(1); myAccount.transferTo(-100,null); if (myAccount.getBalance() != 1) ++nbErrors; } // - illegal case: resulting balance below lower limit { Account myAccount = new Account(); Account yourAccount = new Account(myAccount.getLowerLimit()); long yourBalance = yourAccount.getBalance(); if (myAccount.getLowerLimit() > Long.MIN_VALUE+1) { myAccount.transferTo(Long.MAX_VALUE,yourAccount); if (myAccount.getBalance() != 0) ++nbErrors; if (yourAccount.getBalance() != yourBalance) ++nbErrors; } } // - illegal case: resulting balance above upper limit { Account myAccount = new Account(); myAccount.deposit(maximumDeposit(myAccount)); long myBalance = myAccount.getBalance(); Account yourAccount = new Account(myAccount.getUpperLimit()); long yourBalance = yourAccount.getBalance(); if (yourAccount.getUpperLimit() < Long.MAX_VALUE) { myAccount.transferTo(Long.MAX_VALUE,yourAccount); if (myAccount.getBalance() != myBalance) ++nbErrors; if (yourAccount.getBalance() != yourBalance) ++nbErrors; } } // Producing statistics. if (nbErrors == 0) { System.out.println("Class Account OK!"); } else { System.out.println("Number of Errors: " + nbErrors); } } /** * Calculate the maximum amount that can be withdrawn from * to stay above its lower limit. The resulting value * cannot exceed the largest possible value in the type long. */ static public long maximumWithdraw(Account account) { // algorithm is written to avoid overflow during calculation long balance = account.getBalance(); long lower = account.getLowerLimit(); // <= 0 if (balance >= Long.MAX_VALUE + lower) { return Long.MAX_VALUE; } else { return (balance - lower); } }
/** * Calculate the maximum amount that can be deposited to * not exceeding its upper limit. The resulting value * cannot exceed the largest possible value in the type long. */ static public long maximumDeposit(Account account) { // algorithm is written to avoid overflow during calculation long balance = account.getBalance(); long upper = account.getUpperLimit(); // > 0 if ( (balance < 0) && (upper > Long.MAX_VALUE + balance) ) { return Long.MAX_VALUE; } else { return upper - balance; } } } Voorbeeld 33: verbeterde zwarte-doos-test voor de klasse van bankrekeningen
In het testprogramma wordt bijgehouden hoeveel fouten gedetecteerd werden tijdens de test. Bij het detecteren van een fout wordt deze teller telkens opgehoogd met 1. Indien geen fouten ontdekt werden in de uitvoering van het testprogramma, wordt een boodschap uitgeschreven dat de implementatie van de klasse correct is. Indien wel fouten gedetecteerd werden, wordt het aantal fouten uitgeschreven. De uitgeschreven boodschap is een concatenatie (via de operator +) van een string met de waarde van deze teller, die automatisch getransformeerd wordt naar een string. Merk op dat iedere test ingesloten wordt binnen een blok. Op die manier kunnen alle gegevens, die nodig zijn binnen een bepaalde test, lokaal gehouden worden voor die test alleen. We zijn van oordeel dat hierdoor de leesbaarheid van testprogramma’s verhoogt. Een blok in Java is een constructie, waarin een aantal instructies en declaraties van variabelen samengenomen worden. Het lichaam van een blok wordt omgeven door {…}. Het bereik (scope) van variabelen, die binnen een blok gedeclareerd worden is beperkt tot dat blok. Variabelen kunnen enkel gebruikt worden vanaf het punt van hun declaratie tot het einde van het blok waarin ze gedeclareerd werden. Voor iedere variabele zal geheugen gereserveerd worden bij het verwerken van de declaratie ervan. Dit geheugen zal terug vrijgegeven worden, zodra het blok waarin de variabele gedeclareerd werd wordt verlaten. Merk op dat twee hulpklassenmethoden worden ingevoerd. De eerste methode maximumWithdraw berekent het grootst mogelijke bedrag, dat van de gegeven rekening kan afgehaald worden. Uiteraard kan dit bedrag het grootst mogelijke getal binnen het type long niet overschrijden. De methode maximumDeposit berekent op analoge manier het grootst mogelijke bedrag dat op de gegeven rekening kan gestort worden. Voor de constructor met een argument worden drie testen gedaan. Een eerste met een aanvaardbare waarde (1), een tweede met een waarde beneden de laagst mogelijke waarde
voor de balans, en een derde met een waarde boven de hoogst mogelijke waarde voor de balans. Merk op dat de twee laatste gevallen niet altijd kunnen getest worden. Indien bijvoorbeeld de ondergrens voor de balans van een bankrekening samenvalt met de kleinst mogelijke waarde binnen het type long, is het onmogelijk een nieuwe bankrekening aan te maken waarvan de initiële balans beneden die ondergrens ligt. Een gelijkaardig geval doet zich voor bij het testen van de mutatoren deposit, withdraw en transferTo. Zo is het immers mogelijk dat het afhalen van een bedrag gelijk aan het grootst mogelijke getal binnen het type long niet resulteert in een balans die beneden de ondergrens voor de betrokken rekening ligt. De zwarte-doos-teststrategie is een eenvoudige strategie voor de verificatie van software systemen. Zoals voor alle dynamische verificatiestrategieën, kan een test volgens het zwarte-doos-principe enkel de aanwezigheid van fouten aan het licht brengen. Het welslagen van een zwarte-doos-test is geenszins een garantie dat de software (de klasse) volledig correct is. Een specifiek nadeel van de zwarte-doos-teststrategie is dat bepaalde paden in de implementatie van een methode potentieel niet bewandeld worden tijdens de test. Een andere strategie in de context van dynamische verificatie, met name de witte-doosstrategie, zal hieraan tegemoet komen. Deze strategie wordt besproken in het tweede deel van deze tekst.
0.1.6
Codeerregels
N Naaaam mccoonnvveennttiieess • Identificatoren voor klassen volgen de titelstijl: opeenvolgende woorden worden zonder scheiding aan mekaar geschreven; elk woord begint met een hoofdletter. • Identificatoren voor instantiatiemethodes en klassenmethodes bestaan uit een aantal woorden, die aan mekaar geschreven worden. Alle woorden, behalve het eerste, beginnen met een hoofdletter. • Identificatoren voor formele argumenten en lokale variabelen van methodes bestaan uit een aantal woorden, die aan mekaar geschreven worden. Alle woorden, behalve het eerste woord, beginnen met een hoofdletter.
• Identificatoren voor instantiatievariabelen beginnen met een dollarteken. Opeenvolgende woorden worden aan mekaar geschreven. Het eerste woord begint met een kleine letter, eventuele volgende woorden beginnen met een hoofdletter. • Identificatoren voor wijzigbare klassenvariabelen beginnen met een onderlijningsteken. Opeenvolgende woorden worden aan mekaar geschreven. Het eerste woord begint met een kleine letter, eventuele volgende woorden beginnen met een hoofdletter. • Identificatoren voor niet-wijzigbare klassenvariabelen beginnen niet met een speciaal karakter, maar worden volledig in hoofdletters geschreven. Opeenvolgende woorden worden gescheiden door een onderlijningsteken.
D Deeffiinniittiiee vvaann kkllaasssseenn • De definitie van een klasse zal een hoofding omvatten, waarin de betekenis van haar objecten bondig wordt toegelicht. Deze hoofding zal daarenboven de belangrijkste karakteristieken bondig introduceren.
D Deeffiinniittiiee vvaann M Meetthhooddeess • De definitie van een methode zal een hoofding omvatten, waarin de semantiek (betekenis) van de methode bondig wordt toegelicht.
D Deeffiinniittiiee vvaann m muuttaattoorreenn • Elke mutator zal zich beperken tot het wijzigen van de toestand van het direct betrokken object, van objecten die als actueel argument worden meegegeven, van objecten waarmee deze objecten in relatie staan, en van klassenvariabelen van de klassen waartoe deze objecten behoren. • Elke mutator zal bij voorkeur minstens de toestand van het direct betrokken object wijzigen. Het direct betrokken object kan binnen een mutator ook enkel gebruikt worden om de toestandsveranderingen aan andere objecten te controleren.
• Een mutator zal geen resultaat teruggeven.
D Deeffiinniittiiee vvaann iinnssppeeccttoorreenn • Elke inspector in een klasse zal enkel informatie teruggeven omtrent de toestand van het impliciete object, van objecten die als actueel argument worden meegegeven, van objecten waarmee deze objecten in relatie staan en/of van klassenvariabelen van de klassen waartoe deze objecten behoren. • Elke inspector zal minstens de toestand van het impliciet object inspecteren, of (occasioneel) klassenvariabelen van de klasse waartoe het impliciet argument behoort. • Een inspector zal geen toestandswijziging veroorzaken.
IIm mpplleem meennttaattiiee vvaann m meetthhooddeess • In conditionele uitdrukkingen zal steeds gebruik gemaakt worden van de conditionele logische operatoren && en ||. • In conditionele opdrachten zullen de then-tak en de else-tak bij voorkeur omsloten worden door {…}. • Complexere methodes zullen zoveel mogelijk in functie van primitievere methodes geïmplementeerd worden, om een optimale aanpasbaarheid en herbruikbaarheid te bekomen.