DEEL 1 | Basisprincipes van objectoriëntatie
HOOFDSTUK 3 Interactie tussen objecten
Belangrijkste concepten in dit hoofdstuk:
• • • • • •
68
abstractie modularisatie objecten maken objectdiagrammen methodeaanroepen debuggers
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
In dit hoofdstuk besproken Java-constructies: klassentypes, logische operatoren (&&, ||), stringsamenvoeging, modulo-operator (%), objectconstructie (new), methodeaanroepen (puntnotatie), this In de vorige hoofdstukken hebben we bekeken wat objecten zijn en hoe ze tot stand komen. We hadden het over velden, constructors en methodes toen we klassendefinities bestudeerden. We zullen nu een stap verder gaan. Om interessante toepassingen te maken is het niet voldoende om individueel werkende objecten te hebben. De objecten moeten gecombineerd worden zodat ze samenwerken om een gezamenlijke taak uit te voeren. In dit hoofdstuk zullen we een kleine toepassing bouwen die bestaat uit drie objecten, en voor methodes zorgen waarmee andere methodes kunnen worden aangeroepen om het gezamenlijke doel te bereiken.
3.1
Het klokvoorbeeld
De interactie tussen objecten zullen we illustreren met het display van een digitale klok als voorbeeld. Het display toont de uren en minuten, gescheiden door een dubbele punt (figuur 3.1). Voor deze oefening zullen we eerst een klok maken met een 24-uursdisplay. Daarbij toont het display de tijd van 00:00 (middernacht) tot en met 23:59 (één minuut voor middernacht). Het zal blijken dat een klok met een twaalfuursnotatie iets lastiger is te maken dan een 24-uursklok. We zullen die daarom bewaren tot het eind van dit hoofdstuk.
11:03 Figuur 3.1 Een display van een digitale klok
3.2
Abstractie en modularisatie
In eerste instantie zouden we kunnen proberen om de hele tijdaanduiding in een enkele klasse te implementeren. Dat is ten slotte het enige wat we tot nu toe gedaan hebben: klassen maken om een taak uitgevoerd te krijgen. Maar in dit geval zullen we een iets andere aanpak gebruiken. We zullen eerst proberen of we deelcomponenten kunnen onderscheiden in het probleem waar we vervolgens afzonderlijke klassen voor kunnen maken. De reden is complexiteit. Naarmate we vorderen in dit boek zullen de voorbeelden die we gebruiken en de programma’s die we schrijven steeds complexer worden. Weinig taken, zoals de kaartautomaat, kunnen we nog wel als één enkel probleem oplossen. De volledige taak ervan is overzichtelijk en kan gemakkelijk met één klasse uitgevoerd worden. Voor complexere taken is dat een te simplistisch concept. Naarmate de omvang van een probleem toeneemt wordt het steeds lastiger om alle details ervan tegelijkertijd in de gaten te houden.
69
DEEL 1 | Basisprincipes van objectoriëntatie
CONCEPT
Abstractie is de mogelijkheid om details van onderdelen te negeren om de aandacht te richten op een probleem op een hoger niveau. De oplossing die we gebruiken voor het probleem van de complexiteit noemen we abstractie. We splitsen het probleem op in subproblemen, en vervolgens in subsubproblemen, enzovoort, tot de afzonderlijke problemen klein genoeg zijn om gemakkelijk te worden opgelost. Zodra we een van de subproblemen hebben opgelost hoeven we ons over de details daarvan niet meer druk te maken. We kunnen de oplossing als bouwsteen gebruiken voor de oplossing van een volgend probleem. Deze techniek wordt soms ook wel verdeel-en-heers (Engels: divide and conquer) genoemd. We zullen dit toelichten met een voorbeeld. Laten we eens kijken naar een technicus van een autofabriek die een nieuwe auto ontwerpt. Hij denkt na over de onderdelen van de auto, zoals de vorm van de carrosserie, de afmeting en plaats van de motor, het aantal en de afmetingen van de stoelen in het passagierscompartiment, de exacte wielbasis, enzovoort. Een andere technicus daarentegen, die bijvoorbeeld belast is met het ontwerp van de motor (in de praktijk is dat een heel team van technici, maar voor de eenvoud van het voorbeeld zullen we deze taak onderbrengen bij één enkele technicus), denkt bij een motor aan de talloze onderdelen daarvan: de cilinders, het brandstofinspuitsysteem, de krukas, de elektronica, enzovoort. Ze ziet de motor niet als een enkele entiteit, maar als een complex samenspel van een groot aantal onderdelen. Een van deze onderdelen zou een bougie kunnen zijn. Dan is er een technicus (misschien in een ander bedrijf) die belast is met het ontwerp van de bougies. Zij ziet de bougie als een complex kunstwerk dat bestaat uit een groot aantal onderdelen. Ze heeft wellicht diepgaand onderzoek gedaan naar het optimale materiaal voor de contacten, naar het optimale materiaal en het optimale productieproces voor de fabricage van de isolatie. Hetzelfde geldt voor veel andere onderdelen. Een ontwerper op het hoogste niveau zal een wiel beschouwen als één onderdeel. Een andere technicus, veel lager in de keten, kan haar dagen vullen met het bedenken van een chemische samenstelling van de benodigde materialen om goede banden mee te maken. Voor de constructeur is de band een complex ding. De autofabrikant koopt gewoonweg banden bij een bandenfabrikant en ziet die banden als één enkele entiteit. Dat is abstractie. De ingenieur in de autofabriek abstraheert de informatie van de bandenfabrikant om zich te kunnen concentreren op de constructie van een wiel. De ontwerper van de carrosserie van de auto abstraheert de technische informatie van de wielen en de motor om zich te kunnen concentreren op het ontwerp van de carrosserie (ze zal waarschijnlijk alleen geïnteresseerd zijn in de afmetingen van de motor en de wielen). Hetzelfde geldt voor elke andere component. Terwijl iemand zich bezighoudt met het ontwerp van het passagierscompartiment kan iemand anders bezig zijn met het ontwerp van de stof waar uiteindelijk de stoelen mee bekleed zullen gaan worden. Waar het op aankomt, is dat de auto uit zo veel details bestaat dat het onmogelijk wordt dat één persoon alles over elk onderdeel weet. Als dat noodzakelijk zou zijn, zouden er nooit auto’s gebouwd worden.
70
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
CONCEPT
Modularisatie is het proces waarmee iets in goed gedefinieerde delen wordt opgesplitst die afzonderlijk kunnen worden uitgewerkt en op goed gedefinieerde manieren samenwerken.
De reden waarom er met succes auto’s worden gebouwd is dat de constructeurs gebruikmaken van modularisatie en abstractie. Ze splitsen de auto op in onafhankelijk modules (wielen, motor, versnellingsbak, stoelen, stuurwiel, enzovoort) en laten afzonderlijke mensen, onafhankelijk van elkaar, aan de afzonderlijke modules werken. Wanneer een module klaar is gebruiken ze abstractie. Ze bekijken die module als één enkele component die wordt gebruikt om meer complexe componenten te maken. Modularisatie en abstractie zijn dus elkaars complement. Modularisatie is het proces waarmee grote dingen (problemen) opgesplitst worden in kleinere delen, terwijl abstractie de mogelijkheid biedt om details te negeren en de aandacht te richten op het grotere geheel.
3.3
Abstractie in software
Dezelfde principes van modularisatie en abstractie die we in de vorige paragraaf zagen, worden ook gebruikt bij het ontwikkelen van software. Om het overzicht te houden in complexe programmatuur proberen we subcomponenten te onderscheiden die we als onafhankelijke entiteiten kunnen programmeren. Dan proberen we de subcomponenten te gebruiken alsof ze eenvoudige delen waren, zonder ons zorgen te maken over de complexiteit ervan. Bij objectgeoriënteerd programmeren zijn deze componenten en subcomponenten objecten. Als we een auto in software zouden proberen te maken, met behulp van een objectgeoriënteerde taal, zouden we precies hetzelfde doen als de autotechnici. In plaats van de auto in één enkel, gigantisch object te implementeren, zouden we eerst afzonderlijke objecten voor een motor, een versnellingsbak, een wiel, een stoel, enzovoort maken, en vervolgens het auto-object samenstellen uit die kleinere objecten. Het is niet altijd even gemakkelijk om vast te stellen welke soort objecten (en daarmee klassen) in een softwaresysteem nodig zijn om een probleem op te lossen. We zullen daar later in dit boek op terugkomen. Op dit moment zullen we beginnen met een relatief eenvoudig voorbeeld. Terug dus naar onze digitale klok.
3.4
Modularisatie in het klokvoorbeeld
Laten we eens beter kijken naar het klokdisplayvoorbeeld. We gebruiken de zojuist beschreven abstractieconcepten om de beste manier te vinden om naar dit voorbeeld te kijken, zodat we enkele klassen kunnen schrijven om het klokdisplay te programmeren. We kunnen het display beschouwen als een display met vier tekens (twee tekens voor de uren, twee voor de minuten). Als we nu een niveau hoger abstraheren zien we dat het display ook beschreven kan worden als twee afzonderlijke displays met twee cijfers (één paar voor de uren en één paar voor de minuten). Eén paar begint bij 0, wordt elk uur met 1 opgehoogd
71
DEEL 1 | Basisprincipes van objectoriëntatie
en begint opnieuw bij 0 nadat het maximum van 23 bereikt is. Het andere paar wordt elke minuut met 1 opgehoogd en begint opnieuw bij 0 nadat het maximum van 59 bereikt is. Op basis van de overeenkomsten in het gedrag van deze twee displays kunnen we nog verder abstraheren van de concepten uren en minuten. We kunnen de twee displays ook zien als objecten die waarden kunnen weergeven tussen 0 en een bepaald maximum. De waarde kan worden opgehoogd, maar als deze het maximum bereikt, wordt deze teruggezet op 0. We lijken nu een zinnig abstractieniveau bereikt te hebben dat we kunnen simuleren met een klasse: een tweecijferigdisplayklasse. Voor onze klok zullen we eerst een klasse programmeren voor één tweecijferig display (figuur 3.2) en daar vervolgens een accessormethode voor maken om de waarde op te halen en twee mutatormethodes om de waarde in te stellen en deze op te hogen. Zodra we deze klasse hebben gedefinieerd, hoeven we alleen maar twee objecten van deze klasse te maken met verschillende maxima om de hele klok te construeren.
03 Figuur 3.2 Een tweecijferig display
3.5
De klassen voor het klokdisplay opzetten
Zoals we hierboven hebben vastgesteld, moeten we om een klokdisplay te maken eerst een tweecijferig display maken. Dit display moet twee waarden opslaan. De ene is het maximum tot waar het kan tellen voordat de waarde weer wordt teruggezet op nul. De andere is de huidige waarde. We zullen beide als geheelgetalvelden in onze klasse opnemen (code 3.1). Code 3.1 Klasse voor een tweecijferig display public class NumberDisplay {
private int limit; private int value; De constructor en de methodes zijn weggelaten.
}
CONCEPT
Klassen definiëren types. Een klassennaam kan worden gebruikt als het type voor een variabele. Variabelen die een klasse als hun type hebben, kunnen objecten van die klasse opslaan.
72
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
We zullen later terugkomen op de overige informatie van deze klasse. We gaan er even van uit dat we de klasse NumberDisplay kunnen maken, en denken eerst na over het complete klokdisplay. Voor een compleet klokdisplay hebben we een object nodig dat, intern, werkt met twee getaldisplays (één vootr de uren en één voor de minuten). Elk gtetaldisplay is dus een veld in het klokdisplay (code 3.2). We maken hier gebruik van iets wat we nog niet eerder hebben gezien: klassen definiëren types. Code 3.2 De klasse ClockDisplay bevat twee NumberDisplay-velden public class ClockDisplay { }
private NumberDisplay hours; private NumberDisplay minutes; De constructor en de methodes zijn weggelaten.
Bij de bespreking van velden in hoofdstuk 2, hebben we gezien dat het woord private in de declaratie van het veld gevolgd wordt door een type en een naam voor het veld. Hier gebruiken we de klasse NumberDisplay als type voor de velden die de namen hours en minutes hebben. Dat betekent dat klassennamen gebruikt kunnen worden als type. Het type van een veld geeft aan welk soort waarden in het veld opgeslagen kunnen worden. Als het type een klasse is, kan het veld objecten van die klasse bevatten.
3.6
Klassendiagrammen versus objectdiagrammen
De in de vorige paragraaf beschreven structuur (een ClockDisplay-object dat twee NumberDisplay-objecten bevat) kunnen we grafisch voorstellen in een objectdiagram, op de manier zoals is weergegeven in figuur 3.3(a). In dit diagram zie je dat we te maken hebben met drie objecten. In figuur 3.3(b) is het klassendiagram voor dezelfde situatie weergegeven.
myDisplay: ClockDisplay
:NumberDisplay
ClockDisplay
11
hours minutes NumberDisplay
:NumberDisplay a
03
b
Figuur 3.3 Objectdiagram en klassendiagram voor ClockDisplay
73
DEEL 1 | Basisprincipes van objectoriëntatie
CONCEPT
Het klassendiagram bevat de klassen van een toepassing en hun onderlinge relaties. Het bevat informatie over de broncode. Het is een statisch beeld van een programma.
Merk op dat het klassendiagram slechts twee klassen bevat, terwijl het objectdiagram drie objecten bevat. Dat komt doordat we verschillende objecten van dezelfde klasse kunnen maken. We maken hier twee NumberDisplay-objecten van de klasse NumberDisplay. Deze twee diagrammen geven elk een ander beeld van dezelfde toepassing. Het klassendiagram geeft een statisch beeld. Het is een weergave van datgene wat we hebben op het moment dat we het programma schrijven. We hebben twee klassen en de pijl geeft aan dat de klasse ClockDisplay gebruikmaakt van de klasse NumberDisplay (dat wil zeggen dat NumberDisplay voorkomt in de broncode van ClockDisplay). We zeggen ook dat ClockDisplay afhankelijk is van NumberDisplay. Om het programma te starten zullen we een object van de klasse ClockDisplay maken. We zullen het klokdisplay zo programmeren dat het automatisch twee NumberDisplay-objecten voor zichzelf maakt. Het objectdiagram toont de situatie in runtime (wanneer de toepassing wordt uitgevoerd). Dit wordt ook wel de dynamische weergave genoemd. CONCEPTEN
Het objectdiagram toont de objecten en hun onderlinge relaties op een bepaald moment tijdens de uitvoering van een toepassing. Het geeft informatie over objecten terwijl het programma wordt uitgevoerd. Het geeft een dynamisch beeld van een programma. Objectverwijzingen. Variabelen van objecttypes bevatten verwijzingen naar objecten.
Het objectdiagram bevat ook een ander belangrijk detail: wanneer een variabele een object opslaat, wordt het object niet rechtstreeks in de variabele opgeslagen, maar wordt er een objectverwijzing opgeslagen in de variabele. In het diagram is de variabele weergegeven als een wit vlak met een rand en de objectverwijzing als een pijl. Het object waarnaar verwezen wordt, is opgeslagen buiten het object dat de verwijzing bevat en de twee worden aan elkaar gekoppeld door de objectverwijzing. Het is erg belangrijk om deze twee verschillende diagrammen en de verschillende beelden goed te begrijpen. BlueJ toont alleen het statische beeld. In het hoofdvenster ervan wordt het klassendiagram weergegeven. Om Java-programma’s te kunnen plannen en begrijpen moet je zelf objectdiagrammen op papier of in je hoofd kunnen maken. Wanneer we denken over wat ons programma zal doen, zullen we nadenken over de objectstructuren die het zal maken en hoe deze objecten op elkaar zullen reageren. Het is essentieel dat je de objectstructuren kunt visualiseren.
74
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Oefening 3.1 Denk nog een keer terug aan het project lab-classes dat we in de hoofdstukken 1 en 2 hebben besproken. Stel je voor dat we een object LabClass maken en drie Student-objecten. Vervolgens plannen we alle drie de studenten in dat lab in. Probeer nu een klassendiagram en een objectdiagram voor die situatie te tekenen. Benoem en verklaar de verschillen ertussen. Oefening 3.2 Op welk(e) moment(en) kan een klassendiagram veranderen? Hoe wordt het veranderd? Oefening 3.3 Op welk(e) moment(en) kan een objectdiagram veranderen? Hoe wordt het veranderd? Oefening 3.4 Schrijf een definitie van een veld met de naam tutor dat een verwijzing kan bevatten naar een object van het type Instructor.
3.7
Primitieve types en objecttypes
Java kent twee sterk van elkaar verschillende soorten types: primitieve types en objecttypes. De primitieve types zijn allemaal vooraf in de taal Java gedefinieerd. Enkele daarvan zijn int en boolean. In bijlage B vind je een complete lijst met primitieve types. Objecttypes worden gedefinieerd door klassen. Sommige klassen worden gedefinieerd door het standaard Java-systeem (zoals String); andere klassen kunnen we zelf schrijven. CONCEPT
De primitieve types in Java zijn alle types die geen objecttype zijn. Types zoals int, boolean, char, double en long zijn de meest gebruikte primitieve types. Primitieve types hebben geen methodes. Zowel primitieve types als objecttypes kunnen gebruikt worden als types, maar er zijn situaties waarin ze zich anders gedragen. Een verschil is hoe waarden worden opgeslagen. Zoals we in de diagrammen kunnen zien, worden primitieve waarden rechtstreeks opgeslagen in een variabele (we hebben de waarde rechtstreeks in het variabelenkader weergegeven, bijvoorbeeld in hoofdstuk 2, figuur 2.3). Objecten, daarentegen, worden niet rechtstreeks opgeslagen in de variabele, maar in een verwijzing naar het object (weergegeven als een pijl in de diagrammen, zoals in figuur 3.3(a)). We zullen later nog andere verschillen tussen primitieve types en objecttypes zien.
3.8
De broncode van ClockDisplay
Voor we de broncode beter gaan bekijken is het wellicht handig als je eerst het voorbeeld bekijkt in BlueJ.
75
DEEL 1 | Basisprincipes van objectoriëntatie
Oefening 3.5 Start BlueJ, open het voorbeeld clock-display en experimenteer ermee. Maak een ClockDisplay-object met behulp van de constructor die geen parameters nodig heeft, en open vervolgens de objectinspector voor dit object. Pas, met het objectinspectorvenster open, de methodes van het object toe. Bekijk het veld display String in het inspectorvenster. Lees het projectcommentaar (door te dubbelkikken op het opmerkingenpictogram in het hoofdscherm) voor meer informatie.
3.8.1
De klasse NumberDisplay
We zullen nu de klasse NumberDisplay in detail bekijken. In code 3.3 is de complete broncode weergegeven. Over het algemeen is deze klasse redelijk ongecompliceerd. De klasse bevat de twee velden die we in paragraaf 3.5 bespraken, een constructor en vier methodes (getValue, setValue, getDisplayValue en increment). De constructor ontvangt de maximumwaarde als parameter. Als bijvoorbeeld voor het maximum 24 wordt ingevoerd, zal het display bij het bereiken van die waarde opnieuw beginnen op te lopen vanaf 0. Het bereik van de displaywaarden loopt dus van 0 tot en met 23. Doordat het maximum in te stellen is, kunnen we deze klasse voor zowel de urenaanduiding als de minutenaanduiding gebruiken. Voor de urenaanduiding maken we een NumberDisplay-object met als maximum 24; voor het minutendisplay maken we er een met een maximum van 60. De constructor slaat het maximum op in een veld en stelt de huidige waarde van het display in op 0. Code 3.3 De klasse NumberDisplay /** * De klasse NumberDisplay simuleert een digitaal getaldisplay * dat waarden kan bevatten tussen nul en een bepaald maximum. * Het maximum kan worden gespecificeerd wanneer het display * wordt gemaakt. De waarden liggen tussen nul en het maximum * minus 1. Als de klasse wordt gebruikt om bijvoorbeeld de * seconden op een digitale klok aan te duiden, zal het * maximum 60 zijn en zullen de weergegeven waarden van 0 * tot en met 59 lopen. Wanneer het wordt opgehoogd, begint het * display automatisch opnieuw bij nul zodra het maximum * wordt bereikt. * * @author Michael Kölling and David J. Barnes * @version 2011.07.31 */ public class NumberDisplay { private int limit; private int value;
76
/** * Constructor voor objecten van de klasse NumberDisplay
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
*/ public NumberDisplay(int rollOverLimit) { limit = rollOverLimit; value = 0; } /** * Retourneer de huidige waarde. */ public int getValue() { return value; } /** * Stel de waarde van het display in op de ingevoerde * waarde. Doe niets als de nieuwe waarde kleiner is * dan nul of de limiet overschrijdt. */ public void setValue(int replacementValue) { if((replacementValue >= 0) && (replacementValue < limit)) { value = replacementValue; } } /** * Retourneer de displaywaarde. (Dat wil zeggen: geef de * huidige waarde als een tweecijferige string. Als de * waarde kleiner is dan tien zal die worden aangevuld met * een voorloopnul.) * / public String getDisplayValue() { if(value < 10) { return “0” + value; } else { return “” + value; } } /** * Verhoog de displaywaarde met 1, maar begin opnieuw bij * nul als het maximum wordt bereikt. */ public void increment() { value = (value + 1) % limit; } }
77
DEEL 1 | Basisprincipes van objectoriëntatie
Daarna volgt een eenvoudige accessormethode voor de huidige displaywaarde (get Value). Hiermee kunnen andere objecten de huidige waarde van het display uitlezen. De volgende mutatormethode setValue is interessanter. Deze ziet er als volgt uit: public void setValue(int replacementValue) { if((replacementValue >= 0) && (replacementValue < limit)) { value = replacementValue; } }
Hiermee voeren we de nieuwe waarde voor het display als parameter in de methode in. Voor we de waarde echter toekennen, moeten we controleren of de waarde geldig is. De geldige waarden liggen in het bereik van 0 tot 1 minder dan het maximum. We kunnen een if-statement gebruiken om, voordat we de waarde toekennen, te controleren of de waarde geldig is. Het symbool && is een logische en-operator. Het zorgt ervoor dat de voorwaarde in het if-statement waar is als de voorwaarden aan weerskanten van het &&-symbool waar zijn. Meer informatie hierover vind je hieronder in het blok Logische operatoren. In bijlage C vind je een tabel met alle logische operatoren in Java.
Logische operatoren Logische operatoren gebruiken booleaanse waarden (waar of onwaar) als argument en produceren een nieuwe booleaanse waarde als resultaat. De drie belangrijkste logische operatoren zijn ‘en’, ‘of’ en ‘niet’. In Java worden ze geschreven als:
&& (en) || (of) ! (niet)
De expressie
a && b
is waar als zowel a als b waar zijn, maar onwaar in alle andere gevallen. De expressie
a || b
is waar als ofwel a ofwel b of beide waar zijn, maar onwaar als beide onwaar zijn. De expressie
!a
is waar als a onwaar is, maar onwaar als a waar is.
Oefening 3.6 Wat gebeurt er wanneer de methode setValue wordt toegepast met een ongeldige waarde? Is dit een goede oplossing? Kun je een betere oplossing bedenken?
78
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Oefening 3.7 Wat zou er gebeuren als je de operator >= in de test zou vervangen door >, met andere woorden: als je het conditionele statement verandert in het volgende?
if((replacementValue > 0) && (replacementValue < limit))
Oefening 3.8 Wat zou er gebeuren als je de operator && in de test zou vervangen door ||, zodat het conditionele statement verandert in het volgende?
if((replacementValue >= 0) || (replacementValue < limit))
Oefening 3.9 Welke van de volgende expressies retourneren true (waar)?
! (4 < 5) ! false (2 > 2) || ((4 == 4) && (1 < 0)) (2 > 2) || (4 == 4) && (1 < 0) (34 != 33) && ! false
Schrijf je antwoorden eerst op en open dan het evaluatievak in BlueJ om te zien of ze kloppen. Oefening 3.10 Schrijf een expressie met de booleaanse variabelen a en b die alleen true oplevert wanneer a en b of beide waar of beide onwaar zijn. Oefening 3.11 Schrijf een expressie met de booleaanse variabelen a en b die true oplevert wanneer ofwel a ofwel b waar is, maar false wanneer a en b beide waar of beide onwaar zijn. (Dit wordt ook een exclusieve of genoemd.) Oefening 3.12 Bekijk de expressie (a && b). Schrijf een gelijkwaardige expressie (die alleen true oplevert bij precies dezelfde waarden van a en b), maar zonder de operator && te gebruiken.
De volgende methode, getDisplayValue, retourneert ook de waarde van het display, maar in een andere indeling. Dat willen we omdat we de waarde als string van twee tekens willen weergeven. Als de huidige tijd 3:05 is, willen dat het display 03:05 weergeeft en niet 3:5. Om dat gemakkelijk te kunnen doen, hebben we de methode getDisplayValue geschreven. Deze methode retourneert de huidige waarde als een string en plaatst een voorloopnul als de waarde lager is dan 10. Dat gebeurt in dit deel van de code: if(value < 10) { return “0” + value; } else { return “” + value; }
Merk op dat de nul (“0”) tussen dubbele aanhalingstekens staat. Dat betekent dat we de string 0 weergeven en niet het gehele getal 0. De expressie “0” + value
79
DEEL 1 | Basisprincipes van objectoriëntatie
‘telt’ een string en een geheel getal bij elkaar ‘op’ (omdat het type van value int is). De plusoperator plakt dus weer de string aan het getal, zoals we eerder zagen in paragraaf 2.9. Voor we verder gaan zullen we het aan elkaar plakken van strings nog even beter bekijken.
3.8.2
Aaneenschakelen van strings
De plusoperator (+) heeft verschillende betekenissen, afhankelijk van het type van de argumenten. Als beide argumenten getallen zijn, worden deze erdoor opgeteld (wat te verwachten was). Op die manier wordt
42 + 12
het totaal van deze twee getallen: 54. Als de argumenten echter strings zijn, betekent het plusteken dat de strings samengevoegd moeten worden met als resultaat één enkele string die bestaat uit beide argumenten. Het resultaat van de de expressie
“Java” + “met BlueJ”
is de string
“Javamet BlueJ”
Merk op dat het systeem niet automatisch een spatie tussen de strings plaatst. Als je een spatie wilt hebben, moet je die zelf in een van de strings plaatsen. Als een van de argumenten van een optelling een string is, maar de andere niet, wordt van het andere argument automatisch een string gemaakt en vervolgens worden de twee strings aan elkaar geplakt. Op die manier wordt
“antwoord: “ + 42
de string
“antwoord: 42”
Dit werkt voor alle types. Elk type dat ‘opgeteld’ wordt bij een string wordt automatisch geconverteerd naar een string en dan samengevoegd. Terug naar onze code in de methode getDisplayValue. Als value bijvoorbeeld de waarde 3 bevat, zal de regel return “0” + value;
de string “03” retourneren. Als de waarde hoger is dan 9, hebben we een trucje toegepast: return “” + value;
In dit geval voegen we een lege string samen met de waarde van value. Het resultaat is dat de waarde geconverteerd zal worden tot een string, zonder dat er andere tekens vóór geplaatst worden. We gebruiken de plusoperator dus alleen maar om de gehele waarde te converteren naar een waarde van het type String.
80
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Oefening 3.13 Werkt de methode getDisplayValue in alle omstandigheden correct? Welke aannames worden er binnen deze methode gemaakt? Wat gebeurt er als je een display maakt met een maximumwaarde van bijvoorbeeld 800? Oefening 3.14 Is er een verschil in het resultaat van de regel
return value + “”;
in plaats van
return “” + value;
in de methode getDisplayValue?
3.8.3
De modulo-operator
De laatste methode in de klasse NumberDisplay verhoogt de displaywaarde met 1. Deze zorgt er ook voor dat de waarde terugspringt naar 0 wanneer het maximum bereikt wordt: public void increment() { value = (value + 1) % limit; }
Deze methode gebruikt de modulo-operator (%). De modulo-operator berekent de rest van een gehele deling. Het resultaat van de deling
27 / 4
kan in gehele getallen worden uitgedrukt als
resultaat = 6, rest = 3
De modulo-operator retourneert alleen de rest van zo’n deling. Op die manier wordt het resultaat van de expressie (27 % 4) dus 3.
Oefening 3.15 Leg de werking van de modulo-operator uit. Mogelijk moet je hiervoor ook andere bronnen aanboren (online Java-taalbronnen, andere boeken over Java, enzovoort). Oefening 3.16 Wat is het resultaat van de expressie (8 % 3)? Oefening 3.17 Voer de expressie (8 % 3) in Evualueer window in. Probeer ook andere getallen. Wat gebeurt er als je de modulo-operator gebruikt in combinatie met negatieve getallen? Oefening 3.18 Wat zijn alle mogelijke resultaten van de expressie (n % 5), waarbij n een gehele variabele is? Oefening 3.19 Wat zijn alle mogelijke resultaten van de expressie (n % m), waarbij n en m gehele variabelen zijn?
81
DEEL 1 | Basisprincipes van objectoriëntatie
Oefening 3.20 Leg in detail uit hoe de methode increment werkt. Oefening 3.21 Herschrijf de methode increment zonder de modulo-operator en gebruik daarvoor in de plaats een if-statement. Welke oplossing is beter? Oefening 3.22 Open het project clock-display in BlueJ en test de klasse NumberDisplay door enkele NumberDisplay-objecten te maken en hun methodes aan te roepen.
3.8.4
De klasse ClockDisplay
Nu we gezien hebben hoe we een klasse kunnen maken die een tweecijferig display definieert, zullen we de klasse ClockDisplay beter bekijken – de klasse die twee getaldisplays combineert tot een echt klokdisplay. In code 3.4 is de complete broncode van de klasse ClockDisplay weergegeven. Net als bij de klasse NumberDisplay zullen we hier ook alle velden, constructors en methodes afzonderlijk bekijken. Code 3.4 Implementatie van de klasse ClockDisplay /** * De klasse ClockDisplay implementeert een digitaal * klokdisplay voor een klok met 24-uursnotatie. De klok toont * de uren en minuten. * Het bereik van de klok is 00:00 (middernacht) tot en met * 23:59 (één minuut voor middernacht). * * Het klokdisplay ontvangt elke minuut ‘tikken’ (via de * methode timeTick) en reageert door het display op te hogen. * Dit gebeurt op dezelfde manier als bij een gewone klok: de * urenaanduiding wordt opgehoogd wanneer de minuten naar nul * overspringen. * * @author Michael Kölling and David J. Barnes * @version 2011.07.31 */ public class ClockDisplay { private NumberDisplay hours; private NumberDisplay minutes; private String displayString; // simuleert het eigenlijke // display /** * Constructor voor ClockDisplay-objecten. Deze constructor * maakt een nieuwe klok die ingesteld is op 00:00. */ public ClockDisplay() { hours = new NumberDisplay(24); 82
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
minutes = new NumberDisplay(60); updateDisplay(); } /** * Constructor voor ClockDisplay-objecten. Deze constructor * maakt een nieuwe klok die is ingesteld op een bepaalde * tijd die ingesteld is via de parameters. */ public ClockDisplay(int hour, int minute) { hours = new NumberDisplay(24); minutes = new NumberDisplay(60); setTime(hour, minute); } /** * Deze methode moet elke minuut eenmaal aangeroepen * worden, waardoor het klokdisplay een minuut vooruit * wordt gezet. */ public void timeTick() { minutes.increment(); if(minutes.getValue() == 0) { // Maximum bereikt! hours.increment(); } updateDisplay(); } /** * Stel de tijd van het display in op de ingevoerde uren * en minuten. */ public void setTime(int hour, int minute) { hours.setValue(hour); minutes.setValue(minute); updateDisplay(); } /** * Retourneer de huidige tijd van dit display in de * indeling UU:MM. */ public String getTime() { return displayString; } /** * Werk de interne string bij die het display voorstelt. */ private void updateDisplay() { displayString = hours.getDisplayValue() + “:” + minutes.getDisplayValue(); } }
83
DEEL 1 | Basisprincipes van objectoriëntatie
In dit project gebruiken we het veld displayString om het eigenlijke display van de klok te simuleren (zoals we zagen in oefening 3.5). Als deze software op een echte klok uitgevoerd zou worden, zou de uitvoer ervan op het display van de klok worden weergegeven. Deze string dient dus als softwaresimulatie voor het uitvoerapparaat van een klok.1 Om dit te bereiken gebruiken we een stringveld en een methode: public class ClockDisplay { private String displayString; Andere velden en methodes zijn weggelaten. }
/** * Werk de interne string bij die het display voorstelt. */ private void updateDisplay() { Implementatie van de methode is weggelaten. }
Steeds wanneer we het display van de klok willen laten veranderen, roepen we de interne methode updateDisplay aan. In onze simulatie zal deze methode de displaystring wijzigen (we zullen de broncode daarvoor zo meteen zien). In een echte klok zou deze methode ook aanwezig zijn om de echte tijdaanduiding aan te passen. Naast de displaystring heeft de klasse ClockDisplay maar twee andere velden: hours en minutes. Elk van deze velden kan een object van het type NumberDisplay bevatten. De logische waarde van het display van de klok (de huidige tijd) wordt opgeslagen in deze NumberDisplay-objecten. In figuur 3.4 is een objectdiagram van deze toepassing weergegeven voor het moment 15:23.
myDisplay: ClockDisplay
:NumberDisplay
hours
limit
24
minutes
value
15
:NumberDisplay limit
60
value
23
Figuur 3.4 Objectdiagram van het klokdisplay 1.
84
e map met projecten bij het boek bevat ook een versie van dit project met een eenvoudige grafische D gebruikersinterface (GUI), genaamd clock-display-with-GUI. De geïnteresseerde lezer vindt het wellicht leuk met dit project te experimenteren; het komt echter niet aan de orde in dit boek.
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
3.9
Objecten die objecten maken
De eerste vraag die we ons moeten stellen is de volgende: waar komen deze drie objecten vandaan? Wanneer we een klokdisplay willen gebruiken zouden we ook een ClockDisplay-object kunnen maken. We nemen dan aan dat dit klokdisplay uren en minuten weergeeft. Door eenvoudigweg een klokdisplay te maken, verwachten we impliciet dat we twee getaldisplays gemaakt hebben voor de uren en minuten. CONCEPT
Objecten maken. Objecten kunnen andere objecten maken met behulp van de opera tor new. Omdat wij de klasse ClockDisplay zelf schrijven, is het ook onze taak om dat te laten gebeuren. We schrijven de code in de constructor van de klasse ClockDisplay die twee NumberDisplay-objecten maakt en opslaat. Omdat de constructor automatisch wordt uitgevoerd wanneer een ClockDisplay-object gemaakt wordt, zullen tegelijkertijd ook de NumberDisplay-objecten automatisch worden gemaakt. Hier is de code van de ClockDisplay-constructor die dit voor elkaar krijgt: public class ClockDisplay { private NumberDisplay hours; private NumberDisplay minutes; De overige velden zijn weggelaten. public { }
ClockDisplay() hours = new NumberDisplay(24); minutes = new NumberDisplay(60); updateDisplay();
De methodes zijn weggelaten.
}
De eerste twee regels in de constructor maken elk een nieuw NumberDisplay-object en wijzen deze toe aan een variabele. De syntax voor het maken van een nieuw object is: new ClassName (parameterlijst)
De bewerking new doet twee dingen: 1. maakt een nieuw object van de genoemde klasse (in dit geval NumberDisplay); 2. voert de constructor van die klasse uit. Als de constructor van de klasse zo gedefinieerd is dat hij parameters heeft, moeten de actuele parameters in het new-statement opgegeven worden. De constructor van klasse NumberDisplay verwacht bijvoorbeeld een geheel getal als parameter: public NumberDisplay (int rollOverLimit)
85
DEEL 1 | Basisprincipes van objectoriëntatie
De functie new voor de klasse NumberDisplay, die deze constructor aanroept, moet dus een actuele parameter van het type int leveren die past in de header die voor de constructor gedefinieerd is:
new NumberDisplay (24);
Datzelfde geldt ook voor de methodes die we bespraken in paragraaf 2.4. Met deze constructor hebben we bereikt wat we wilden: als iemand nu een ClockDisplay-object maakt, zal de ClockDisplay-constructor automatisch worden uitgevoerd en twee NumberDisplay-objecten maken. Op dat moment is de klok gereed voor gebruik.
Oefening 3.23 Maak een ClockDisplay-object door de volgende constructor te selecteren:
new ClockDisplay()
Roep de methode getTime ervan aan om te zien op welke tijd de klok is ingesteld. Kun je bedenken waarom de klok op die bepaalde tijd start? Oefening 3.24 Hoe vaak zou je de methode timeTick van een nieuw ClockDisplayobject moeten aanroepen om deze 01:00 te laten bereiken? Op welke andere manier zou je de klok deze tijd kunnen laten aangeven? Oefening 3.25 Maak een NumberDisplay-object met een maximum van 80 in Evua lueer window door de volgende regel in te voeren:
NumberDisplay nd = new NumberDisplay(80);
Roep vervolgens de methodes getValue(), setValue(int waarde) en increment() in het evaluatievak aan. Dat kun je doen door bijvoorbeeld nd.getValue() in te voeren (zie ook paragraaf 3.11.2 over de puntnotatie). Merk op dat statements (mutators) aan het eind gevolgd moeten worden door een puntkomma. Bij expressies (accessors) is dat niet het geval. Oefening 3.26 Schrijf de signatuur van een constructor die overeenkomt met de volgende instructie voor het maken van een object:
new Editor(“readme.txt”, –1)
Oefening 3.27 Schrijf Java-statements om een variabele met de naam window te definiëren van het type Rectangle, maak dan een rechthoekobject en ken dit toe aan die variabele. De constructor van de rechthoek heeft twee parameters van het type int.
3.10
Meerdere constructors
Mogelijk heb je opgemerkt dat bij het maken van een ClockDisplay-object zich een contextmenu opende waarin je twee mogelijkheden kreeg:
86
new ClockDisplay() new ClockDisplay(int hour, int minute)
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
CONCEPT
Overladen. Een klasse kan meer dan één constructor of meer dan één methode met dezelfde naam bevatten, zolang ze allemaal maar een andere set parametertypes accepteren. De reden daarvan is dat de klasse ClockDisplay twee constructors bevat. Op die manier kun je een ClockDisplay-object op twee verschillende manieren initialiseren. Als je de constructor zonder parameters gebruikt, zal de starttijd van de klok 00:00 zijn. Als je echter een klok wilt maken met een andere starttijd, kun je dat gemakkelijk doen met de tweede constructor. Het komt vaak voor dat klassendefinities verschillende versies van constructors of methodes bevatten waarmee een bepaalde taak op verschillende manieren kan worden uitgevoerd door een andere set parameters te gebruiken. Dit wordt overladen van een constructor of methode genoemd.
Oefening 3.28 Bekijk de tweede constructor in de broncode van ClockDisplay. Leg uit wat deze doet en hoe. Oefening 3.29 Bespreek de overeenkomsten en verschillen tussen de twee constructors. Waarom wordt updateDisplay bijvoorbeeld niet aangeroepen in de tweede constructor?
3.11 3.11.1
Methodeaanroepen Interne methodeaanroepen
De laatste regel van de eerste ClockDisplay-constructor bestaat uit het statement
updateDisplay();
CONCEPT
Methodes kunnen andere methodes van dezelfde klasse aanroepen. Dit wordt een interne methodeaanroep genoemd. Dit statement is een methodeaanroep. Zoals we eerder hebben gezien heeft de klasse ClockDisplay een methode met de volgende signatuur:
private void updateDisplay()
De methodeaanroep updateDisplay() roept deze methode aan. Omdat deze methode zich in dezelfde klasse bevindt als de aanroep van de methode, spreken we ook wel van een interne methodeaanroep. Interne methodeaanroepen hebben de syntax: naamVanDeMethode (parameterlijst)
87
DEEL 1 | Basisprincipes van objectoriëntatie
In ons voorbeeld heeft de methode geen parameters, zodat de parameterlijst leeg is. Dit wordt aangeduid door de twee lege haakjes. Wanneer in de broncode een methodeaanroep staat, wordt de bijbehorende methode uitgevoerd, wordt er naar de methodeaanroep teruggekeerd, en wordt daarna het volgende statement na de aanroep uitgevoerd. De header van een methode past alleen bij een methodeaanroep als zowel de naam als de parameterlijst van de methode overeenstemmen. In dit geval zijn beide parameterlijsten leeg en dus stemmen de signatuur van de methode en de methodeaanroep overeen. De voorwaarde dat zowel de naam van de methode als de parameterlijsten moeten kloppen, is erg belangrijk, omdat er meer dan één methode met dezelfde naam in een klasse kan voorkomen – namelijk als die methode overladen is. In ons voorbeeld dient de methodeaanroep om de displaystring bij te werken. Nadat de twee getaldisplays gemaakt zijn, wordt de displaystring ingesteld om de tijd weer te geven die is opgeslagen in de getaldisplayobjecten. De uitwerking van de methode updateDisplay zullen we zo meteen bespreken.
3.11.2
Externe methodeaanroepen
We zullen nu de volgende methode bekijken, de methode timeTick. De definitie is: public void timeTick() { minutes.increment(); if(minutes.getValue() == 0) { // Maximum bereikt! hours.increment(); } updateDisplay(); }
CONCEPT
Methodes kunnen methodes van andere objecten aanroepen met behulp van de puntnotatie. Dit wordt een externe methodeaanroep genoemd. Als dit display verbonden zou zijn met een echte klok, zou deze methode elke 60 seconden door de elektronische timer van de klok worden aangeroepen. Voor dit moment zullen we dat echter zelf doen als test van het display. Wanneer de methode timeTick aangeroepen wordt, voert deze eerst de volgende regel uit
minutes.increment();
Dit statement roept de methode increment van het minutes-object aan. Op die manier zal, wanneer een van de methodes van het ClockDisplay-object aangeroepen wordt, deze methode op zijn beurt weer een methode van een ander object aanroepen om een bepaald deel van de opdracht uit te voeren. Een methodeaanroep naar een methode van een ander object wordt een externe methodeaanroep genoemd. De syntax van een externe methodeaanroep is object.naamVanDeMethode(parameterlijst) 88
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Deze syntax staat bekend als de puntnotatie. De puntnotatie bestaat uit een objectnaam, een punt, de methodenaam en de parameters voor de aanroep. Het is erg belangrijk om in te zien dat we hier de naam van een object gebruiken en niet de naam van een klasse. We gebruiken de naam minutes in plaats van NumberDisplay. De methode timeTick heeft vervolgens een if-statement om te controleren of de uren ook opgehoogd moeten worden. Als onderdeel van de voorwaarde in het ifstatement roept het een andere methode van het minutes-object aan: getValue. Deze methode retourneert de huidige waarde van de minuten. Als die waarde nul is, weten we dat het display zojuist teruggesprongen is naar nul, en is het duidelijk dat de uren moeten worden opgehoogd. Dat is precies wat de code doet. Als de waarde van de minuten ongelijk is aan nul hoeft er niets te gebeuren: de waarde van de uren hoeft niet gewijzigd te worden. Het if-statement heeft daarom geen else nodig. Nu moeten ook de resterende drie methodes van de klasse ClockDisplay duidelijk zijn (zie code 3.4). De methode setTime accepteert twee parameters – de urenaanduiding en de minutenaanduiding – en stelt de klok in op de ingevoerde tijd. Als we de body van de methode beter bekijken, zien we dat dit bereikt wordt door de methodes setValue van beide getaldisplays aan te roepen; die voor de uren en die voor de minuten. Dan wordt de methode updateDisplay aangeroepen om de displaystring daarop aan te passen, op dezelfde manier als de constructor dat doet. De methode getTime is niet belangrijk – deze retourneert de huidige display string. Omdat de displaystring altijd actueel is, hoeven we verder niets te doen. De methode updateDisplay ten slotte is verantwoordelijk voor het actualiseren van de displaystring, zodat de string altijd de tijd bevat die wordt voorgesteld met de twee getaldisplayobjecten. Deze methode wordt steeds aangeroepen als de tijd van de klok verandert. Ze werkt door de methodes getDisplayValue van alle NumberDisplayobjecten aan te roepen. Deze methodes retourneren de waarde van elk afzonderlijk getaldisplay. Vervolgens worden deze twee waarden met een dubbele punt ertussen aan elkaar geplakt tot één enkele string.
Oefening 3.30 Gegeven een variabele
Printer p1;
die op dit moment een verwijzing naar een printerobject bevat, en ook gegeven twee methodes binnen de klasse Printer met de headers
public void print(String filename, boolean doubleSided) public int getStatus(int delay)
Schrijf twee mogelijke aanroepen van elk van deze methodes.
3.11.3
Samenvatting van het klokdisplay
Het is belangrijk om even stil te staan bij de manier waarop het voorbeeld in dit hoofdstuk abstractie gebruikt heeft om het probleem op te splitsen in kleinere onderdelen. Als je de broncode van de klasse ClockDisplay beter bekijkt, zul je zien dat we
89
DEEL 1 | Basisprincipes van objectoriëntatie
gewoon een NumberDisplay-object maken, zonder dat we ons druk maken over de manier waarop het werkt. Vervolgens roepen we de methodes (increment en getvalue) van dat object aan. Op dit niveau nemen we gewoon aan dat increment de waarde van het display correct zal ophogen, zonder ons af te vragen hoe dat gebeurt. In projecten worden deze verschillende klassen vaak door verschillende mensen geschreven. Je zult er op dit moment al wel het belang van hebben ingezien dat samenwerkende programmeurs goede afspraken maken over welke methodeheaders een klasse moet hebben en wat deze methodes precies moeten doen. Zo kan de ene programmeur zich concentreren op het schrijven van de methodes, terwijl de andere ze gewoon kan gebruiken. De set methodes die een object beschikbaar stelt aan andere objecten wordt de interface van dat object genoemd. We zullen hier later in dit boek uitgebreid op terugkomen.
Oefening 3.31 Uitdaging Verander de klok van een 24-uursklok in een 12-uursklok. Pas op: dit is minder gemakkelijk dan het op het eerste gezicht lijkt. Bij een 12-uursklok worden de uren na middernacht en na de middag niet weergegeven als 00:30 maar als 12:30. Het minutendisplay toont waarden van 0 tot en met 59, maar de urenaanduiding toont waarden van 1 tot en met 12! Oefening 3.32 Er zijn (ten minste) twee manieren waarop je een 12-uursklok kunt maken. Eén mogelijkheid is om uurwaarden van 1 tot en met 12 op te slaan. Een andere mogelijkheid is dat je de klok intern ook gewoon als een 24-uursklok laat werken, maar de displaystring van het klokdisplay aanpast zodat die 4:23 of 4.23 pm aangeeft wanneer de interne waarde 16:23 is. Programmeer beide varianten. Welke optie is het eenvoudigst? Welke is beter? Waarom?
3.12
Een ander voorbeeld van interactie tussen objecten
We zullen nu dezelfde concepten bekijken aan de hand van een ander voorbeeld, met andere technieken. We hebben het nog steeds over de manier waarop objecten andere objecten maken en hoe objecten elkaars methodes aanroepen. In de eerste helft van dit hoofdstuk hebben we de meest fundamentele techniek gebruikt om een bepaald programma te analyseren: het lezen van de code. De vaardigheid om broncode te lezen en te begrijpen, is een van de meest essentiële vaardigheden waarover een softwareontwikkelaar moet beschikken, en we zullen die vaardigheid bij elk project waaraan we werken nodig hebben. Maar soms is het handig om extra technieken te hebben om beter te begrijpen hoe een programma wordt uitgevoerd. We zullen daarom nu een gereedschap bekijken waarmee dat kan: de debugger. CONCEPT
Een debugger is een programma waarmee programmeurs een toepassing stap voor stap uitvoeren. De debugger kan worden gebruikt om programmeerfouten op te sporen. 90
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Een debugger is een programma waarmee programmeurs een toepassing stap voor stap uitvoeren. Meestal wordt een debugger gebruikt om een programma op bepaalde punten in de broncode te stoppen en te starten en de waarden van variabelen te bestuderen. De naam ‘debugger’ Fouten in computerprogramma’s worden gewoonlijk ‘bugs’ genoemd. Programma’s waarmee het makkelijker wordt om fouten te herstellen, worden daarom debuggers genoemd. Het is niet helemaal zeker waar de term bug vandaan komt. Er is een beroemd geval van wat wel de eerste computerbug genoemd wordt: in 1945 werd een echt insect (Engels: bug), om precies te zijn een mot, gevonden in de Mark II-computer van de computerpionier Grace Murray Hopper. In het National Museum of American History van het Smithsonian Institute is een logboek aanwezig met daarin een pagina waarop deze mot met plakband is vastgezet en de opmerking ‘first actual case of a bug being found’. De formulering suggereert echter dat de term bug al werd gebruikt, nog voordat deze mot een probleem veroorzaakte in de Mark II. Als je ‘first computer bug’ invoert in een zoekmachine zul je zelfs een afbeelding van de betreffende mot vinden!
Debuggers zijn er van erg eenvoudig tot uiterst complex. De debuggers voor professionele ontwikkelaars hebben een groot aantal functies voor geavanceerde analyses van veel aspecten van een toepassing. BlueJ heeft een ingebouwde debugger die veel minder uitgebreid is. We kunnen er onze programma’s mee stoppen, de code per regel doorlopen en de waarden van de variabelen ermee controleren. Hoewel deze debugger erg eenvoudig lijkt, levert hij ons toch een schat aan informatie op. Voor we met de debugger aan de slag gaan, zullen we eerst het voorbeeld dat we daarvoor zullen gebruiken beter bekijken: een simulatie van een e-mailsysteem.
3.12.1
Een voorbeeld van een e-mailsysteem
We zullen eerst de functies van het project mail-system bekijken. Op dit moment is het onbelangrijk om de broncode te bestuderen. We zullen het bestaande project uitvoeren om meer inzicht te krijgen in wat het doet.
Oefening 3.33 Open het project mail-system dat je kunt vinden op de cd bij dit boek. Het idee van dit project is te simuleren hoe gebruikers berichten aan elkaar sturen. Een gebruiker gebruikt een mailclient om berichten naar een server te verzenden, die bestemd zijn voor de mailclient van een andere gebruiker. Maak eerst een MailServerobject. Maak nu ook een MailClient-object voor één van de gebruikers. Wanneer je de client maakt, moet je een MailServer-instantie als parameter specificeren. Gebruik de mailserver die je zojuist gemaakt hebt. Ook moet je een gebruikersnaam
91
DEEL 1 | Basisprincipes van objectoriëntatie
voor de mailclient invoeren. Maak nu op dezelfde manier een tweede MailClient met een andere gebruikersnaam. Experimenteer met de MailClient-objecten. Ze kunnen gebruikt worden om berichten van de ene mailclient naar een andere te verzenden (met behulp van de methode sendMailItem) en berichten te ontvangen (met behulp van de methodes getNextMailItem of printNextMailItem).
Als we het mailsysteemproject beter bekijken zien we het volgende: • Het heeft drie klassen: MailServer, MailClient en MailItem. • Er moet een mailserverobject gemaakt worden dat door alle mailclients wordt gebruikt. Het verwerkt de uitwisseling van berichten. • Het is mogelijk om verschillende mailclientobjecten te maken. Elke mailclient heeft een eigen gebruikersnaam. • Berichten kunnen worden verzonden van de ene mailclient naar een andere via een methode in de klasse mailclient. • Berichten kunnen een voor een van de server ontvangen worden door een mailclient, met behulp van een methode in de mailclient. • De klasse MailItem wordt nooit door de gebruiker zelf gebruikt. Deze wordt in de mailclients en de server gebruikt om berichten te maken, op te slaan en uit te wisselen. Oefening 3.34 Teken een objectdiagram van de situatie die ontstaan is nadat je mailserver en drie mailclients hebt gemaakt. Objectdiagrammen hebben we besproken in paragraaf 3.6.
De drie klassen verschillen in complexiteit. MailItem is redelijk eenvoudig. We zullen er maar één klein stukje van bespreken zodat je de rest ervan zelf kunt onderzoeken. MailServer is op dit moment redelijk complex. Het maakt gebruik van concepten die pas een heel stuk verderop in dit boek worden besproken. We zullen deze klasse dus op tdit moment laten voor wat die is. We gaan er gewoon vanuit dat deze werkt – nog een voorbeeld van de manier waarop abstractie wordt gebruikt om details te verbergen waar we ons niet druk om hoeven te maken. De klasse MailClient is de interessantste en daarom zullen we die wel beter bekijken.
3.12.2
Het sleutelwoord this
Van de klasse MailItem zullen we alleen de constructor bespreken. Deze gebruikt een Java-constructie die we nog niet eerder tegengekomen zijn. De broncode is weergegeven in code 3.5.
92
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Code 3.5 Velden en constructor van de klasse MailItem public class MailItem {
// De verzender van het item. private String from; // De beoogde ontvanger. private String to; // De tekst van het bericht. private String message;
/** * Maak een mailitem van een verzender voor een bepaalde * ontvanger met daarin het bericht. * @param from De verzender van dit item. * @param to De beoogde ontvanger van dit item. * @param message De tekst van het te verzenden bericht. */ public MailItem(String from, String to, String message) { this.from = from; this.to = to; this.message = message; } }
De methodes zijn weggelaten.
De nieuwe Java-constructie die we in dit fragment tegenkomen is het sleutelwoord this:
this.from = from;
De hele regel is een toekenningsstatement. Hierin wordt de waarde aan de rechterkant (from) toegekend aan de variabele aan de linkerkant (this.from). De reden voor het gebruik van deze constructie is dat hier een situatie ontstaat die overladen (Engels: name overloading) wordt genoemd: dezelfde naam wordt gebruikt voor twee verschillende entiteiten. De klasse bevat drie velden: from, to en message. De constructor heeft drie parameters, ook met de namen from, to en message! Dus hoeveel variabelen zijn er op het moment dat de constructor wordt uitgevoerd? Het antwoord is zes: drie velden en drie parameters. Het is belangrijk te begrijpen dat de velden en de parameters afzonderlijke variabelen zijn die onafhankelijk van elkaar kunnen bestaan, zelfs als ze dezelfde naam hebben. In Java is het geen probleem wanneer een parameter en een veld dezelfde naam hebben. Het is wel een probleem dat we moeten kunnen verwijzen naar deze zes variabelen op zo’n manier dat we ze uit elkaar kunnen houden. Als we gewoon de variabele naam from gebruiken in de constructor, bijvoorbeeld in een statement Systeem.out. println(from), is het dan duidelijk welke variabele zal worden gebruikt, de parameter of het veld? De Java-specificatie bevat het antwoord op deze vraag. Hierin wordt gespecificeerd
93
DEEL 1 | Basisprincipes van objectoriëntatie
dat de definitie van het dichtstbijzijnde blok zal worden gebruikt. Omdat de parameter from gedefinieerd is in de constructor en het veld from in de klasse, zal de parameter worden gebruikt. De definitie daarvan is ‘dichterbij’ de regel die er gebruik van maakt. We hebben nu alleen nog maar een mechanisme nodig om een veld te benaderen wanneer er een dichterbij gedefinieerde variabele met dezelfde naam bestaat. Dat is waar het sleutelwoord this voor wordt gebruikt. De expressie this verwijst naar het huidige object. De notatie this.from verwijst naar het veld from in het huidige object. Met deze constructie kunnen we dus naar het veld verwijzen, in plaats van naar de parameter met dezelfde naam. We kunnen nu het toekenningsstatement nog een keer lezen:
this.from = from;
Dit statement, zoals we nu zien, heeft het volgende effect:
veld met de naam from = parameter met de naam from;
Met andere woorden: het statement kent de waarde van de parameter toe aan het veld met dezelfde naam. Dit is precies wat we nodig hebben om het object op de juiste manier te initialiseren. Toch blijft er een vraag hangen: waarom doen we dit allemaal? Het hele probleem kan gemakkelijk voorkomen worden door de velden en de parameters eenvoudigweg verschillende namen te geven. De reden om dit allemaal te doen is de leesbaarheid van de broncode. Soms is er een naam die perfect beschrijft waarvoor een variabele dient. Die naam is dan zo passend dat het zonde is om een andere naam te verzinnen. We willen de naam gebruiken voor de parameter, zodat de aanroeper weet welke invoer nodig is, en we willen de naam gebruiken voor het veld zodat degene die de klasse schrijft, weet waar het veld voor wordt gebruikt. Als een naam een perfecte beschrijving is, is het zinnig om die voor beide te gebruiken en de moeite te nemen om het sleutelwoord this te gebruiken in het toekenningsstatement en zo het naamconflict op te lossen.
3.13
Een debugger gebruiken
De interessantste klasse in het mailsysteemvoorbeeld is de mailclient. We zullen deze onderzoeken met een debugger. De mailclient heeft drie methodes: getNextMailItem, printNextMailItem en sendMailItemMessage. We zullen eerst de methode printNextMailItem bekijken. Voor we beginnen met de debugger moeten we een situatie verzinnen die we bij ons onderzoek kunnen gebruiken (oefening 3.35). Oefening 3.35 Verzin een situatie die we kunnen onderzoeken: maak een mailserver, maak vervolgens twee mailclients voor de gebruikers Sophie en Juan (noem de instanties ook sophie en juan, zodat je ze beter uit elkaar kunt houden in de objectenbank). Gebruik dan de methode sendMailItem van sophie om een bericht aan juan te verzenden. Lees het bericht nog niet.
94
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Nadat je oefening 3.35 hebt uitgevoerd, hebben we een situatie waarin een mailitem voor Juan op de server is opgeslagen en dat nog moet worden opgehaald. We weten dat de methode printNextMailItem dit mailitem ophaalt en op het scherm afdrukt. Nu kunnen we onderzoeken hoe dit in zijn werk gaat.
3.13.1
Afbreekpunten instellen
Om ons onderzoek te beginnen kunnen we een afbreekpunt instellen (oefening 3.36). Een afbreekpunt is een vlaggetje dat bij een regel broncode wordt geplaatst waardoor de uitvoering van een methode wordt onderbroken op het moment dat het vlaggetje bereikt wordt. In de BlueJ-editor wordt een afbreekpunt weergegeven als een klein stopbord (figuur 3.5). Je kunt een afbreekpunt instellen door de BlueJ-editor te openen, de betreffende regel te markeren (in ons geval de eerste regel van de methode printNextMailItem) en dan de optie Afbreekpunt aan/uit in het menu Gereedschappen van de editor te selecteren. Je kunt ook simpelweg klikken in het gebied naast de regel code waar het afbreekpuntsymbool wordt weergegeven om een afbreekpunt in te stellen of juist te verwijderen. Merk op dat de klasse eerst gecompileerd moet zijn om dit te kunnen doen. Oefening 3.36 Open de editor voor de klasse MailClient en stel een afbreekpunt in op de eerste regel van de methode printNextMailItem, zoals is weergegeven in figuur 3.5.
Figuur 3.5 Een afbreekpunt in de BlueJ-editor
Zodra je het afbreekpunt hebt ingesteld, voer je de methode printNextMailItem in de mailclient van Juan uit. Het editorvenster voor de klasse MailClient en een debuggervenster worden geopend (figuur 3.6). Aan de onderzijde van het debuggervenster vind je enkele besturingsknoppen.
95
DEEL 1 | Basisprincipes van objectoriëntatie
Hiermee kun je de uitvoering van het programma voortzetten of onderbreken. (Zie bijlage F voor een gedetailleerdere uitleg van de bedieningselementen van de debugger.) Aan de rechterkant van het debuggervenster vind je drie gebieden voor variabelen met de koppen static variables, instance variables en local variables. Op dit moment zullen we het vak met de statische variabelen laten voor wat het is. Deze klasse heeft er geen; we zullen er later op terugkomen. We zien dat het object twee instantievariabelen (of velden) heeft, server en user en we kunnen hun huidige waarden zien. De variabele user bevat de string “Juan” en de variabele server bevat een verwijzing naar een ander object. De objectverwijzing is datgene wat we als een pijl in de objectdiagrammen tekenen. Merk op dat er nog geen lokale variabele is. Dit komt doordat de uitvoering stopt voordat de regel met het afbreekpunt uitgevoerd is. Omdat de regel met de afbreekpunt de declaratie bevat van de enige lokale variabele, en die regel nog niet is uitgevoerd, bestaat er nog geen lokale variabele op dit moment.
Figuur 3.6 Het debuggervenster. De uitvoering is gestopt bij een afbreekpunt
Met de debugger kunnen we niet alleen de uitvoering van het programma onderbreken en de inhoud van variabelen bekijken, maar we kunnen er ook één regel code mee uitvoeren.
3.13.2
Stap voor stap debuggen
Wanneer de uitvoering van een programma gestopt is op een afbreekpunt kun je, door te klikken op de knop Stap, één enkele regel code uitvoeren waarna de uitvoering vervolgens automatisch weer onderbroken wordt.
96
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Oefening 3.37 Stap één regel verder in de uitvoering van de methode printNextMailItem door te klikken op de knop Stap.
Het resultaat van het uitvoeren van de eerste regel van de methode printNextMail Item is weergegeven in figuur 3.7. We kunnen zien dat de uitvoering één regel verder is gevorderd (een kleine zwarte pijl naast de regel broncode geeft de huidige positie aan) en de lijst met lokale variabelen, die weergegeven is in het debuggervenster, geeft aan dat er een lokale variabele gemaakt is waaraan een object toegekend is.
Figuur 3.7 Opnieuw gestopt na één stap
Oefening 3.38 Voorspel welke regel gemarkeerd zal zijn als de volgende regel code na de volgende stap uitgevoerd is. Voer vervolgens één stap uit en controleer of je voorspelling correct was. Had je gelijk of niet? Leg uit wat er gebeurde en waarom.
We kunnen nu steeds opnieuw op de knop Stap klikken tot het eind van de methode bereikt is. Op die manier kunnen we zien hoe het programma wordt uitgevoerd. Dit is met name interessant bij conditionele statements: we kunnen goed zien welke vertakking van een if-statement uitgevoerd wordt en zo controleren of dat klopt met hetgeen we verwachtten.
97
DEEL 1 | Basisprincipes van objectoriëntatie
Oefening 3.39 Roep dezelfde methode (printNextMailItem) nogmaals aan. Stap nogmaals door de methode. Verklaar wat je ziet.
3.13.3
Een methode binnenstappen
Toen we de methode printNextMailtem stap voor stap doorliepen hebben we twee methodeaanroepen naar objecten van onze eigen klassen gezien. De regel
MailItem item = server.getNextMailItem(user);
bevat een aanroep van de methode getNextMailItem van het server-object. Als we declaraties van de instantievariabele bekijken, kunnen we zien dat het server-object gedeclareerd is als klasse MailServer. De regel
item.print();
roept de methode print van het item-object aan. In de eerste regel van de methode printNextMailItem kunnen we zien dat item gedeclareerd wordt als klasse MailItem. Met de opdracht Stap in de debugger hebben we abstractie gebruikt: we hebben de methode print van het item-object beschouwd als één enkele instructie en we konden zien dat het effect daarvan is dat de informatie (verzender, geadresseerde en bericht) van het mailitem wordt afgedrukt. Als we ons meer op de details richten kunnen we dieper in het proces kijken en ook de methode print zelf stap voor stap uitvoeren. Dit kunnen we doen met de opdracht Stap dieper in de debugger, in plaats van met de opdracht Stap. Met Stap dieper wordt de aangeroepen methode stap voor stap uitgevoerd en de uitvoering wordt gestopt bij de eerste regel binnen die methode.
Oefening 3.40 Maak opnieuw dezelfde testsituatie als we eerder deden. Dat wil zeggen: verzend een bericht van Sophie aan Juan. Voer vervolgens opnieuw de methode printNextMailItem van de mailclient van Juan uit. Voer opnieuw één stap uit. Gebruik echter deze keer, wanneer je de regel
item.print();
bereikt, de opdracht Stap dieper in plaats van de opdracht Stap. Zorg ervoor dat je de tekstterminal kunt zien terwijl de stap wordt uitgevoerd. Wat zie je? Leg uit.
3.14
Nog een keer: methodeaanroepen
In de experimenten in paragraaf 3.13 hebben we een ander voorbeeld gezien van objectinteractie, dat vergelijkbaar is met iets wat we eerder zagen: objecten roepen methodes van andere objecten aan. In de methode printNextMailItem roept het MailClient-object het MailServer-object aan om het volgende mailitem op te halen. Deze methode (getNextMailItem) retourneerde een waarde – een object van het type MailItem. Dan was er ook nog een aanroep van de methode print van het mailitem. 98
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
Als we hierop abstractie toepassen, kunnen we de methode print beschouwen als één enkele opdracht. Of, als we in meer detail geïnteresseerd zijn, kunnen we één abstractieniveau lager gaan en ook binnen in de methode print kijken. Op die manier kunnen we de debugger gebruiken en zien hoe een object een ander object maakt. De methode sendMessage in de klasse MailClient is een goed voorbeeld. In deze methode wordt in de eerste regel code een MailItem-object gemaakt:
MailItem item = new MailItem(user, to, message);
Het idee hier is dat het mailitem wordt gebruikt als verpakking van een bericht. Het mailitem bevat informatie over de verzender, de ontvanger en het bericht zelf. Bij het verzenden van een bericht maakt een mailclient een mailitem met al deze informatie en slaat dit mailitem vervolgens op de mailserver op. Daar kan het later worden opgehaald door de mailclient van de ontvanger. In de regel code hierboven zien we dat new wordt gebruikt om het nieuwe object te maken en we zien dat de parameters aan de constructor worden doorgegeven. (Niet vergeten: bij het maken van een object gebeuren er twee dingen – het object wordt gemaakt en de constructor wordt uitgevoerd.) De constructor wordt op een vergelijkbare manier aangeroepen als de methodes. Dit kunnen we zien door de opdracht Stap dieper te kiezen bij de regel waar het object wordt gemaakt.
Oefening 3.41 Plaats een afbreekpunt in de eerste regel van de methode sendMail Item in de klasse MailClient. Verzend het bericht vervolgens. Gebruik de functie Stap dieper om de constructor van het mailitem binnen te stappen. In het debuggerscherm voor het MailItem-object kun je de instantievariabelen en de lokale variabelen bekijken die dezelfde namen hebben, zoals we hebben besproken in paragraaf 3.12.2. Stap verder om te zien hoe de instantievariabelen worden geïnitialiseerd. Oefening 3.42 Combineer het lezen van de code, het uitvoeren van methodes, afbreekpunten, en stap voor stap bekijken van de klassen MailItem en MailClient. Merk op dat we nog niet genoeg inzicht hebben in de klasse MailServer. Je kunt deze klasse dan ook voor dit moment negeren. (Je mag natuurlijk nieuwsgierig zijn, maar wees niet verbaasd als je het nog niet begrijpt...) Schrijf op hoe de klassen MailClient en Mail Item op elkaar werken. Teken objectdiagrammen ter ondersteuning van je betoog.
3.15
Samenvatting
In dit hoofdstuk hebben we besproken hoe een probleem opgesplitst kan worden in deelproblemen. We kunnen proberen subcomponenten aan te wijzen in de onderdelen die we willen modelleren, en we kunnen subcomponenten implementeren als onafhankelijke klassen. Op die manier wordt het programmeren van grotere toepassingen minder complex, omdat we de afzonderlijke klassen los van elkaar kunnen programmeren, testen en onderhouden. We hebben gezien hoe dit resulteert in bouwwerken van objecten die met elkaar samenwerken om een bepaalde taak uit te voeren. Objecten kunnen andere objecten
99
DEEL 1 | Basisprincipes van objectoriëntatie
creëren en ze kunnen elkaars methodes aanroepen. Een goed begrip van hoe objecten samenwerken is essentieel bij het ontwerpen, programmeren en debuggen van toepassingen. Om erachter te komen hoe een programma werkt of om fouten op te sporen, kunnen we getekende diagrammen gebruiken, de code lezen en debuggers inzetten.
Oefening 3.43 Gebruik de debugger om het project clock-display te controleren. Plaats afbreekpunten in de constructor en alle methodes van ClockDisplay en doorloop ze dan stap voor stap. Werkt alles naar verwachting? Heb je nieuwe dingen ontdekt? Zo ja, welke? Oefening 3.44 Gebruik de debugger om de methode insertMoney in het project better-ticket-machine uit hoofdstuk 2 te onderzoeken. Voer tests uit waardoor beide vertakkingen van het if-statement worden uitgevoerd. Oefening 3.45 Voeg een onderwerpregel voor een e-mailbericht toe aan alle mailitems in het project mail-system. Controleer of bij het afdrukken van de berichten ook de onderwerpregel wordt afgedrukt. Pas de mailclient daarop aan. Oefening 3.46 Veronderstel de volgende klasse (waarvan alleen fragmenten zijn weergegeven): public class Screen { public Screen(int xRes, int yRes) { ... }
public int numberOfPixels() { ... }
}
public void clear(boolean invert) { ... }
Schrijf een paar regels Java-code voor het maken van een Screen-object en roep vervolgens de methode clear ervan aan als (en alleen als) het aantal pixels groter is dan twee miljoen. (De logica hiervan is nu niet belangrijk; het doel is om iets te schrijven dat syntactisch correct is en dus zal compileren.)
100
HOOFDSTUK 3 | INTERACTIE TUSSEN OBJECTen
NIE U W E TER M EN IN D IT H OO F D ST U K
abstractie, modularisatie, verdeel-en-heers, klassendiagram, objectdiagram, objectverwijzing, overladen, interne methodeaanroep, externe methodeaanroep, verkorte notatie, debugger, afbreekpunt.
s a m e n vatt i n g va n d e co n c e p t e n
•
Abstractie Abstractie is de mogelijkheid om details van onderdelen te nege-
•
Modularisatie Modularisatie is het proces waarmee iets in goed gedefini-
• • •
• • • • • • •
ren om de aandacht te richten op een probleem op een hoger niveau. eerde delen wordt opgesplitst die afzonderlijk kunnen worden uitgewerkt en op goed gedefinieerde manieren samenwerken. Klassen definiëren types Een klassennaam kan worden gebruikt als het type voor een variabele. Variabelen die een klasse als hun type hebben, kunnen objecten van die klasse opslaan. Klassendiagram Het klassendiagram bevat de klassen van een toepassing en hun onderlinge relaties. Het bevat informatie over de broncode. Het is een statisch beeld van een programma. Objectdiagram Het objectdiagram toont de objecten en hun onderlinge relaties op een bepaald moment tijdens de uitvoering van een toepassing. Het geeft informatie over objecten terwijl het programma wordt uitgevoerd. Het geeft een dynamisch beeld van een programma. Objectverwijzingen Variabelen van objecttypes bevatten verwijzingen naar objecten. Primitieve types De primitieve types in Java zijn alle types die geen objecttype zijn. Types zoals int, boolean, char, double en long zijn de meest gebruikte primitieve types. Primitieve types hebben geen methodes. Objecten maken Objecten kunnen andere objecten maken met behulp van de operator new. Overladen Een klasse kan meer dan één constructor of meer dan één methode met dezelfde naam bevatten, zolang ze allemaal maar een andere set parametertypes accepteren. Interne methodeaanroep Methodes kunnen andere methodes van dezelfde klasse aanroepen. Dit wordt een interne methodeaanroep genoemd. Externe methodeaanroep Methodes kunnen methodes van andere objecten aanroepen met behulp van de puntnotatie. Dit wordt een externe methodeaanroep genoemd. Debugger Een debugger is een programma waarmee programmeurs een toepassing stap voor stap uitvoeren. De debugger kan worden gebruikt om programmeerfouten op te sporen.
101