Hoofdstuk 2: Objectgeörienteerd denken
Inleiding
- Het voorgaande hoofdstuk heeft ons een inleiding gebracht rond objecten en klassen. We kunnen klassen definiëren en opstelln en objecten gebruiken. Objectgeoriënteerd programmeren stelt ons in staat om meer te denken in termen van het probleem. We zullen ons bezighouden in dit hoofdstuk met het ontwerp van klassen en hoe we via objectgeoriënteerd programmeren efficienter kunnen programmeren.
Klasse abstractie en encapsulatie
Klasse abstractie is de scheiding van de implementatie en het gebruik van een klasse. De details van implementatie worden geëncapsuleerd en verborgen van de gebruiker.
- In de cursus 'Computers en Programmeren' hebben we geleerd over stapsgewijze verfijning waarin we methodes gebruiken om abstractie te voorzien. Abstractie is de manier waarop we methodes maken, zodat we de details van de implementatie van die methodes niet hoeven te weten.
- Klasse-abstractie scheidt de implementatie van de klasse en het gebruik van de klasse. De maker van de klasse beschrijft de methodes en de gegevensvariabelen en hoe de methodes geïmplementeerd worden, maar de gebruiker van de klasse hoeft niet te weten hoe de maker bepaalde methodes heeft gemaakt. Hij dient enkel te weten wat de methodes doen, welke argumenten ze nodig hebben en wat het resultaat is.
- Een laptop bevat vele componenten die met elkaar moeten samenwerken: een CPU, geheugen, een harde schijf, moederbord,... Elke component kan aanzien worden als een object met zijn eigenschappen (cfr. gegevensvariabelen) en zijn gedrag (cfr. methodes). Om alle componenten met elkaar te verbinden, hoef je niet te weten hoe elke component er van binnen uitziet. De interne implementatie van die componenten is verborgen.
- Beschouw een klasse
Lening
. Een lening kan aanzien worden als een object van die klasse. De interest, het geleende bedrag en de periode van de lening zijn eigenschappen van dat object en zijn beschreven in de klasse en zijn methodes kunnen zijn het maandelijkse te betalen bedrag en het totale bedrag. Een UML kan een ontwerp van zo'n klasse visualizeren.

- Als een gebruiker van de klasse
Lening
hoef je niet te weten hoe de methodes geïmplementeerd zijn. Je vertrouwt op het feit dat de persoon die de klasse gemaakt heeft, ze correct heeft geïmplementeerd. Vanuit de klasse kunnen we een klasse maken waarin we de klasseLening
gebruiken, waarbij we enkel het UML schema hebben als referentie.
- Op deze manier hebben we een klasse gebruikt zonder dat we weten hoe de klasse eruit ziet. We moeten wel de klasse in ons project steken, maar we hoeven de code niet te doorpluizen om te weten hoe we een object moeten gebruiken.
Denken in objecten
Proceduraal programmeren focust op het ontwerpen van methodes. Object-georiënteerd programmeren plaatst gegevens en methodes samen in objecten.
- In 'Computers en Programmeren' hebben we vooral gefocust op fundamentele programmeertechnieken om problemen op te lossen. We gebruikten daarbij lussen, methodes en arrays. Deze technieken geven een solide basis voor object-georienteerd programmeren. Klasses voorzien meer flexibiliteit en kunnen hergebruiktworden in andere software.
- In vorige cursussen hebben we methodes gezien voor o.a. het berekenen van de BMI.
- We zouden een static methode maken om de BMI te bereken. Bv:
public static double berekenBMI(double gewicht, double lengte)
- Dit maakt de berekening van de BMI herbruikbaar, maar dit heeft ook zijn beperkingen. Stel dat we het gewicht en lengte willen associëren met een naam en geboortedatum van een persoon. je zou kunnen variabelen maken, maar die zullen nooit sterk gekoppeld zijn aan het gewicht en lengte. De ideale manier is om de variabelen naam, geboortedatum, gewicht en lengte in objecten te definiëren. Hiervoor moeten we een klassebeschrijving maken. Een UML beschrijving kan zijn:

- Stel dat de BMI klasse reeds beschikbaar.
GebruikBMIKlasse.java
- We maken objecten van de BMI klasse (
bmi1
enbmi2
). Hierdoor kunnen we de methodesgetNaam()
,getBMI()
engetStatus()
gebruiken om informatie van het BMI object terug te geven. - De
BMI
klasse kan als volgt geïmplementeerd zijn:
BMI.java
- Dit voorbeeld illustreert de kracht van object-georiënteerd programmeren. De object-georiënteerde benadering verbindt gegevens en methodes in objecten. Software ontwikkeling met het object georienteerde paradigma focust op objecten en operaties die uitgevoerd worden op objecten.
Klasserelaties
Om een klassen te gaan ontwerpen, moet je de relaties tussen klassen opmaken. De meest voorkomende relaties zijn associatie, aggregatie, compositie en overerving
Klassen gebruiken in andere klassen
- Het komt regelmatig voor dat je andere klassen nodig hebt, wanneer je een bepaalde klasse maakt. Onbewust doen we dat al heel veel. Als je een klasse maakt waarvan de attributen van het type
String
zijn, dan declareren we een attribuut dat van een type is dat in een andere klasse beschreven is. - Stel dat we eigenschappen van een persoon willen in een klasse beschrijven steken. Een persoon heeft een naam, voornaam, leeftijd en een geboortedatum. Een naam en voornaam kan je gemakkelijk voor stellen door een stukje tekst, dus van het type
String
. Leeftijd kan je heel gemakkelijk voorstellen door een geheel getal, dus in javaint
. Een datum kan je gemakklijk voorstellen door een String, zoals "10 April 1980", maar hierdoor kunnen we geen berekeningen uitvoeren zoals het bepalen van de persoon die het eerst verjaart. We maken het ons heel moeilijk om een datum als stuk tekst voor te stellen. In feite kan je een datum gaan voorstellen door 3 getallen: een dag, een maand en een jaar. Die datum kan je dus voorstellen door een nieuw type die je zelf maakt en die je beschrijft in een klasse die zelf een naam geeft.
Een klassebeschrijven voor de klasse
Datum
ziet er als volgt uit:
Abstractie en Modularisatie
- Stel dat we een tijdsaanduiding willen aanmaken in Java. We willen een 24-uursdisplay maken van
00:00
(middernacht) tot23:59
. - In eerste instantie zouden we kunnen proberen om de hele tijdsaanduiding te maken in één enkele klasse. Maar als problemen uitgebreider worden, zullen klassen ook complexer worden. Tot nut toe maakten we klassen om één probleem op te lossen. Het zal snel blijken, dat weinig problemen maar te vatten zijn in één enkele klasse. Naarmate de omvang van het probleem toeneemt wordt het steeds lastiger om alle details ervan tegelijkertijd in de gaten te houden.
- Een oplossing om de complexiteit te reduceren is abstractie. Abstractie is de mogelijkheid om details van onderdelen te negeren om de aandacht te richten op een probleem op een hoger niveau.
- We splitsen het probleem op in subproblemen en vervolgens in subsubproblemen tot elk afzonderlijk probleem klein genoeg is om gemakkelijk te worden opgelost. We kunnen dan deze oplossing als bouwsteen gebruiken voor andere problemen. Deze techniek wordt ook wel verdeel-en-heers genoemd (Engels: divide and conquer).
- Een mooi voorbeeld is het ontwerp van een auto. Een auto is een heel complex systeem waarbij een technicus elk een klein deel van de auto ontwerpt (bv. boring van de cilinder, chemische samenstelling van de band,...). De ingenieur moet een globaal beeld krijgen van de auto en kan niet alle details van de auto overzien. Hij abstraheert de informatie van de technicus in wat hij relevant vindt.
- De auto wordt opgesplitst in onafhankelijke modules en mensen werken, onafhankelijk van elkaar, aan de afzonderlijke modules. Wanneer een module klaar is gebruiken ze abstractie, omdat die module (bv. band, motor,...) kan gebruikt worden in andere systemen (in dit geval auto's).
- Modularisatie is het proces waarmee iets in goed gedefinieerd delen wordt opgepslitst die afzonderlijk kunnen worden uitgewerkt en op een goed gedefinieerde manieren samenwerken.
Een voorbeeld van Abstractie en Modularisatie
- Laat ons dit toepassen in ons klokvoorbeeld. We kunnen de tijdsaanduiding opdelen in twee delen: twee cijfers voor het uur en twee cijfers voor de minuten. Elk deel is dus een twee-cijfering display die begint bij
00
en verhoogt worden naar01
, enz. tot het maximum bereikt is. Voor uren is dat tot 24 en voor de minuten is dat 60. - We kunnen zo'n twee-cijferig display gaan voorstellen met door een klasse. De eigenschappen van zo'n display is de huidige waarde en de maximumwaarden.
- We kunnen de klassenaam
TweeCijferDisplay
gebruiken als een type voor een variabele in een andere klasse. Variabelen die een klasse als type hebben, kunne objecten van die klasse opslaan. - Voor een klokdisplay moeten we dus twee twee-cijferige displays hebben. We kunnen een klasse
KlokDisplay
maken met twee gegevensvariabelen van het typeTweeCijferDisplay
.
- Een klassendiagramma bevat de klassen van een toepassing en hun onderlinge relaties. Het bevat informatie over de broncode. Het is een statisch beeld van een programma.
- Op deze manier gebruiken we een klasse in een andere klasse. Dit is een relatie en die kunnen we voorstellen in een klassediagram. We duiden een dergelijke relatie aan met een pijl. Die pijl van klasse
C1
naar klasseC2
geeft aan dat de klasseC1
objecten gebruikt van klasseC2
. - Het klassendiagramma ziet er dan als volgt uit:

- Een objectdiagramma toont de relaties en hun onderlinge relaties op een bepaald moment tijdens de uitvoering van een toepassing. Het geeft informatie over objecten terwijl het programma wordt uitgevoerd. Daarnaast geeft het een dynamisch beeld van een programma.
- Het is zo dat een object van het type
KlokDisplay
twee objecten van het typeTweeCijferDisplay
nodig heeft: eentje voor de uren en eentje voor de minuten. - Het objectendiagramma ziet er als volgt uit:

- Een mogelijke methode van KlokDisplay is het instellen van een tijd:
setTijd(int uur, int minuut)
. De methode heeft als doel om de waarde van elk van het twee-cijferig display te veranderen. Om het uur in te stellen, moeten we de waarde van uur veranderen. Aangezien uur een gegevensvariabele van het typeTweeCijferDisplay
is, kan dit door de waarde van dat object te veranderen. Een mogelijke implementatie zou zijn:
- Dit zal echter niet lukken omdat de gegevensvariabele
waarde
in de klasseTweeCijferDisplay
alsprivate
gedeclareerd is. En dit is juist de manier van hoe we het moeten doen. Voor het gemak kunnen we die gegevensvariablepublic
declareren, maar dat is heel slechte praktijk. We moeten een extra methode in de klasseTweeCijferDisplay
voorzien die ons mogelijk maakt om de waarde van de gegevensvariabelewaarde
te veranderen. Dit kan door een methodesetWaarde(int waarde)
aan te maken.
TweeCijferDisplay.java
KlokDisplay.java
Objecten die andere objecten maken
Objecten kunnen ander objecten maken met behulp van de operator new.
Voor het maken van een object van de klasse
KlokDisplay
, dienen we dus ook twee objecten van het typeTweeCijferDisplay
te maken. We kunnen dat zelf doen, of we kunnen dit doen tijdens het maken van een object vanKlokDisplay
. Het maken van een object wordt gedaan tijdens de aanroep van een Constructor.Omdat de constructor automatisch wordt uitgevoerd wanneer een
KlokDisplay
-object wordt aangemaakt, zullen tegelijkertijd ook tweeTweeCijferDisplay
-objecten aangemaakt worden. De constructor vanTweeCijferDisplay
wordt aangeroepen wanneer de constructor inKlokDisplay
wordt aangeroepen.
TweeCijferDisplay.java
KlokDisplay.java
Overladen van constructoren
Methodeaanroepen
Interne methodeaanroep: methodes kunnen andere methodes van dezelfde klasse aanroepen. Dit wordt een interne methodeaanroep genoemd.
Externe methodeaanroep: methodes kunnen ook methodes van andere klassen aanroepen met behulp van de punt-notatie. Dit wordt een externe methodeaanroep genoemd.
Associatie
- Associatie is een algemene relatie die de activiteit tussen twee klassen beschrijft. Bijvoorbeeld, een student die een cursus volgt is een associatie tussen de klasse
Student
en de klasseCursus
. Een docent doceert een cursus is een associatie tussenDocent
enCursus
.
- Een associatie wordt in een UML voorgesteld door een pijl. Soms wordt de aard van de associatie boven aan de pijl vermeld.
- Een klasse die betrokken is in een associatie kan worden gespecifieerd met een multipliciteit. Dit wordt meestal bij de klasse gezet en geeft aan hoeveel objecten van klassen betrokken zijn in de associatie. Het teken
*
geeft aan dat een oneindig aantal objecten van de klasse betrokken zijn en het intervalm..n
geeft aan dat tussenm
enn
objecten betrokken zijn in de relatie. - Bijvoorbeeld, elke cursus wordt gedoceerd door 1 docent en elke docent geeft 0 tot 3 vakken. Hierdoor kan je in de UML beschrijving een
1
plaatsen bij de klasseDocent
en0..3
bijCursus
. - Elke student kan inschrijven in elke cursus en elke cursus kan gegeven worden aan minstens 5 en maximaal 60 studenten. Hierdoor kan je een
*
een bijCursus
plaatsen en5...60
bijStudent
.
Student.java
Cursus.java
Docent.java
Aggregatie en Compositie
- Aggregatie is een speciale manier van associatie en stelt een "heeft-een"-relatie voor. Een dergelijke relatie geeft aan de een klasse een andere klasse bevat. De klasse die de andere klasse bevat noemen we ook de aggregerende klasse en de andere klasse is dan de geaggregeerde klasse.
- Een object kan verschillende andere objecten aggregeren. Als een object uitsluitend wordt omvat door een ander object, is deze relatie een compositie.
- Een aggregatie wordt gebruikelijk voorgesteld als een gegevensvariabele in een aggregerende klasse.
Naam.java
Student.java
Adres.java
- Aggregatie kan bestaan tussen objecten van dezelfde klasse. Bijvoorbeeld, een persoon kan een supervisor hebben en die wordt voorgesteld ook door een klasse
Persoon
.
Primitieve datatypes als objecten
Een primitief type is geen object, maar het kan wel gewikkeld worden in een Object
- Vanwege performantie worden primitieve dataypes niet voorgesteld als objecten in Java. Door de overhead in het verwerken van objecten gaat dit ten koste van de performantie van het programma als primitieve types zouden voorgesteld worden als objecten.
- Java biedt een manier aan om een primitief type in een object te plaatsen. Een klasse die een primitief type omvat noemt ook wel een
wikkelklasse
of - in het Engels -wrapperclass
. - Zo kan een
int
in eenInteger
klasse gewikkeld worden of eendouble
in eenDouble
of eenchar
in eenCharacter
klasse. - Java biedt de volgende wikkelklasses aan:
Boolean
,Character
,Double
,Float
,Byte
,Short
,Integer
enLong
. - Wikkelklasses die een numeriek type bevatten zijn vrij gelijkaardig. Elke klasse heeft een methode
doubleValue()
,floatValue()
,intValue()
,longValue()
,shortValue()
andbyteValue()
. Deze methodes "converteren" objecten in waardes van primitieve types. - Een object van een wikkelklasse kan gemaakt wordt via een primitief datatype of van een String. Bijvoorbeeld:
new Double(5.0)
,new Double("5.0")
,new Integer(5)
ennew Integer("5")
. - Een wikkelklasse heeft geen no-arg constructor en eens dat de objecten gemaakt zijn kunnen de gegevensvariabelen van de klasse niet meer verandert worden.
Elke numerieke wikkelklasse heeft de constanten
MAX_VALUE
enMIN_VALUE
.MAX_VALUE
is de maximale waarde die het corresponderende primitieve datatype kan bevatten.MIN_VALUE
is de minimale waardebyte
,short
,int
enlong
. VoorFloat
enDouble
isMIN_VALUE
de minimale positievefloat
endouble
.Elke numerieke wikkelklasse heeft de methodes
doubleValue()
,floatValue()
,intValue()
,longValue()
andshortValue()
om een waarde te retourneren van het typedouble
,float
,int
,long
ofshort
.- Daarnaast bevat elke numerieke wikkelklasse een methode
compareTo
om getallen te vergelijking. Bijvoorbeeld:
Automatische conversie tussen primitieve types en wikkelklasse
- Een waarde van een primitief type kan automatische geconverteerd worden naar een object via een wikkelklasse en vice versa.
- De conversie van een primitieve waarde naar een object van een numerieke wikkelklasse noemt men boxing. Andersom, van het object naar een waarde van een primitief type, noemt met het unboxing.
- De Java-compiler zal automatisch een primitieve waarde boxen en een object unboxen als de context een primitieve waarde vereist. Dit noemt ook wel autoboxing of autounboxing.
- In onderstaand voorbeeld wordt een geheel getal ge-box t
BigInteger
en BigDecimal
- in sommige gevallen moeten we werken met heel grote getallen of met kommagetallen met een heel hoge nauwkeurigheid. Zo hoog zelfs dat de primitieve datatypes niet meer voldoen. In dat geval kunnen we de in Java beschikkebare klassen
BigInteger
enBigDecimal
gebruiken in dejava.math
. Bijvoorbeeld:
BigInteger a = new BigInteger("9223372036854115807"); BigInteger b = new BigInteger("2"); BigInteger c = a.multiply(b); System.out.println(c);
Bij het berekenen van een faculteit botst men algauw tegen de bovengrens van de type aan. Onderstaand programma berekent de faculteit gebruik makend van de klasse
BigIteger
.import java.math.BigInteger; public class Faculteit{ public static void main(String[] args){ System.out.println("50! is \n" + faculteit(50)); } public static BigInteger faculteit(long n){ BigInteger resultaat = BigInteger.ONE; for(int i = 1; i <= n; i++){ resultaat = resultaat.multiply(new BigInteger(i + "")); } return resultaat; } }
BigInteger.ONE
is een constante gedefinieerd in de klasseBigInteger
en stelt de waarde 1 voor.
De klasse String
- Stukjes tekst of Strings zijn objecten die eigenschappen bevatten onder de vorm van gegevensvariabelen en gedrag onder de vorm van methodes. Zo kan je de methode
charAt(index)
toepassen op een String-object om het character op een bepaalde plaats op te halen op basis van een index. - De klasse
String
heeft 13 mogelijke constructoren en meer dan 40 methodes om stukken tekst te behandelen.
Constructie van een String
- Je kan een string object maken door een string literal of elke array van characters.
- Java behandelt een string literal als een object van het type
String
:
Vervangen en opsplitsen van Strings
- Strings hebben methodes om stukken van strings te vervangen of om strings op te splitsen in delen.