Vandaag zullen we kijken naar Meervoudige Erfenis in Java. Een tijdje geleden schreef ik een paar berichten over erfenis, interface en compositie in Java. In dit bericht zullen we kijken naar meervoudige erfenis in Java en vervolgens compositie en erfenis vergelijken.
Meervoudige Erfenis in Java
Meervoudige erfenis in Java is de mogelijkheid om een enkele klasse te maken met meerdere superklassen. In tegenstelling tot sommige andere populaire objectgeoriënteerde programmeertalen zoals C++, biedt Java geen ondersteuning voor meervoudige erfenis in klassen. Java ondersteunt geen meervoudige erfenis in klassen omdat dit tot het diamantprobleem kan leiden en in plaats daarvan zijn er betere manieren om hetzelfde resultaat te bereiken als met meervoudige erfenis.
Diamantprobleem in Java
Om het diamantprobleem gemakkelijk te begrijpen, laten we aannemen dat meervoudige overervingen worden ondersteund in Java. In dat geval zouden we een klassenhiërarchie kunnen hebben zoals in onderstaande afbeelding. Laten we zeggen dat Superklasse een abstracte klasse is die een methode declareert, en ClassA en ClassB zijn concrete klassen.
Superklasse.java
package com.journaldev.inheritance;
public abstract class SuperClass {
public abstract void doSomething();
}
KlasseA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of A");
}
//Eigen methode van KlasseA
public void methodA(){
}
}
KlasseB.java
package com.journaldev.inheritance;
public class ClassB extends SuperClass{
@Override
public void doSomething(){
System.out.println("doSomething implementation of B");
}
//Specifieke methode van KlasseB
public void methodB(){
}
}
Stel nu dat de implementatie van KlasseC er als volgt uit zou zien en dat het zowel KlasseA als KlasseB uitbreidt. KlasseC.java
package com.journaldev.inheritance;
// Dit is slechts een aanname om het diamantprobleem uit te leggen
// Deze code compileert niet
public class ClassC extends ClassA, ClassB{
public void test(){
// Roepen van superklasse methode
doSomething();
}
}
Let op dat de methode test()
een oproep doet naar de superklasse methode doSomething()
. Dit leidt tot ambiguïteit, omdat de compiler niet weet welke methode van de superklasse moet worden uitgevoerd. Vanwege het diamantvormige klassendiagram wordt dit het Diamond-probleem genoemd in Java. Het diamantprobleem in Java is de belangrijkste reden waarom Java geen ondersteuning biedt voor meervoudige overerving in klassen. Merk op dat het bovenstaande probleem met meervoudige klasse-erfenis ook kan optreden met slechts drie klassen waarin ze allemaal ten minste één gemeenschappelijke methode hebben.
Meervoudige Overerving in Java-Interfaces
Je hebt misschien opgemerkt dat ik altijd zeg dat meervoudige overerving niet wordt ondersteund in klassen, maar wel in interfaces. Een enkele interface kan meerdere interfaces uitbreiden, hieronder volgt een eenvoudig voorbeeld. InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {
public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;
public interface InterfaceB {
public void doSomething();
}
Merk op dat beide interfaces dezelfde methode declareren, nu kunnen we een interface hebben die beide interfaces uitbreidt zoals hieronder. InterfaceC.java
package com.journaldev.inheritance;
public interface InterfaceC extends InterfaceA, InterfaceB {
//dezelfde methode wordt gedeclareerd in zowel InterfaceA als InterfaceB
public void doSomething();
}
Dit is perfect acceptabel omdat de interfaces alleen de methoden declareren en de feitelijke implementatie zal worden gedaan door concrete klassen die de interfaces implementeren. Daarom is er geen mogelijkheid voor enige vorm van ambiguïteit bij meervoudige overerving in Java-interfaces. Daarom kan een Java-klasse meerdere interfaces implementeren, iets zoals het onderstaande voorbeeld. InterfacesImpl.java
package com.journaldev.inheritance;
public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {
@Override
public void doSomething() {
System.out.println("doSomething implementation of concrete class");
}
public static void main(String[] args) {
InterfaceA objA = new InterfacesImpl();
InterfaceB objB = new InterfacesImpl();
InterfaceC objC = new InterfacesImpl();
// alle methodenaufrufe hieronder gaan naar dezelfde concrete implementatie
objA.doSomething();
objB.doSomething();
objC.doSomething();
}
}
Heb je opgemerkt dat elke keer dat ik een methode van een superklasse overschrijf of een methode van een interface implementeer, ik de annotatie @Override gebruik. De Override-annotatie is een van de drie ingebouwde java-annotaties en we moeten altijd de override-annotatie gebruiken bij het overschrijven van een methode.
Compositie ter redding
Wat moeten we dan doen als we de functie methodA()
van ClassA
en de functie methodB()
van ClassB
willen gebruiken in ClassC
. De oplossing ligt in het gebruik van compositie. Hier is een herschreven versie van ClassC die compositie gebruikt om beide klassenmethoden te benutten en ook de methode doSomething() van een van de objecten gebruikt. ClassC.java
package com.journaldev.inheritance;
public class ClassC{
ClassA objA = new ClassA();
ClassB objB = new ClassB();
public void test(){
objA.doSomething();
}
public void methodA(){
objA.methodA();
}
public void methodB(){
objB.methodB();
}
}
Compositie versus Erfenis
Een van de beste praktijken in Java-programmering is om “compositie boven overerving te verkiezen”. We zullen enkele aspecten bekijken die deze benadering begunstigen.
-
Stel dat we een superklasse en subklasse hebben zoals hieronder:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } }
ClassD.java
package com.journaldev.inheritance; public class ClassD extends ClassC{ public int test(){ return 0; } }
De bovenstaande code compileert en werkt prima, maar wat als de implementatie van ClassC als volgt wordt gewijzigd:
ClassC.java
package com.journaldev.inheritance; public class ClassC{ public void methodC(){ } public void test(){ } }
Merk op dat de methode
test()
al bestaat in de subklasse maar het retourtype verschilt. Nu zal ClassD niet compileren en als je een IDE gebruikt, zal het je suggereren om het retourtype te veranderen in de superklasse of de subklasse. Stel je nu de situatie voor waarbij we meerdere niveaus van klassikale overerving hebben en de superklasse niet door ons wordt gecontroleerd. We zullen geen andere keuze hebben dan de methodehandtekening van onze subklasse te wijzigen of de naam ervan om de compileringsfout te verwijderen. Bovendien zullen we een wijziging moeten aanbrengen op alle plaatsen waar onze subklassenmethode werd aangeroepen, dus overerving maakt onze code kwetsbaar. Het bovenstaande probleem zal zich nooit voordoen bij samenstelling en dat maakt het gunstiger boven overerving. -
Nog een probleem met overerving is dat we alle methoden van de superklasse blootstellen aan de client en als onze superklasse niet goed is ontworpen en er beveiligingslekken zijn, worden we zelfs getroffen door de slechte implementatie van de superklasse, hoewel we volledige zorg dragen bij het implementeren van onze klasse. Compositie helpt ons bij het bieden van gecontroleerde toegang tot de methoden van de superklasse, terwijl overerving geen controle biedt over de methoden van de superklasse, dit is ook een van de belangrijkste voordelen van compositie ten opzichte van overerving.
-
Een ander probleem met overerving is dat we alle methoden van de superklasse blootstellen aan de client. Als onze superklasse niet goed is ontworpen en er beveiligingslekken zijn, worden we zelfs getroffen door de slechte implementatie van de superklasse, zelfs als we volledige zorg dragen voor de implementatie van onze klasse. Compositie helpt ons bij het bieden van gecontroleerde toegang tot de methoden van de superklasse, terwijl overerving geen controle biedt over de methoden van de superklasse. Dit is ook een van de belangrijkste voordelen van compositie ten opzichte van overerving.
ClassC.java
package com.journaldev.inheritance; public class ClassC{ SuperClass obj = null; public ClassC(SuperClass o){ this.obj = o; } public void test(){ obj.doSomething(); } public static void main(String args[]){ ClassC obj1 = new ClassC(new ClassA()); ClassC obj2 = new ClassC(new ClassB()); obj1.test(); obj2.test(); } }
De output van het bovenstaande programma is:
doSomething-implementatie van A doSomething-implementatie van B
Deze flexibiliteit in methodenoproep is niet beschikbaar bij overerving en bevordert de beste praktijk om de voorkeur te geven aan compositie boven overerving.
-
Unit testing is eenvoudig bij compositie omdat we weten welke methoden we gebruiken vanuit de superklasse en we deze kunnen simuleren voor testdoeleinden. In tegenstelling tot overerving, waar we sterk afhankelijk zijn van de superklasse en niet weten welke methoden van de superklasse zullen worden gebruikt, moeten we alle methoden van de superklasse testen. Dat is extra werk en onnodig vanwege overerving.
Dat is alles voor meervoudige overerving in Java en een bekijke op compositie.
Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java