Mehrfachvererbung in Java

Heute werden wir uns die Mehrfachvererbung in Java ansehen. Vor einiger Zeit habe ich einige Beiträge über Vererbung, Schnittstellen und Komposition in Java geschrieben. In diesem Beitrag werden wir uns die Mehrfachvererbung in Java ansehen und dann Komposition und Vererbung vergleichen.

Mehrfachvererbung in Java

Die Mehrfachvererbung in Java ermöglicht die Erstellung einer einzelnen Klasse mit mehreren Superklassen. Im Gegensatz zu einigen anderen beliebten objektorientierten Programmiersprachen wie C++ unterstützt Java keine Mehrfachvererbung in Klassen. Java unterstützt keine Mehrfachvererbung in Klassen, da dies zu diamantenen Problemen führen kann und es bessere Möglichkeiten gibt, das gleiche Ergebnis wie bei der Mehrfachvererbung zu erzielen, anstatt einen komplexen Weg zur Lösung anzubieten.

Diamantproblem in Java

Um das Diamantproblem leicht zu verstehen, nehmen wir an, dass Mehrfachvererbung in Java unterstützt wird. In diesem Fall könnten wir eine Klassenhierarchie wie im folgenden Bild haben. Angenommen, Superklasse ist eine abstrakte Klasse, die einige Methoden deklariert, und ClassA, ClassB sind konkrete Klassen. SuperClass.java

package com.journaldev.inheritance;

public abstract class SuperClass {

	public abstract void doSomething();
}

ClassA.java

package com.journaldev.inheritance;

public class ClassA extends SuperClass{
	
	@Override
	public void doSomething(){
		System.out.println("doSomething implementation of A");
	}
	
	//KlasseA eigene Methode
	public void methodA(){
		
	}
}

ClassB.java

package com.journaldev.inheritance;

public class ClassB extends SuperClass{

	@Override
	public void doSomething(){
		System.out.println("doSomething implementation of B");
	}
	
	//KlasseB spezifische Methode
	public void methodB(){
		
	}
}

Angenommen, die Implementierung von KlasseC würde etwas wie unten aussehen und sie erweitert sowohl KlasseA als auch KlasseB. ClassC.java

package com.journaldev.inheritance;

// dies ist nur eine Annahme zur Erklärung des Diamantproblems
// dieser Code wird nicht kompilieren
public class ClassC extends ClassA, ClassB{

	public void test(){
		// Aufruf der Methode der Superklasse
		doSomething();
	}

}

Beachten Sie, dass die Methode test() einen Aufruf der übergeordneten Methode doSomething() macht. Dies führt zu Unklarheit, da der Compiler nicht weiß, welche übergeordnete Methode ausgeführt werden soll. Aufgrund des diamantförmigen Klassendiagramms wird dies in Java als Diamantproblem bezeichnet. Das Diamantproblem ist der Hauptgrund, warum Java keine Mehrfachvererbung in Klassen unterstützt. Beachten Sie, dass das oben beschriebene Problem mit der Mehrfachklassenererbung auch bei nur drei Klassen auftreten kann, von denen alle mindestens eine gemeinsame Methode haben.

Mehrfachvererbung in Java-Schnittstellen

Sie haben vielleicht bemerkt, dass ich immer sage, dass Mehrfachvererbung in Klassen nicht unterstützt wird, aber in Schnittstellen unterstützt wird. Eine einzelne Schnittstelle kann mehrere Schnittstellen erweitern, hier ist ein einfaches Beispiel. InterfaceA.java

package com.journaldev.inheritance;

public interface InterfaceA {

	public void doSomething();
}

InterfaceB.java

package com.journaldev.inheritance;

public interface InterfaceB {

	public void doSomething();
}

Beachten Sie, dass beide Schnittstellen die gleiche Methode deklarieren. Jetzt können wir eine Schnittstelle erstellen, die beide Schnittstellen erweitert, wie unten gezeigt. InterfaceC.java

package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

	//die gleiche Methode wird in InterfaceA und InterfaceB deklariert
	public void doSomething();
	
}

Das ist völlig in Ordnung, da die Schnittstellen nur die Methoden deklarieren und die tatsächliche Implementierung von konkreten Klassen durchgeführt wird. Es besteht also keine Möglichkeit für Mehrfachvererbung in Java-Schnittstellen. Deshalb kann eine Java-Klasse mehrere Schnittstellen implementieren, ähnlich wie im folgenden Beispiel. 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 unten führen zur gleichen konkreten Implementierung
		objA.doSomething();
		objB.doSomething();
		objC.doSomething();
	}

}

Haben Sie bemerkt, dass ich jedes Mal, wenn ich eine Methode der Oberklasse überschreibe oder eine Methode einer Schnittstelle implementiere, die @Override-Annotation verwende? Die Override-Annotation ist eine der drei integrierten Java-Annotationen und wir sollten immer die Override-Annotation verwenden, wenn wir eine Methode überschreiben.

Komposition zur Rettung

Was also tun, wenn wir die Funktion methodA() von ClassA und die Funktion methodB() von ClassB in ClassC nutzen möchten? Die Lösung liegt in der Verwendung von Komposition. Hier ist eine überarbeitete Version von ClassC, die die Methoden beider Klassen durch Komposition nutzt und auch die Methode doSomething() von einem der Objekte verwendet. 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();
	}
}

Komposition vs. Vererbung

Eine der bewährten Praktiken des Java-Programmierens ist es, „die Komposition der Vererbung vorzuziehen“. Wir werden einige Aspekte untersuchen, die diesen Ansatz begünstigen.

  1. Angenommen, wir haben eine Superklasse und eine Unterklasse wie folgt: 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;
    	}
    }
    

    Der obige Code wird kompiliert und funktioniert einwandfrei, aber was ist, wenn die Implementierung von ClassC wie folgt geändert wird: ClassC.java

    package com.journaldev.inheritance;
    
    public class ClassC{
    
    	public void methodC(){
    	}
    
    	public void test(){
    	}
    }
    

    Bemerken Sie, dass die Methode test() bereits in der Unterklasse existiert, aber der Rückgabetyp unterschiedlich ist. Nun wird ClassD nicht kompiliert, und wenn Sie eine IDE verwenden, wird sie vorschlagen, den Rückgabetyp entweder in der Superklasse oder in der Unterklasse zu ändern. Stellen Sie sich nun die Situation vor, in der wir mehrere Ebenen der Klassenvererbung haben und die Superklasse nicht von uns kontrolliert wird. Wir haben keine andere Wahl, als die Signatur unserer Unterklasse-Methode oder ihren Namen zu ändern, um den Kompilierungsfehler zu beheben. Außerdem müssen wir an allen Stellen, an denen unsere Unterklasse-Methode aufgerufen wurde, eine Änderung vornehmen. Vererbung macht also unseren Code zerbrechlich. Das oben genannte Problem tritt mit der Komposition niemals auf, und das macht sie gegenüber der Vererbung bevorzugter.

  2. Ein weiteres Problem bei der Vererbung ist, dass alle Methoden der Superklasse dem Client offenbart werden, und wenn unsere Superklasse nicht ordnungsgemäß entworfen ist und Sicherheitslücken aufweist, werden wir trotz aller Sorgfalt bei der Implementierung unserer Klasse von der schlechten Implementierung der Superklasse beeinflusst. Komposition hilft uns dabei, einen kontrollierten Zugriff auf die Methoden der Superklasse zu ermöglichen, während die Vererbung keinen Kontrollzugriff auf die Methoden der Superklasse bietet. Dies ist ebenfalls einer der Hauptvorteile von Komposition gegenüber Vererbung.

  3. Ein weiterer Vorteil der Komposition besteht darin, dass sie Flexibilität bei der Methodenaufruf ermöglicht. Unsere obige Implementierung der ClassC ist nicht optimal und bietet eine Bindung zur Kompilierzeit mit der Methode, die aufgerufen wird. Mit minimalen Änderungen können wir den Methodenaufruf flexibel gestalten und ihn dynamisch machen. 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();
    	}
    }
    

    Die Ausgabe des obigen Programms ist:

    doSomething implementation von A
    doSomething implementation von B
    

    Diese Flexibilität beim Methodenaufruf ist bei der Vererbung nicht verfügbar und unterstützt bewährte Praktiken, die die Komposition der Vererbung vorziehen.

  4. Unit-Tests sind in der Komposition einfach, weil wir wissen, welche Methoden wir von der Oberklasse verwenden und wir sie für Tests simulieren können. Bei Vererbung hängen wir stark von der Oberklasse ab und wissen nicht, welche Methoden der Oberklasse verwendet werden. Daher müssen wir alle Methoden der Oberklasse testen, was zusätzliche Arbeit ist und aufgrund der Vererbung unnötig ist.

Das war alles zu Mehrfachvererbung in Java und ein kurzer Blick auf die Komposition.

Source:
https://www.digitalocean.com/community/tutorials/multiple-inheritance-in-java