Hoofdstuk 4: Overerving en Polymorfisme

Inleiding

Objectgeorienteerd programmeren laat je toe om nieuwe klassen af te leiden van bestaande klassen. Dit wordt overerving genoemd

  • Het proceduraal denken focust je op het ontwerpen van methode en het object-geörienteerd denken verbindt gegevens en methodes samen in objecten. Software ontwerp maakt gebruik van object-geörienteerd denken laat toe om objecten zelf te definiëren via klassen.
  • Overerving is een belangrijk en krachtig tool in het hergebruik van software. Veronderstel dat je voor een CAD programma een aantal klassen moet definiëren die cirkels, rechthoeken en driehoeken modelleren. Deze klassen hebben veel gemeen.

Superklassen en Subklassen

Overerving laat toe om je een algemene klasse de definiëren, de zogenaamde superklasse en later kan je die klasse uitbreiden naar meer gespecializeerdere klassen, de zogenaamde subklassen.

  • Een klasse gebruik je om objecten te modelleren van eenzelfde type. Verschillende klassen hebben misschien enkele eigenschappen of methodes gemeenschappelijk, die kunnen dan opgenomen worden in een algemene klasse en die kunnen dan gedeeld worden door andere klassen. Je kan een gespecializeerde klasse definiëren als uitbreiding op een algemene klasse.
  • Beschouw een verzameling van klassen die geometrische objecten wil voorstellen. Deze klassen worden gebruikt om cirkels en rechthoeken te definiëren. Geometrische objecten hebben veel eigenschappen (attributen) en methodes gemeenschappelijk. Elke geometrisch object kan getekend worden in een bepaald kleur of kan opgevuld zijn of niet. De klasse kan dan de gegevensvelden kleur en opgevuld bevatten, met elk een setter en getter methode. Deze kunnen in een algemene klasse GeometrischObject vervat zitten.
  • Veronderstel dat deze klasse ook een eigenschap datumCreatie heeft en de methodes getDatumCreatie() en toString(). De toString() methode retourneert een string voorstelling van het object. Omdat cirkel een speciaal soort geometrisch object is, heeft het ook eigenschappen en methodes gemeen met andere objecten. Het is dus logisch om een klasse Cirkel te definiëren dat een uitbreiding is op de klasse GeometrischObject.

  • In een UML diagram wordt overerving aangeduid door een volle puil. Deze pijl vertrekt van de subklasse en wijst naar de superklasse.

Een voorbeeld van een UML diagram voor de klasse `GeometrischObject` en de subklassen `Cirkel` en `Rechthoek`
Figuur - Een voorbeeld van een UML diagram voor de klasse `GeometrischObject` en de subklassen `Cirkel` en `Rechthoek`
  • In de terminologie van Java: een klasse C1 dat een uitbreiding is van een andere klasse C2, wordt ook een subklasse genoemd. en de klasse C2 wordt dan de superklasse genoemd. Een superklasse wordt ook soms wel eens de ouderklasse of basisklasse genoemd. Een subklasse wordt ook wel de kindklasse, uitgebreide klasse of afgeleide klasse genoemd.
  • Een klasse Cirkel erft dus alle toegankelijke gegevensvariabelen over van de superklasse GeometrischObject. In die zin is de klasse Cirkel een uitbreiding van de klasse GeometrischObject. We duiden dit aan in de declaratie van de klasse door het sleutelwoord extends. Een klasse beschrijving van een subklasse begint dan als volgt
public class Cirkel extends GeometrischObject
  • Via het sleutelwoord extends weet Java dat de klasse Cirkel alle methodes overerft van de klasse GeometrischObject.
  • Je kan overerving zien als een 'is-een'-relatie. Een cirkel en een rechthoek is een geometrisch object. Een subklasse is niet een deelverzameling van een superklasse. Integendeel, een subklasse bevat meer eigenschappen en methodes dan zijn superklasse. Een subklasse is specifieker dan een superklasse.
  • Gegevensvariabelen die private gedeclareerd zijn, zijn niet toegankelijk van buiten de klasse.
  • Niet alle 'is-een'-relatie moet via overerving gaan. Een vierkant is een rechthoek, maar het is niet helemaal juist om vierkant als uitbreiding te beschouwen van rechthoek. Dit omdat de gegevensvariabelen breedte en hoogte niet gepast zijn voor een vierkant. Het zou beter zijn om een klasse Vierkant te laten overerven van de klasse GeometrischObject.
  • Sommige programmeertalen maken het de programmeur mogelijk om een subklassen te maken die afgeleid zijn van meerdere superklassen. Dit is gekend als meervoudige overerving. Java dit niet toe: een subklasse kan enkel maar overerven van één superklasse. Dit is enkelvoudige overerving.

Het sleutelwoord super

Het sleutelwoord super verwijst naar de superklasse en kan gebruikt worden om methodes en constructoren van de superklasse aan te roepen.

  • Het sleutelwoord this werd gebruik als verwijzing naar het oproepende object. Het sleutelwoord super verwijst naar de superklasse van de klasse waar super voorkomt.
  • Het kan gebruikt worden om:
    • een constructor van de superklasse aan te roepen
    • een methode van de superklasse aan te roepen

Aanroepen van constructoren van superklassen

  • De syntax om een constructor van een superklasse aan te roepen is:
super(), of super(parameters);
  • De uitdrukking super() roept de no-argument constructor aan en super(parameters) roept de constructor aan dat past met de argumenten.
  • Een voorbeeld:

GeometrischObject.java

public GeometrischObject{ private String kleur; private boolean opgevuld; public GeometrischObject( String kleur, boolean opgevuld){ this.kleur = kleur; this.opgevuld = opgevuld; } }

Cirkel.java

public Cirkel extends GeometrischObject{ private double straal; public Cirkel(double straal, String kleur, boolean opgevuld){ super(kleur, opgevuld); this.straal = straal; } }
  • Een constructor van een afgeleide klasse kan een overladen constructor of een constructor van zijn superklasse aanroepen. Als geen van beide expliciet worden aangeroepen, dan zal de compiler automatisch een super() aanroep doen als eerste uitdrukking in de constructor.
  • In andere woorden, wanneer een object van een subklasse wordt aangemaakt, zal de constructor van de subklasse eerst de constructor van de superklasse aanroepen, vóór de uitdrukkingen die in de constructor van de subklasse staan.
  • Op deze manier zal een afgeleide klasse altijd eerst de constructor van de superklasse aanroepen (ookal wordt hij niet expliciet aangeroepen in de subklasse). Dit noemt men ook wel het aaneenschakelen van constructoren.

Aanroepen van methodes van constructoren

  • Het sleutelwoord super kan ook gebruikt worden om een methode van de superklasse aan te roepen:
super.methode(parameters);
  • Zo kan je methodes aanroepen die enkel in de superklasse zijn gedeclareerd en geïmplementeerd.

GeometrischObject.java

public GeometrischObject{ private String kleur; private boolean opgevuld; public GeometrischObject( String kleur, boolean opgevuld){ this.kleur = kleur; this.opgevuld = opgevuld; } public String getKleur(){ return kleur; } }

Cirkel.java

public Cirkel{ private double straal; public Cirkel(double straal, String kleur, boolean opgevuld){ super(kleur, opgevuld); this.straal = straal; } public void printCirkel(){ System.out.println("De cirkel heeft een straal " + straal + " en heeft de kleur " + super.getKleur()); } }
  • In het voorbeeld heeft de klasse Cirkel geen methode getKleur, die moet aangeroepen worden via de superklasse GeometrischObject.

Overschrijven van methodes

Om een methode te overschrijven, moet de methode gedefinieerd zijn inde subklasse met dezelfde signatuur en hetzelfde retourtype als de definitie in de superklasse

  • Een subklasse erft alle methodes van de superklasse, maar soms is het nodig dat de subklasse een eigen implementatie heeft voor een methode. Dit wordt aangeduid als overschrijven van methodes.

GeometrischObject.java

public GeometrischObject{ private String kleur; private boolean opgevuld; public GeometrischObject( String kleur, boolean opgevuld){ this.kleur = kleur; this.opgevuld = opgevuld; } public String toString(){ return "Figuur met kleur " + kleur + " en " + (opgevuld ? "opgevuld " : "niet opgevuld "); } }

Cirkel.java

public Cirkel{ private double straal; public Cirkel(double straal, String kleur, boolean opgevuld){ super(kleur, opgevuld); this.straal = straal; } public String toString(){ //Zelfde signatuur als bij GeometrischObject.java return super.toString() + " en met straal gelijk aan " + straal; } }
  • In het bovenstaand voorbeeld is de methode toString() gedefinieerd in de klasse GeometrischObject en wordt aangevuld in de klasse Cirkel. Beide methodes kunnen in de klasse Cirkel gebruikt worden.
  • Enkele aandachtspunten
    • Een methode kan enkel overschreven worden als hij in de superklasse als public gedeclareerd is. Dus een private methode kan niet worden overschreven, omdat deze methode niet toegankelijke is buiten de klasse.
    • Een statische methode (met het sleutelwoord static) kan overgeërfd worden, maar kan niet overschreven worden. Een statische methode gedeclareerd in de superklasse kan gebruikt worden in de subklasse, maar kan niet overschreven worden.

Overschrijven vr Overladen

Overladen is het definiëren van meerdere methodes met dezelfde naam, maar een andere signatuur. Overschrijven is het maken van een nieuwe implementatie voor een methode in de subklasse.

  • Overladen van methodes is het maken van meerdere methodes met dezelfde naam, maar met een ander signatuur. De signatuur was de verzameling van alle paramters die de methode verwacht. Om een methode te overschrijven moet dezelfde methode gedefinieerd worden in subklasse met dezelfde signatuur en hetzelfde retourtype.

Test_a.java

public class Test_a { public static void main(String[] args){ A a = new A(); a.p(10); a.p(10.0); } } public class B { public void p(double i){ System.out.println(i * 2); } } public A extends B{ // Deze methode overschrijft de methode van de klasse B public void p(double i){ System.out.println(i) } }

Test_b.java

public class Test_b { public static void main(String[] args){ A a = new A(); a.p(10); a.p(10.0); } } public class B { public void p(double i){ System.out.println(i * 2); } } public A extends B{ // Deze methode overlaadt de methode van de klasse B public void p(int i){ System.out.println(i) } }
  • In de code Test_a.java overschrijft de methode p(double i) in de klasse A de methode p(double i) in de klasse B. In de code Test_b.java biedt klasse B de methode p(double i) aan en biedt klasse A de methode p(int i) aan. De klasse A is een uitbreiding van klasse B. Bijgevolg is de methode p(double i) ook beschikbaar in de klasse A. dus de methode p(int i) overlaadt de methode p(double i).
  • Merk op:

    • Overschreven methodes bevinden zich in verschillende klassen. Deze klassen zijn gerelateerd via overerving. Overladen methodes bevinden zich in dezelfde klasse of in verschillende klassen. Waarbij deze klassen ook gerelateerd zijn via overerving.
    • Overschreven methodes hebben dezelfde signatuur en retourtype; overladen methodes hebben dezelfde naam, maar een verschillende parameterlijst.
  • Om fouten te vermijden, kan je gebruik maken van een speciale Java syntax: override annaotatie. Hiervoor dien je @Override te zetten voor een methode in de subklasse. bijvoorbeeld:

public class Cirkel extends GeometrischObject{ @Override public String String(){ return super.toString() + "\nde straal is " + straal; } }
  • Deze annotatie duidt aan de geannoteerde methode verplicht is om een methode in de superklasse te overschrijven. Als deze methode in de superklasse niet bestaat, dan genereert de methode aan fout. Hierdoor, kan je fouten verhinderen waardoor je zeker bent dat je een methode aan het overschrijven bent en geen methode aan het overladen.

De klasse Object en de toString() methode

elke klasse is een afgeleide klasse van de klasse java.lang.Object

  • Zelfs al heb je in een klasse geen overerving gespecifieerd, dan nog is een klasse die je maakt afgeleid van de klasse Object. Dit is het principe van objectgeörienteerd programmeren: alles is een object en elk object heeft zijn eigenschappen en methodes, die beschreven zijn in een klasse.

  • De volgende twee klassen zijn equivalent:

    public class Klasse{ ... }

en

public class Klasse extends Object{ ... }
  • De klassen String, GeometrischObject erven impliciet over van de klasse Object. Het is belangrijk om te weten dat er in de klasse Object ook enkele methode gedefinieerd zijn zoals de methode toString().
  • De signatuur van de methode toString() is
public String toString()
  • Door de methode aan te roepen maak je een String aan die typisch het object beschrijft. Bij default, zal de toString() methode de naam van de klasse waartoe het object behoort, weergeven. Daarna komt er het at teken (@) en de geheugenlokatie van het object. Bijvoorbeeld, het volgend stukje code zal resulteren in:
Cirkel cirkel_obj1 = new Cirkel(); System.out.println(cirkel_obj1.toString());
  • De uitkomst van dit stukje code is: Cirkel@15037e5. Deze boodschap heeft geen relevante informatie. In de klasse Cirkel zullen we typisch de methode toString() overschrijven met een nieuwe implementatie. Bijvoorbeeld:
public class Lening{ //Andere methodes en gegevensvariabelen zijn weggelaten public String toString(){ return "opgemaakt op " + datumOpgemaakt + "\nkleur : " + kleur; } }

Polymorfisme

Polymorfise betekent dat een variabele gedeclareerd als een supertype kan verwijzen naar een object van een subtype

  • De drie zuilen van objectgeörienteerd programmeren zijn encapsulatie, overerving en polymorfisme. De eerste twee hebben we al gezien. Hier gaan we dieper in op de laatste zuil, polymorfisme.
  • Laat ons eerst twee termen introduceren: subtype en supertype. Een klasse definieert een type. A type gedefinieerd door een subklasse noemen we een subtype, en een type gedefinieerd door een superklasse noemen we een supertype. Hierdoor, kan je zeggen dat Cirkel een subtype is van GeometrischObject en GeometrischObject is een supertype van Cirkel.
  • Zoals we weten is een subklasse een specializatie van een superklasse; elke instantie (object) van een subklasse erft eigenschappen en methodes van de superklasse, maar niet omgekeerd. Elke cirkel is een geometrisch object, maar niet elk geometrisch object is een cirkel.
  • Hierdoor kan je een instantie van een subklasse altijd doorgeven als actuele parameter van zijn superklasse.
  • We illustreren dit met een voorbeeld:
public class PolymorfismeDemo{ /** Main methode **/ public static void main(String[] args){ toonObject(new Cirkel(1, "rood")); toonObject(new Rechthoek(1, "blauw")); } /** toon een Geometrisch object met zijn eigenschappen **/ public static void toonObject(GeometrischObject object){ System.out.println("Gemaakt op " + object.getDatumGemaakt() + ". Kleur is " + object.getKleur()); } }
  • De methode toonObject heeft een parameter van het type GeometrischObject. Je kan die methode aanroepen door een instantie door te geven van het type GeometrischObject. Zowel Cirkel en Rechthoek zijn van het type GeometrischObject, waardoor instanties van het type Cirkel en Rechthoek ook kunnen doorgegeven worden aan de methode.
  • Een object van een subklasse kan dus gebruiktw worden waar een object van zijn superklasse wordt gebruikt. Dit is bekend als polymorfisme. Eenvoudigweg, betekent polymorfisme dat een variabele van een supertype kan verwijzen naar een object van een subtype.

Dynamische binding

Een methode kan geïmplementeerd zijn in verschillende klassen binnen een overervingsrelatie. De Java Virtuele Machine bepaalt welke methode er moet uitgevoerd worden at runtime (= tijdens het uitvoeren van het programma).

  • Een methode kan gedefinieerd worden in een superklasse en overschreven worden in een subklasse. Bijvoorbeeld, de toString() methode is gedefinieerd in een klasse Object en wordt overschreven in de klasse GeometrischObject. Beschouw het volgend stukje code:
Object o = new GeometrischObject(); System.out.println(o.toString());
  • Welke implementatie van de toString() methode wordt opgeroepen door het object o?
  • Om deze vraag te beantwoorden, introduceren we twee termen: gedeclareerd type en actueel type. In dit geval is Object het gedeclareerde type van o. Het type dat in de declaratie van de variabele of verwijzing staat is het gedeclareerde type van de variabele.
  • Tijdens de uitvoering wordt het duidelijk dat o eigenlijk een GeometrischObject is. De instantie is gecreëerd door een constructor van GeometrischObject. Het actuele type van o is GeometrischObject. Het actuele type van een variabele is de actuele klasse voor het object waarnaar de variabele verwijst.
  • Welke toString() methode dat wordt aangeroepen door o wordt bepaald door het actuele type van o. Dit noemen we dynamische binding (Engels: dynamic binding).
  • Dynamische binding werkt als volgt: Stel dat een object o een instantie is van klasses C1, C2, C3,... waarbij C1 een subklasse is van C2 en C2 een subklasse van C3,... Wanneer het object o een methode p uitvoert dan zal Java op zoek gaan naar een implementatie van die methode p in de klasses C1, C2, C3, ... in die volgorde totdat de methode gevonden is. Wanneer een implementatie gevonden is, stopt de zoektocht en wordt die implementatie uitgevoerd. Dus als de klasse C1 geen implementatie voor de methode heeft, en C2 en C3 wel, dan wordt de implementatie in klasse C2 uitgevoerd.

DynamischeBinding.java

public class DynamischeBinding{ public static void main(String[] args){ m(new MasterStudent()); m(new Student()); m(new Persoon()); m(new Object()); } public static void m(Object x){ System.out.println(x.toString()); } }

MasterStudent.java

public class MasterStudent extends Student{ }

Student.java

public class Student extends Persoon{ @Override public String toString(){ return "Student"; } }

Persoon.java

public class Persoon{ @Override public String toString(){ return "Persoon"; } }
  • Het uitvoeren van de main methode in de klasse DynamischeBinding.java resulteert in de volgende uitvoer:
Student
Student
Persoon
java.lang.Object@130c19b

results matching ""

    No results matching ""