Ereditarietà multipla in Java

Oggi ci occuperemo della Multipla Ereditarietà in Java. Qualche tempo fa ho scritto alcuni post sull’ereditarietà, sull’interfaccia e sulla composizione in Java. In questo post, esamineremo la multipla ereditarietà in Java e poi compareremo la composizione e l’ereditarietà.

Multipla Ereditarietà in Java

La multipla ereditarietà in Java è la capacità di creare una singola classe con più superclassi. A differenza di altri popolari linguaggi di programmazione orientati agli oggetti come C++, Java non fornisce supporto per la multipla ereditarietà nelle classi. Java non supporta la multipla ereditarietà nelle classi perché può portare al problema del diamante e invece di fornire un modo complesso per risolverlo, ci sono modi migliori attraverso i quali possiamo ottenere lo stesso risultato della multipla ereditarietà.

Problema del diamante in Java

Per capire facilmente il problema del diamante, supponiamo che il supporto per l’ereditarietà multipla fosse supportato in Java. In tal caso, potremmo avere una gerarchia di classi come nell’immagine sottostante. Supponiamo che SuperClasse sia una classe astratta che dichiara alcuni metodi e ClassA, ClassB siano classi concrete. SuperClasse.java

package com.journaldev.inheritance;

public abstract class SuperClass {

	public abstract void doSomething();
}

ClasseA.java

package com.journaldev.inheritance;

public class ClassA extends SuperClass{
	
	@Override
	public void doSomething(){
		System.out.println("doSomething implementation of A");
	}
	
	//Metodo proprio di ClasseA
	public void methodA(){
		
	}
}

ClasseB.java

package com.journaldev.inheritance;

public class ClassB extends SuperClass{

	@Override
	public void doSomething(){
		System.out.println("doSomething implementation of B");
	}
	
	//Metodo specifico di ClasseB
	public void methodB(){
		
	}
}

Ora supponiamo che l’implementazione di ClasseC sarebbe qualcosa del genere e che estende sia ClasseA che ClasseB. ClasseC.java

package com.journaldev.inheritance;

// questa è solo un'ipotesi per spiegare il problema del diamante
//questo codice non verrà compilato
public class ClassC extends ClassA, ClassB{

	public void test(){
		//chiamata al metodo della superclasse
		doSomething();
	}

}

Si nota che il metodo test() effettua una chiamata al metodo doSomething() della superclasse. Questo porta all’ambiguità poiché il compilatore non sa quale metodo della superclasse eseguire. A causa del diagramma delle classi a forma di diamante, questo è chiamato Problema del Diamante in Java. Il problema del diamante in Java è la principale ragione per cui Java non supporta le eredità multiple nelle classi. Si noti che il problema sopra con l’ereditarietà di classe multipla può verificarsi anche con solo tre classi in cui tutte hanno almeno un metodo comune.

Ereditarietà Multipla nelle Interfacce Java

Potrebbe essere notato che sto sempre dicendo che le eredità multiple non sono supportate nelle classi, ma sono supportate nelle interfacce. Un’interfaccia singola può estendere più interfacce, di seguito è riportato un esempio semplice. InterfaceA.java

package com.journaldev.inheritance;

public interface InterfaceA {

	public void doSomething();
}

InterfaceB.java

package com.journaldev.inheritance;

public interface InterfaceB {

	public void doSomething();
}

Si noti che entrambe le interfacce stanno dichiarando lo stesso metodo, ora possiamo avere un’interfaccia che estende entrambe queste interfacce come segue. InterfaceC.java

package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

	// lo stesso metodo è dichiarato sia in InterfaceA che in InterfaceB
	public void doSomething();
	
}

Questo è perfettamente accettabile perché le interfacce stanno solo dichiarando i metodi e l’implementazione effettiva sarà fatta dalle classi concrete che implementano le interfacce. Quindi non c’è possibilità di ambiguità nei multipli ereditamenti nelle interfacce Java. Ecco perché una classe Java può implementare più interfacce, qualcosa come nell’esempio seguente. 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();
		
		//tutte le chiamate di metodo qui sotto stanno andando alla stessa implementazione concreta
		objA.doSomething();
		objB.doSomething();
		objC.doSomething();
	}

}

Hai notato che ogni volta che sovrascrivo un metodo della superclasse o implemento un metodo dell’interfaccia, sto usando l’annotazione @Override. L’annotazione Override è una delle tre annotazioni Java integrate e dovremmo sempre usare l’annotazione override quando sovrascriviamo un metodo.

Composizione per il salvataggio

Quindi cosa fare se vogliamo utilizzare la funzione methodA() di ClassA e la funzione methodB() di ClassB in ClassC. La soluzione sta nell’utilizzare composizione. Ecco una versione refattorizzata di ClassC che utilizza la composizione per utilizzare i metodi di entrambe le classi e utilizza anche il metodo doSomething() di uno degli oggetti. 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();
	}
}

Composizione vs Ereditarietà

Uno dei migliori approcci alla programmazione Java è “favorire la composizione rispetto all’ereditarietà”. Esamineremo alcuni degli aspetti che favoriscono questo approccio.

  1. Supponiamo di avere una superclasse e una sottoclasse come segue: 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;
    	}
    }
    

    Il codice sopra compila e funziona correttamente ma cosa succede se l’implementazione della ClasseC viene modificata come segue: ClassC.java

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

    Nota che il metodo test() esiste già nella sottoclasse ma il tipo di ritorno è diverso. Ora la ClasseD non compilerà e se stai utilizzando un IDE, ti suggerirà di cambiare il tipo di ritorno sia nella superclasse che nella sottoclasse. Immagina ora la situazione in cui abbiamo più livelli di ereditarietà di classi e la superclasse non è controllata da noi. Non avremo altra scelta che cambiare la firma del metodo della nostra sottoclasse o il suo nome per rimuovere l’errore di compilazione. Inoltre, dovremo apportare una modifica in tutti i luoghi in cui il metodo della nostra sottoclasse veniva invocato, rendendo quindi il nostro codice fragile. Il problema sopra descritto non si verificherà mai con la composizione e questo la rende più favorevole rispetto all’ereditarietà.

  2. Un altro problema dell’ereditarietà è che esponiamo tutti i metodi della superclasse al cliente e se la nostra superclasse non è progettata correttamente e presenta falle di sicurezza, anche se ci prendiamo cura completa nell’implementare la nostra classe, siamo influenzati dalla scarsa implementazione della superclasse. La composizione ci aiuta a fornire un accesso controllato ai metodi della superclasse, mentre l’ereditarietà non fornisce alcun controllo sui metodi della superclasse, questo è anche uno dei principali vantaggi della composizione rispetto all’ereditarietà.

  3. Un altro vantaggio della composizione è che offre flessibilità nell’invocazione dei metodi. La nostra implementazione di ClassC non è ottimale e fornisce un vincolo di compilazione con il metodo che verrà invocato; con un cambiamento minimo possiamo rendere l’invocazione del metodo flessibile e renderla dinamica. 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();
    	}
    }
    

    L’output del programma sopra è:

    implementazione di doSomething di A
    implementazione di doSomething di B
    

    Questa flessibilità nell’invocazione del metodo non è disponibile nell’ereditarietà e favorisce la buona pratica di preferire la composizione rispetto all’ereditarietà.

  4. Il testing unitario è semplice nella composizione perché sappiamo quali metodi stiamo utilizzando dalla superclasse e possiamo simulare il tutto per il testing mentre nell’ereditarietà dipendiamo pesantemente dalla superclasse e non sappiamo quali metodi della superclasse verranno utilizzati, quindi dobbiamo testare tutti i metodi della superclasse, questo è un lavoro aggiuntivo e dobbiamo farlo inutilmente a causa dell’ereditarietà.

E questo è tutto per le eredità multiple in Java e un breve sguardo alla composizione.

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