OOP in Java: Classes, objecten, encapsulation, inheritance en abstractie
Leer objectgeoriënteerd programmeren in Java met praktische voorbeelden. Beheers classes, objecten, inheritance, encapsulation en abstracte classes met behulp van een restaurantsysteem voor het menu.
Java staat steevast in de top drie van populairste programmeertalen ter wereld. De adoptie in domeinen als enterprise-softwareontwikkeling, Android-apps en grootschalige webapplicaties is ongeëvenaard. Het sterke typesysteem, het uitgebreide ecosysteem en de “write once, run anywhere”-eigenschap maken Java bijzonder aantrekkelijk voor het bouwen van robuuste, schaalbare systemen. In dit artikel verkennen we hoe de objectgeoriënteerde programmeerfuncties van Java ontwikkelaars helpen deze mogelijkheden effectief te benutten, zodat ze onderhoudbare en schaalbare applicaties kunnen bouwen door middel van goede code-organisatie en hergebruik.
Een opmerking over het organiseren en uitvoeren van Java-code
Voordat we code gaan schrijven, doen we eerst wat setup.
Net als bij de syntax heeft Java strikte regels voor de organisatie van code.
Allereerst moet elke public class in een eigen bestand staan, exact genoemd naar de class maar met de extensie .java. Dus als ik een Laptop-class wil schrijven, moet de bestandsnaam Laptop.java—hoofdlettergevoelig. Je kunt niet-public classes in hetzelfde bestand hebben, maar het is beter ze te scheiden. We lopen wat voor de muziek uit—we hebben het over het organiseren van classes nog voordat we ze schrijven—maar het is verstandig om vooraf een globaal idee te hebben waar je dingen neerzet.
Alle Java-projecten moeten een Main.java-bestand hebben met de Main-class. Hier test je je classes door er objecten van aan te maken.
Om Java-code uit te voeren gebruiken weIntelliJ IDEA, een populaire Java-IDE. Na installatie van IntelliJ:
Maak een nieuw Java-project aan (File > New > Project)
Klik met de rechtermuisknop op de src-map om het bestand Main.java te maken en plak de volgende inhoud erin:
publicclassMain{publicstaticvoidmain(String[] args){// Create and tests objects here}}Mogelijk gemaakt door
Wanneer we het over classes hebben, schrijven we code in andere bestanden dan Main.java. Maar als we het hebben over het aanmaken en testen van objecten, schakelen we over naar Main.java.
Om het programma uit te voeren kun je op de groene play-knop naast de main-methode klikken:
De output verschijnt in het Run-venster onderaan.
Als je helemaal nieuw bent met Java, bekijk dan onzeIntroduction to Java-cursus, die de basis van Java-typen en control flow behandelt voordat je verdergaat.
Laten we er dan nu induiken.
Java-classes en objecten
Wat zijn classes nu precies?
Classes zijn programmeerconstructies in Java om concepten uit de echte wereld te representeren. Neem bijvoorbeeld deze MenuItem-class (maak in je IDE een bestand aan om deze class in te schrijven):
De class geeft ons een blauwdruk of sjabloon om verschillende menu-items in een restaurant te representeren. Door de twee attributen van de class, name, en price, te variëren, kunnen we talloze menu-objecten maken, zoals een burger of een salade.
Om een class in Java te maken, begin je met een regel die het toegangsniveau van de class beschrijft (private, public of protected) gevolgd door de classnaam. Direct na de accolades beschrijf je de attributen van je class.
Maar hoe maken we objecten die bij deze class horen? In Java kan dat met constructor-methoden:
publicclassMenuItem{publicString name;publicdouble price;// ConstructorpublicMenuItem(String name,double price){this.name = name;this.price = price;}}Mogelijk gemaakt door
Een constructor is een speciale methode die wordt aangeroepen wanneer we een nieuw object van een class maken. Hij initialiseert de attributen van het object met de waarden die we meegeven. In het bovenstaande voorbeeld neemt de constructor een name- en price-parameter en kent ze toe aan de velden van het object met het trefwoord ‘this’, dat verwijst naar een toekomstige objectinstantie.
De syntax voor de constructor wijkt af van andere methoden in een class omdat je geen returntype hoeft op te geven. Ook moet de constructor dezelfde naam hebben als de class en zou hij hetzelfde aantal attributen moeten initialiseren als je na de classdefinitie hebt gedeclareerd. Hierboven maakt de constructor twee attributen aan omdat we er twee na de classdefinitie hebben gedeclareerd: name en price.
Nadat je je class en de constructor hebt geschreven, kun je er instanties (objecten) van maken in je main-methode:
publicclassMain{publicstaticvoidmain(String[] args){// Create objects hereMenuItem burger =newMenuItem("Burger",3.5);MenuItem salad =newMenuItem("Salad",2.5);System.out.println(burger.name +", "+ burger.price);}}Mogelijk gemaakt door
Hierboven maken we twee MenuItem-objecten in de variabelen burger en salad. Zoals in Java vereist, moet je het type van de variabele declareren, in dit geval MenuItem. Vervolgens schrijf je het trefwoord new, gevolgd door het aanroepen van de constructormethode, om een instantie van onze class te maken.
Naast de constructor kun je gewone methoden maken die je class gedrag geven. Hieronder voegen we bijvoorbeeld een methode toe om de totaalprijs na belasting te berekenen:
publicclassMenuItem{publicString name;publicdouble price;// ConstructorpublicMenuItem(String name,double price){this.name = name;this.price = price;}// Method to calculate price after taxpublicdoublegetPriceAfterTax(){double taxRate =0.08;// 8% tax ratereturn price +(price * taxRate);}}Mogelijk gemaakt door
Nu kunnen we de totaalprijs inclusief belasting berekenen:
publicclassMain{publicstaticvoidmain(String[] args){MenuItem burger =newMenuItem("Burger",3.5);System.out.println("Price after tax: $"+ burger.getPriceAfterTax());}}Mogelijk gemaakt door
Het doel van classes is om een blauwdruk te bieden voor het maken van objecten. Die objecten worden vervolgens gebruikt door andere scripts of programma's. Onze MenuItem-objecten kunnen bijvoorbeeld worden gebruikt door een gebruikersinterface die hun naam, prijs en afbeelding op een scherm toont.
Daarom moeten we onze classes zo ontwerpen dat instanties alleen gebruikt kunnen worden zoals we bedoeld hebben. Op dit moment is onze MenuItem-class erg basic en foutgevoelig. Iemand zou objecten kunnen maken met belachelijke attributen, zoals een appeltaart met een negatieve prijs of een broodje van een miljoen dollar:
// Inside Main.javaMenuItem applePie =newMenuItem("Apple Pie",-5.99);// Negative price!MenuItem sandwich =newMenuItem("Sandwich",1000000);// Unreasonably expensiveSystem.out.println("Apple pie price: $"+ applePie.price);System.out.println("Sandwich price: $"+ sandwich.price);Mogelijk gemaakt door
Dus: het eerste wat je na het schrijven van een class doet, is de attributen beschermen door te beperken hoe ze worden aangemaakt en benaderd. Om te beginnen willen we alleen positieve waarden toestaan voor price en een maximum instellen om te voorkomen dat per ongeluk belachelijk dure items worden getoond.
In Java kunnen we dit doen met setter-methoden:
publicclassMenuItem{privateString name;privatedouble price;privatestaticfinaldoubleMAX_PRICE=100.0;publicMenuItem(String name,double price){this.name = name;setPrice(price);}publicvoidsetPrice(double price){if(price <0){thrownewIllegalArgumentException("Price cannot be negative");}if(price >MAX_PRICE){thrownewIllegalArgumentException("Price cannot exceed $"+MAX_PRICE);}this.price = price;}}Mogelijk gemaakt door
Laten we bekijken wat er nieuw is in het bovenstaande codeblok:
1. We hebben de attributen private gemaakt door het trefwoord private toe te voegen. Dit betekent dat ze alleen binnen de MenuItem-class toegankelijk zijn. Encapsulation begint met deze cruciale stap.
2. We hebben een nieuwe constante toegevoegd, MAX_PRICE, die:
private is (alleen binnen de class toegankelijk)
static is (gedeeld door alle instanties)
final is (kan na initialisatie niet worden gewijzigd)
is ingesteld op $100,0 als redelijk maximum
3. We hebben een setPrice()-methode toegevoegd die:
Een price-parameter aanneemt
Valideert dat de prijs niet negatief is
Valideert dat de prijs MAX_PRICE niet overschrijdt
IllegalArgumentException gooit met beschrijvende boodschappen als de validatie faalt
De prijs alleen instelt als alle validaties slagen
4. We hebben de constructor aangepast om setPrice() te gebruiken in plaats van de prijs direct toe te wijzen. Zo vindt prijsvalidatie plaats tijdens het aanmaken van het object.
We hebben zojuist een van de kernpijlers van goed objectgeoriënteerd ontwerp geïmplementeerd — encapsulation. Dit paradigma dwingt gegevensafscherming en gecontroleerde toegang tot objectattributen af, zodat interne implementatiedetails beschermd zijn tegen externe inmenging en alleen via goed gedefinieerde interfaces kunnen worden aangepast.
Laten we dit punt kracht bijzetten door encapsulation toe te passen op het attribuut name. Stel dat we een koffiezaak hebben die alleen lattes, cappuccino's, espresso's, americanos en mocha's serveert.
Onze menunamen mogen dan alleen uit deze lijst komen. Zo kun je dit afdwingen in code:
// Rest of the class here...privatestaticfinalString[]VALID_NAMES={"latte","cappuccino","espresso","americano","mocha"};privateString name;publicvoidsetName(String name){String lowercaseName = name.toLowerCase();for(String validName :VALID_NAMES){if(validName.equals(lowercaseName)){this.name = name;return;}}thrownewIllegalArgumentException("Invalid drink name. Must be one of: "+String.join(", ",VALID_NAMES));}Mogelijk gemaakt door
De bovenstaande code implementeert naamvalidatie voor menu-items in een koffiezaak. Even in stukjes:
1. Eerst wordt een private static final-array VALID_NAMES gedefinieerd met de enige toegestane dranknamen: latte, cappuccino, espresso, americano en mocha. Deze array is:
private: alleen binnen de class toegankelijk
static: gedeeld door alle instanties
final: na initialisatie niet te wijzigen
2. Er wordt een private String-veld name gedeclareerd om de dranknaam op te slaan
3. De methode setName() implementeert de validatielogica:
Neemt een String-parameter name aan
Zet die om naar lowercase om de vergelijking case-insensitief te maken
Loopt door de array VALID_NAMES
Stelt de naam in en returnt als er een match is
Gooi een IllegalArgumentException met een beschrijvende boodschap en alle geldige opties als er geen match is
Hier is de volledige class tot nu toe:
publicclassMenuItem{privateString name;privatedouble price;privatestaticfinalString[]VALID_NAMES={"latte","cappuccino","espresso","americano","mocha"};privatestaticfinaldoubleMAX_PRICE=100.0;publicMenuItem(String name,double price){setName(name);setPrice(price);}publicvoidsetName(String name){String lowercaseName = name.toLowerCase();for(String validName :VALID_NAMES){if(validName.equals(lowercaseName)){this.name = name;return;}}thrownewIllegalArgumentException("Invalid drink name. Must be one of: "+String.join(", ",VALID_NAMES));}publicvoidsetPrice(double price){if(price <0){thrownewIllegalArgumentException("Price cannot be negative");}if(price >MAX_PRICE){thrownewIllegalArgumentException("Price cannot exceed $"+MAX_PRICE);}this.price = price;}}Mogelijk gemaakt door
Nadat we hebben beschermd hoe attributen worden aangemaakt, willen we ook beschermen hoe ze worden benaderd. Dat doen we met getter-methoden:
publicclassMenuItem{// Rest of the code here...publicStringgetName(){return name;}publicdoublegetPrice(){return price;}}Mogelijk gemaakt door
Getter-methoden bieden gecontroleerde toegang tot private attributen van een class. Ze lossen het probleem van directe attribuuttoegang op, wat kan leiden tot ongewenste wijzigingen en het doorbreken van encapsulation.
Zonder getters zouden we bijvoorbeeld attributen direct kunnen benaderen:
MenuItem item =newMenuItem("Latte",4.99);String name = item.name;// Direct access to attribute
item.name ="INVALID";// Can modify directly, bypassing validationMogelijk gemaakt door
Met getters dwingen we correcte toegang af:
MenuItem item =newMenuItem("Latte",4.99);String name = item.getName();// Controlled access through getter// item.name = "INVALID"; // Not allowed - must use setName() which validatesMogelijk gemaakt door
Deze vorm van encapsulation:
Beschermt de gegevensintegriteit door ongeldige wijzigingen te voorkomen
Stelt ons in staat de interne implementatie te wijzigen zonder code te breken die de class gebruikt
Biedt één toegangspunt waar indien nodig extra logica kan worden toegevoegd
Maakt de code onderhoudbaarder en minder foutgevoelig
Inheritance
Onze class begint er goed uit te zien, maar er zijn best wat problemen. Voor een groot restaurant met veel soorten gerechten en dranken is de class bijvoorbeeld niet flexibel genoeg.
Als we verschillende soorten etenswaren willen toevoegen, lopen we tegen diverse uitdagingen aan. Sommige gerechten kunnen als afhaal bereid worden, andere moeten direct genuttigd worden. Menu-items kunnen verschillende prijzen en kortingen hebben. Gerechten hebben mogelijk temperatuurtracking of speciale opslag nodig. Dranken kunnen warm of koud zijn met aanpasbare ingrediënten. Items kunnen allergeneninformatie en portieopties nodig hebben. Het huidige systeem kan niet met al deze variërende eisen omgaan.
Inheritance biedt een elegante oplossing voor al deze problemen. We kunnen gespecialiseerde versies van menu-items maken door een basis-MenuItem-class met gemeenschappelijke attributen te definiëren en vervolgens child classes te maken die deze basis overnemen en unieke eigenschappen toevoegen.
Zo zouden we een Drink-class voor dranken kunnen hebben met temperatuuropties, een Food-class voor items die direct geconsumeerd moeten worden, en een Dessert-class voor items met speciale opslagbehoeften — allemaal met overname van de kernfunctionaliteit van menu-items.
Classes uitbreiden
Laten we die ideeën implementeren, te beginnen met Drink:
publicclassDrinkextendsMenuItem{privateboolean isCold;publicDrink(String name,double price,boolean isCold){this.name = name;this.price = price;this.isCold = isCold;}publicbooleangetIsCold(){return isCold;}publicvoidsetIsCold(boolean isCold){this.isCold = isCold;}}Mogelijk gemaakt door
Om een child class te definiëren die erft van een parent, gebruiken we het trefwoord extends na de naam van de child-class, gevolgd door de parent-class. Na de classdefinitie definiëren we eventuele nieuwe attributen van deze child en implementeren we de constructor.
Let echter op dat we de initialisatie van name en price samen met isCold moeten herhalen. Dat is niet ideaal, want de parent-class kan honderden attributen hebben. Bovendien zal de bovenstaande code een fout geven bij compilatie omdat dit niet de juiste manier is om attributen van de parent-class te initialiseren. De juiste manier is met het trefwoord super:
publicclassDrinkextendsMenuItem{privateboolean isCold;publicDrink(String name,double price,boolean isCold){super(name, price);this.isCold = isCold;}publicbooleangetIsCold(){return isCold;}publicvoidsetIsCold(boolean isCold){this.isCold = isCold;}}Mogelijk gemaakt door
Het trefwoord super wordt gebruikt om de constructor van de parent-class aan te roepen. In dit geval roept super(name, price) de constructor van MenuItem aan om die attributen te initialiseren, waardoor we duplicatie vermijden. We hoeven alleen het nieuwe attribuut isCold te initialiseren dat specifiek is voor de Drink-class.
Het trefwoord is erg flexibel omdat je het overal in de child-class kunt gebruiken om naar de parent-class te verwijzen. Om bijvoorbeeld een parent-methode aan te roepen gebruik je super.methodName(), terwijl super.attributeName voor attributen is.
Method overriding
Stel dat we een nieuwe methode aan onze classes willen toevoegen om de totaalprijs na belasting te berekenen. Omdat verschillende menu-items verschillende btw-tarieven kunnen hebben (bijvoorbeeld bereide maaltijden vs. verpakte dranken), kunnen we method overriding gebruiken om specifieke belastingberekeningen in elke child-class te implementeren, terwijl we in de parent-class een gemeenschappelijke methodenaam behouden.
Zo ziet dat eruit:
publicclassMenuItem{// Rest of the MenuItem classpublicdoublecalculateTotalPrice(){// Default tax rate of 10%return price *1.10;}}publicclassFoodextendsMenuItem{privateboolean isVegetarian;publicFood(String name,double price,boolean isVegetarian){super(name, price);this.isVegetarian = isVegetarian;}@OverridepublicdoublecalculateTotalPrice(){// Food has 15% taxreturnsuper.getPrice()*1.15;}}publicclassDrinkextendsMenuItem{privateboolean isCold;publicDrink(String name,double price,boolean isCold){super(name, price);this.isCold = isCold;}@OverridepublicdoublecalculateTotalPrice(){// Drinks have 8% taxreturnsuper.getPrice()*1.08;}}Mogelijk gemaakt door
In dit voorbeeld maakt method overriding het mogelijk dat elke subclass zijn eigen implementatie van calculateTotalPrice() biedt:
De basisclass MenuItem definieert een standaardbelasting van 10%.
Wanneer de classes Food en DrinkMenuItem uitbreiden, overschrijven ze deze methode om hun eigen tarieven te implementeren:
Food-items hebben een hoger tarief van 15%
Dranken hebben een lager tarief van 8%
De annotatie @Override wordt gebruikt om expliciet aan te geven dat deze methoden de parent-methode overschrijven. Dit helpt fouten te vangen als de methodsignature niet overeenkomt met die van de parent-class.
Elke subclass kan nog steeds de prijs van de parent-class benaderen via super.getPrice(), wat laat zien hoe overschreven methoden functionaliteit van de parent-class kunnen gebruiken en tegelijk hun eigen gedrag toevoegen.
Kortom, method overriding is een wezenlijk onderdeel van inheritance waarmee subclasses hun eigen implementatie kunnen geven van methoden die in de parent-class zijn gedefinieerd, zodat ze specifieker gedrag kunnen bieden met behoud van dezelfde methodsignature.
Abstracte classes
Onze MenuItem-classhiërarchie werkt, maar er is een probleem: zou iemand een kale MenuItem-instantie mogen maken? In ons restaurant is elk menu-item immers óf Food óf Drink—een "generiek menu-item" bestaat niet.
We kunnen dit voorkomen door MenuItem abstract te maken. Een abstracte class biedt alleen een basisblauwdruk—hij kan alleen als parent-class voor inheritance dienen en niet direct worden geïnstantieerd.
Om MenuItem abstract te maken, voegen we het trefwoord abstract toe na de access modifier:
publicabstractclassMenuItem{privateString name;privatedouble price;publicMenuItem(String name,double price){setName(name);setPrice(price);}// Existing getters/setters remain the same// Make this method abstract - every subclass MUST implement itpublicabstractdoublecalculateTotalPrice();}Mogelijk gemaakt door
Abstracte classes kunnen ook abstracte methoden hebben, zoals calculateTotalPrice() hierboven. Deze abstracte methoden fungeren als contracten die subclasses dwingen hun eigen implementatie te leveren. Met andere woorden: elke abstracte methode in een abstracte class moet door child classes worden geïmplementeerd.
Laten we Food en Drink herschrijven met deze wijzigingen in gedachten:
Met deze menu-implementatie hebben we gezien hoe abstractie en inheritance samenwerken om flexibele, onderhoudbare code te creëren die zich gemakkelijk kan aanpassen aan verschillende zakelijke vereisten.
Conclusie
Vandaag hebben we een glimp opgevangen van wat Java kan als objectgeoriënteerde programmeertaal. We hebben de basis van classes, objecten en enkele belangrijke pijlers van OOP behandeld: encapsulation, inheritance en abstractie aan de hand van een restaurantsysteem voor het menu.
Om dit systeem productierijp te maken, moet je nog veel meer leren, zoals interfaces (onderdeel van abstractie), polymorfisme en OOP-ontwerpprincipes. Wil je meer over deze concepten leren, bekijk dan onzeIntroduction to OOP in Java-cursus.
Wat zijn de vereisten om deze Java OOP-tutorial te volgen?⌄
Je moet basiskennis hebben van Java-programmeren, waaronder datatypen, variabelen, control flow (if/else, lussen) en de basis-syntax. Onze Introduction to Java-cursus behandelt deze fundamenten als je een opfrisser nodig hebt.
Waarom een restaurantsysteem voor het menu gebruiken om OOP-concepten uit te leggen?⌄
Een restaurantsysteem voor het menu is een intuïtief voorbeeld dat de toepassingen van OOP-principes in de echte wereld laat zien. Het illustreert op natuurlijke wijze inheritance (verschillende soorten menu-items), encapsulation (het beschermen van prijs- en naamwaarden) en interfaces (loyaliteitsprogramma's, temperatuurregeling). De meeste mensen begrijpen hoe restaurants werken, waardoor de concepten makkelijker te begrijpen zijn.
Wat is het verschil tussen abstracte classes en interfaces in Java?⌄
Een class kan slechts één abstracte class extenden maar meerdere interfaces implementeren.
Abstracte classes kunnen velden en constructors hebben; interfaces niet.
Abstracte classes kunnen zowel abstracte als concrete methoden hebben; interfaces hadden traditioneel alleen abstracte methoden (vóór Java 8).
Abstracte classes bieden een basisimplementatie, terwijl interfaces een contract definiëren.
Ik ben een contentmaker op het gebied van data science met meer dan 2 jaar ervaring en een van de grootste achterbannen op Medium. Ik schrijf graag uitgebreide artikelen over AI en ML met een vleugje sarcastische stijl, want je moet toch íets doen om ze wat minder saai te maken. Ik heb meer dan 130 artikelen en een DataCamp-cursus gemaakt, en er is nog een nieuwe in de maak. Mijn content is door meer dan 5 miljoen paar ogen gezien, van wie 20k volgers werden op zowel Medium als LinkedIn.
Java staat steevast in de top drie van populairste programmeertalen ter wereld. De adoptie in domeinen als enterprise-softwareontwikkeling, Android-apps en grootschalige webapplicaties is ongeëvenaard. Het sterke typesysteem, het uitgebreide ecosysteem en de “write once, run anywhere”-eigenschap maken Java bijzonder aantrekkelijk voor het bouwen van robuuste, schaalbare systemen. In dit artikel verkennen we hoe de objectgeoriënteerde programmeerfuncties van Java ontwikkelaars helpen deze mogelijkheden effectief te benutten, zodat ze onderhoudbare en schaalbare applicaties kunnen bouwen door middel van goede code-organisatie en hergebruik.
Een opmerking over het organiseren en uitvoeren van Java-code
Voordat we code gaan schrijven, doen we eerst wat setup.
Net als bij de syntax heeft Java strikte regels voor de organisatie van code.
Allereerst moet elke public class in een eigen bestand staan, exact genoemd naar de class maar met de extensie
.java. Dus als ik een Laptop-class wil schrijven, moet de bestandsnaamLaptop.java—hoofdlettergevoelig. Je kunt niet-public classes in hetzelfde bestand hebben, maar het is beter ze te scheiden. We lopen wat voor de muziek uit—we hebben het over het organiseren van classes nog voordat we ze schrijven—maar het is verstandig om vooraf een globaal idee te hebben waar je dingen neerzet.Alle Java-projecten moeten een
Main.java-bestand hebben met de Main-class. Hier test je je classes door er objecten van aan te maken.Om Java-code uit te voeren gebruiken we IntelliJ IDEA, een populaire Java-IDE. Na installatie van IntelliJ:
Main.javate maken en plak de volgende inhoud erin:Wanneer we het over classes hebben, schrijven we code in andere bestanden dan
Main.java. Maar als we het hebben over het aanmaken en testen van objecten, schakelen we over naarMain.java.Om het programma uit te voeren kun je op de groene play-knop naast de main-methode klikken:
De output verschijnt in het Run-venster onderaan.
Als je helemaal nieuw bent met Java, bekijk dan onze Introduction to Java-cursus, die de basis van Java-typen en control flow behandelt voordat je verdergaat.
Laten we er dan nu induiken.
Java-classes en objecten
Wat zijn classes nu precies?
Classes zijn programmeerconstructies in Java om concepten uit de echte wereld te representeren. Neem bijvoorbeeld deze MenuItem-class (maak in je IDE een bestand aan om deze class in te schrijven):
De class geeft ons een blauwdruk of sjabloon om verschillende menu-items in een restaurant te representeren. Door de twee attributen van de class,
name, enprice, te variëren, kunnen we talloze menu-objecten maken, zoals een burger of een salade.Om een class in Java te maken, begin je met een regel die het toegangsniveau van de class beschrijft (
private,publicofprotected) gevolgd door de classnaam. Direct na de accolades beschrijf je de attributen van je class.Maar hoe maken we objecten die bij deze class horen? In Java kan dat met constructor-methoden:
Een constructor is een speciale methode die wordt aangeroepen wanneer we een nieuw object van een class maken. Hij initialiseert de attributen van het object met de waarden die we meegeven. In het bovenstaande voorbeeld neemt de constructor een name- en price-parameter en kent ze toe aan de velden van het object met het trefwoord ‘this’, dat verwijst naar een toekomstige objectinstantie.
De syntax voor de constructor wijkt af van andere methoden in een class omdat je geen returntype hoeft op te geven. Ook moet de constructor dezelfde naam hebben als de class en zou hij hetzelfde aantal attributen moeten initialiseren als je na de classdefinitie hebt gedeclareerd. Hierboven maakt de constructor twee attributen aan omdat we er twee na de classdefinitie hebben gedeclareerd:
nameenprice.Nadat je je class en de constructor hebt geschreven, kun je er instanties (objecten) van maken in je main-methode:
Output:
Hierboven maken we twee
MenuItem-objecten in de variabelenburgerensalad. Zoals in Java vereist, moet je het type van de variabele declareren, in dit gevalMenuItem. Vervolgens schrijf je het trefwoord new, gevolgd door het aanroepen van de constructormethode, om een instantie van onze class te maken.Naast de constructor kun je gewone methoden maken die je class gedrag geven. Hieronder voegen we bijvoorbeeld een methode toe om de totaalprijs na belasting te berekenen:
Nu kunnen we de totaalprijs inclusief belasting berekenen:
Output:
Encapsulation
Het doel van classes is om een blauwdruk te bieden voor het maken van objecten. Die objecten worden vervolgens gebruikt door andere scripts of programma's. Onze
MenuItem-objecten kunnen bijvoorbeeld worden gebruikt door een gebruikersinterface die hun naam, prijs en afbeelding op een scherm toont.Daarom moeten we onze classes zo ontwerpen dat instanties alleen gebruikt kunnen worden zoals we bedoeld hebben. Op dit moment is onze
MenuItem-class erg basic en foutgevoelig. Iemand zou objecten kunnen maken met belachelijke attributen, zoals een appeltaart met een negatieve prijs of een broodje van een miljoen dollar:Dus: het eerste wat je na het schrijven van een class doet, is de attributen beschermen door te beperken hoe ze worden aangemaakt en benaderd. Om te beginnen willen we alleen positieve waarden toestaan voor price en een maximum instellen om te voorkomen dat per ongeluk belachelijk dure items worden getoond.
In Java kunnen we dit doen met setter-methoden:
Laten we bekijken wat er nieuw is in het bovenstaande codeblok:
1. We hebben de attributen private gemaakt door het trefwoord
privatetoe te voegen. Dit betekent dat ze alleen binnen de MenuItem-class toegankelijk zijn. Encapsulation begint met deze cruciale stap.2. We hebben een nieuwe constante toegevoegd,
MAX_PRICE, die:3. We hebben een
setPrice()-methode toegevoegd die:4. We hebben de constructor aangepast om
setPrice()te gebruiken in plaats van de prijs direct toe te wijzen. Zo vindt prijsvalidatie plaats tijdens het aanmaken van het object.We hebben zojuist een van de kernpijlers van goed objectgeoriënteerd ontwerp geïmplementeerd — encapsulation. Dit paradigma dwingt gegevensafscherming en gecontroleerde toegang tot objectattributen af, zodat interne implementatiedetails beschermd zijn tegen externe inmenging en alleen via goed gedefinieerde interfaces kunnen worden aangepast.
Laten we dit punt kracht bijzetten door encapsulation toe te passen op het attribuut name. Stel dat we een koffiezaak hebben die alleen lattes, cappuccino's, espresso's, americanos en mocha's serveert.
Onze menunamen mogen dan alleen uit deze lijst komen. Zo kun je dit afdwingen in code:
De bovenstaande code implementeert naamvalidatie voor menu-items in een koffiezaak. Even in stukjes:
1. Eerst wordt een private static final-array
VALID_NAMESgedefinieerd met de enige toegestane dranknamen: latte, cappuccino, espresso, americano en mocha. Deze array is:2. Er wordt een private String-veld name gedeclareerd om de dranknaam op te slaan
3. De methode
setName()implementeert de validatielogica:VALID_NAMESHier is de volledige class tot nu toe:
Nadat we hebben beschermd hoe attributen worden aangemaakt, willen we ook beschermen hoe ze worden benaderd. Dat doen we met getter-methoden:
Getter-methoden bieden gecontroleerde toegang tot private attributen van een class. Ze lossen het probleem van directe attribuuttoegang op, wat kan leiden tot ongewenste wijzigingen en het doorbreken van encapsulation.
Zonder getters zouden we bijvoorbeeld attributen direct kunnen benaderen:
Met getters dwingen we correcte toegang af:
Deze vorm van encapsulation:
Inheritance
Onze class begint er goed uit te zien, maar er zijn best wat problemen. Voor een groot restaurant met veel soorten gerechten en dranken is de class bijvoorbeeld niet flexibel genoeg.
Als we verschillende soorten etenswaren willen toevoegen, lopen we tegen diverse uitdagingen aan. Sommige gerechten kunnen als afhaal bereid worden, andere moeten direct genuttigd worden. Menu-items kunnen verschillende prijzen en kortingen hebben. Gerechten hebben mogelijk temperatuurtracking of speciale opslag nodig. Dranken kunnen warm of koud zijn met aanpasbare ingrediënten. Items kunnen allergeneninformatie en portieopties nodig hebben. Het huidige systeem kan niet met al deze variërende eisen omgaan.
Inheritance biedt een elegante oplossing voor al deze problemen. We kunnen gespecialiseerde versies van menu-items maken door een basis-
MenuItem-class met gemeenschappelijke attributen te definiëren en vervolgens child classes te maken die deze basis overnemen en unieke eigenschappen toevoegen.Zo zouden we een
Drink-class voor dranken kunnen hebben met temperatuuropties, eenFood-class voor items die direct geconsumeerd moeten worden, en eenDessert-class voor items met speciale opslagbehoeften — allemaal met overname van de kernfunctionaliteit van menu-items.Classes uitbreiden
Laten we die ideeën implementeren, te beginnen met
Drink:Om een child class te definiëren die erft van een parent, gebruiken we het trefwoord
extendsna de naam van de child-class, gevolgd door de parent-class. Na de classdefinitie definiëren we eventuele nieuwe attributen van deze child en implementeren we de constructor.Let echter op dat we de initialisatie van
nameenpricesamen metisColdmoeten herhalen. Dat is niet ideaal, want de parent-class kan honderden attributen hebben. Bovendien zal de bovenstaande code een fout geven bij compilatie omdat dit niet de juiste manier is om attributen van de parent-class te initialiseren. De juiste manier is met het trefwoordsuper:Het trefwoord
superwordt gebruikt om de constructor van de parent-class aan te roepen. In dit geval roeptsuper(name, price)de constructor vanMenuItemaan om die attributen te initialiseren, waardoor we duplicatie vermijden. We hoeven alleen het nieuwe attribuutisColdte initialiseren dat specifiek is voor deDrink-class.Het trefwoord is erg flexibel omdat je het overal in de child-class kunt gebruiken om naar de parent-class te verwijzen. Om bijvoorbeeld een parent-methode aan te roepen gebruik je
super.methodName(), terwijlsuper.attributeNamevoor attributen is.Method overriding
Stel dat we een nieuwe methode aan onze classes willen toevoegen om de totaalprijs na belasting te berekenen. Omdat verschillende menu-items verschillende btw-tarieven kunnen hebben (bijvoorbeeld bereide maaltijden vs. verpakte dranken), kunnen we method overriding gebruiken om specifieke belastingberekeningen in elke child-class te implementeren, terwijl we in de parent-class een gemeenschappelijke methodenaam behouden.
Zo ziet dat eruit:
In dit voorbeeld maakt method overriding het mogelijk dat elke subclass zijn eigen implementatie van
calculateTotalPrice()biedt:De basisclass
MenuItemdefinieert een standaardbelasting van 10%.Wanneer de classes
FoodenDrinkMenuItemuitbreiden, overschrijven ze deze methode om hun eigen tarieven te implementeren:De annotatie
@Overridewordt gebruikt om expliciet aan te geven dat deze methoden de parent-methode overschrijven. Dit helpt fouten te vangen als de methodsignature niet overeenkomt met die van de parent-class.Elke subclass kan nog steeds de prijs van de parent-class benaderen via
super.getPrice(), wat laat zien hoe overschreven methoden functionaliteit van de parent-class kunnen gebruiken en tegelijk hun eigen gedrag toevoegen.Kortom, method overriding is een wezenlijk onderdeel van inheritance waarmee subclasses hun eigen implementatie kunnen geven van methoden die in de parent-class zijn gedefinieerd, zodat ze specifieker gedrag kunnen bieden met behoud van dezelfde methodsignature.
Abstracte classes
Onze
MenuItem-classhiërarchie werkt, maar er is een probleem: zou iemand een kaleMenuItem-instantie mogen maken? In ons restaurant is elk menu-item immers óf Food óf Drink—een "generiek menu-item" bestaat niet.We kunnen dit voorkomen door
MenuItemabstract te maken. Een abstracte class biedt alleen een basisblauwdruk—hij kan alleen als parent-class voor inheritance dienen en niet direct worden geïnstantieerd.Om
MenuItemabstract te maken, voegen we het trefwoordabstracttoe na de access modifier:Abstracte classes kunnen ook abstracte methoden hebben, zoals
calculateTotalPrice()hierboven. Deze abstracte methoden fungeren als contracten die subclasses dwingen hun eigen implementatie te leveren. Met andere woorden: elke abstracte methode in een abstracte class moet door child classes worden geïmplementeerd.Laten we
FoodenDrinkherschrijven met deze wijzigingen in gedachten:Met deze menu-implementatie hebben we gezien hoe abstractie en inheritance samenwerken om flexibele, onderhoudbare code te creëren die zich gemakkelijk kan aanpassen aan verschillende zakelijke vereisten.
Conclusie
Vandaag hebben we een glimp opgevangen van wat Java kan als objectgeoriënteerde programmeertaal. We hebben de basis van classes, objecten en enkele belangrijke pijlers van OOP behandeld: encapsulation, inheritance en abstractie aan de hand van een restaurantsysteem voor het menu.
Om dit systeem productierijp te maken, moet je nog veel meer leren, zoals interfaces (onderdeel van abstractie), polymorfisme en OOP-ontwerpprincipes. Wil je meer over deze concepten leren, bekijk dan onze Introduction to OOP in Java-cursus.
Als je je Java-kennis wilt testen, probeer dan enkele vragen te beantwoorden in ons artikel met Java-sollicitatievragen.