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.
UML diagramma van Lening
Figuur - UML diagramma van Lening
  • 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 klasse Lening gebruiken, waarbij we enkel het UML schema hebben als referentie.
import java.util.Scanner; public class TestLening{ /** Main methode */ public static void main(String[] args){ // Maak een scanner object zodat invoer mogelijk wordt Scanner invoer = new Scanner(System.in); // Vraag naar de jaarlijkse interestvoet in System.out.print("Voer de Jaarlijkse interestvoet in, bv. 8,25:"); double jaarlijkseInterestVoet = invoer.nextDouble(); // Vraag naar het aantal jaar System.out.print("Voer het aantal jaar in (geheel getal)"); int aantalJaar = invoer.nextInt(); // Vraag naar het te lene bedrag System.out.print("Voer het te lenen bedrag in:"); double leenBedrag= invoer.nextDouble(); // Maak een Lening object Lening lening = new Lening(jaarlijkseInterestVoet, aantalJaar, leenBedrag); // Toon de informatie: startDatum, maandelijkse betaling en het totaal betaalde bedrag System.out.println("De lening was gemaakto op " + lening.getStartDatum().toString() + "."); System.out.println("De maandelijkse betaling is " + lening.getMaandelijkseBetaling()); System.out.println("Het totaal betaalde bedrag is " + lening.getTotaalBetaling()); } }
  • 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.
import java.util.Scanner; public class BerekenBMI { public static void main(String[] args) { Scanner invoer = new Scanner(System.in); // Vraag de gebruiker om zijn gewicht in kg System.out.print("Voer gewicht in (in kg): "); double gewicht = invoer.nextDouble(); // Vraag de gebruiker naar zijn lengte in m System.out.print("Voer je lengte in (in m): "); double lengte = invoer.nextDouble(); // Bereken BMI double bmi = gewicht / (lengte * lengte); // Display result System.out.println("BMI is " + bmi); if (bmi < 18.5) System.out.println("Ondergewicht"); else if (bmi < 25) System.out.println("Normaal"); else if (bmi < 30) System.out.println("Overgewicht"); else System.out.println("Obees"); } }
  • 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:
UML beschrijving van de klasse BMI
Figuur - UML beschrijving van de klasse BMI
  • Stel dat de BMI klasse reeds beschikbaar.

GebruikBMIKlasse.java

public class GebruikBMIKlasse{ public static void main(String[] args){ BMI bmi1 = new BMI("Sofie Desmet", 18, 145,70); System.out.println("De BMI voor " + bmi1.getNaam() + " is " + bmi1.getBMI() + " " + bmi1.getStatus()); BMI bmi2 = new BMI("Steven Louagie", 214,80); System.out.println("De BMI voor " + bmi2.getNaam() + " is " + bmi2.getBMI() + " " + bmi2.getStatus()); } }
  • We maken objecten van de BMI klasse (bmi1 en bmi2). Hierdoor kunnen we de methodes getNaam(), getBMI() en getStatus() gebruiken om informatie van het BMI object terug te geven.
  • De BMI klasse kan als volgt geïmplementeerd zijn:

BMI.java

public class BMI { private String naam; private int leeftijd; private double gewicht; // in kilogram private double lengte; // in meter public BMI(String naam, int leeftijd, double gewicht, double lengte) { this.naam = naam; this.leeftijd = leeftijd; this.gewicht = gewicht; this.lengte = lengte; } public BMI(String naam, double gewicht, double lengte) { this(naam, 20, gewicht, lengte); } public double getBMI() { double bmi = gewicht / (lengte * lengte); return Math.round(bmi * 100) / 100.0; } public String getStatus() { double bmi = getBMI(); if (bmi < 18.5) return "Ondergewicht"; else if (bmi < 25) return "Normaal"; else if (bmi < 30) return "Overgewicht"; else return "Obees"; } public String getNaam() { return naam; } public int getLeeftijd() { return leeftijd; } public double getGewicht() { return gewicht; } public double getLengte() { return lengte; } }
  • 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 java int. 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:

TODO: figuur
Figuur - TODO: figuur

Abstractie en Modularisatie

  • Stel dat we een tijdsaanduiding willen aanmaken in Java. We willen een 24-uursdisplay maken van 00:00 (middernacht) tot 23: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 naar 01, 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.
public class TweeCijferDisplay{ private int maximum; private int waarde; //Constructors en methodes zijn weggelaten }
  • 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 type TweeCijferDisplay.
public class KlokDisplay{ private TweeCijferDisplay uren; private TweeCijferDispaly minuten; //Constructors en methodes zijn weggelaten }
  • 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 klasse C2 geeft aan dat de klasse C1 objecten gebruikt van klasse C2.
  • Het klassendiagramma ziet er dan als volgt uit:
Een klassediagramma van de klasse KlokDisplay
Figuur - Een klassediagramma van de klasse KlokDisplay
  • 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 type TweeCijferDisplay nodig heeft: eentje voor de uren en eentje voor de minuten.
  • Het objectendiagramma ziet er als volgt uit:
Een objectendiagramma van KlokDisplay
Figuur - Een objectendiagramma van KlokDisplay
  • 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 type TweeCijferDisplay is, kan dit door de waarde van dat object te veranderen. Een mogelijke implementatie zou zijn:
/******************** * Stel de tijd van het display in op de ingevoerde uren en minuten */ public void setTijd(int uren, int minuten){ uren.waarde = uren; // dit zal een compileerfout geven: waarde is private gedeclareerd minuten.waarde = minuten; }
  • Dit zal echter niet lukken omdat de gegevensvariabele waarde in de klasse TweeCijferDisplay als private gedeclareerd is. En dit is juist de manier van hoe we het moeten doen. Voor het gemak kunnen we die gegevensvariable public declareren, maar dat is heel slechte praktijk. We moeten een extra methode in de klasse TweeCijferDisplay voorzien die ons mogelijk maakt om de waarde van de gegevensvariabele waarde te veranderen. Dit kan door een methode setWaarde(int waarde) aan te maken.

TweeCijferDisplay.java

public class TweeCijferDisplay{ private int maximum; private int waarde; public void setWaarde(int waarde){ this.waarde = waarde; } //andere Constructors en methodes zijn weggelaten }

KlokDisplay.java

public class KlokDisplay{ private TweeCijferDisplay uren; private TweeCijferDispaly minuten; public void setTijd(int uur, int minuut){ uren.setWaarde(uur); minuten.setWaarde(minuut); } //Constructors en methodes zijn weggelaten }

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 type TweeCijferDisplay te maken. We kunnen dat zelf doen, of we kunnen dit doen tijdens het maken van een object van KlokDisplay. 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 twee TweeCijferDisplay-objecten aangemaakt worden. De constructor van TweeCijferDisplay wordt aangeroepen wanneer de constructor in KlokDisplay wordt aangeroepen.

TweeCijferDisplay.java

public class TweeCijferDisplay{ private int maximum; private int waarde; public TweeCijferDisplay(int max){ this.maximum = max; } public void setWaarde(int waarde){ this.waarde = waarde; } //andere Constructors en methodes zijn weggelaten }

KlokDisplay.java

public class KlokDisplay{ private TweeCijferDisplay uren; private TweeCijferDispaly minuten; public KlokDisplay(){ uren = new TweeCijferDisplay(24); minuten = new TweeCijferDisplay(60); } public void setTijd(int uur, int minuut){ uren.setWaarde(uur); minuten.setWaarde(minuut); } //Constructors en methodes zijn weggelaten }

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 klasse Cursus. Een docent doceert een cursus is een associatie tussen Docent en Cursus.
TODO: figuur
Figuur - TODO: figuur
  • 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 interval m..n geeft aan dat tussen m en n 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 klasse Docent en 0..3 bij Cursus.
  • 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 bij Cursus plaatsen en 5...60 bij Student.

Student.java

public class Student{ private Cursus[] lijstCursussen; ... public void voegToeCursus(Cursus s){ ... } }

Cursus.java

public class Cursus{ private Student[] lijstStudenten; private Docent docent; public void voegToeStudent(Student s) ... } public void setDocent(Docent d){ this.docent = d; } }

Docent.java

public class Docent{ private Cursus[] lijstCursus; public void voegToeCursus(Cursus c) ... } }

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

public class Naam{ ... }

Student.java

public class Student{ private Naam naam; private Adres adres; ... }

Adres.java

public class Adres{ ... }
  • 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 een Integer klasse gewikkeld worden of een double in een Double of een char in een Character klasse.
  • Java biedt de volgende wikkelklasses aan: Boolean, Character, Double, Float, Byte, Short, Integer en Long.
  • Wikkelklasses die een numeriek type bevatten zijn vrij gelijkaardig. Elke klasse heeft een methode doubleValue(), floatValue(),intValue(), longValue(), shortValue() and byteValue(). 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) en new 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 en MIN_VALUE. MAX_VALUE is de maximale waarde die het corresponderende primitieve datatype kan bevatten. MIN_VALUE is de minimale waarde byte, short, int en long. Voor Float en Double is MIN_VALUE de minimale positieve float en double.

  • Elke numerieke wikkelklasse heeft de methodes doubleValue(), floatValue(), intValue(), longValue() and shortValue() om een waarde te retourneren van het type double, float, int, long of short.

  • Daarnaast bevat elke numerieke wikkelklasse een methode compareTo om getallen te vergelijking. Bijvoorbeeld:
Double d1 = new Double(12.4); Double d2 = new Double(12.3); d1.compareTo(d2); //retourneert 1

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
Integer intObject = 2; Integer[] intArray = {1, 2, 3}; System.out.println("" + intArray[0] + intArray[1] + intArray[2]);

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 en BigDecimal gebruiken in de java.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 klasse BigInteger 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.
String boodschap = new String("Welkom bij Java!"); char[] tekenArray= {'G','o','e','d','e','m','o','r','g','e','n'}; String boodschap = new String(tekenArray);
  • Java behandelt een string literal als een object van het type String:
String boodschap = "Welkom bij Java!"; // is equivalent met new String("Welkom bij Java!");

Vervangen en opsplitsen van Strings

  • Strings hebben methodes om stukken van strings te vervangen of om strings op te splitsen in delen.

results matching ""

    No results matching ""