JavaRush /Java Blog /Random-IT /Ereditarietà multipla in Java. Confronto tra composizione...
HonyaSaar
Livello 26
Москва

Ereditarietà multipla in Java. Confronto tra composizione ed ereditarietà

Pubblicato nel gruppo Random-IT
Qualche tempo fa ho scritto diversi post sull'ereditarietà, le interfacce e la composizione in Java. In questo articolo esamineremo l'ereditarietà multipla e poi conosceremo i vantaggi della composizione rispetto all'ereditarietà.
Ereditarietà multipla in Java.  Confronto tra composizione ed eredità - 1

Eredità multipla in Java

L'ereditarietà multipla è la capacità di creare classi con più classi genitore. A differenza di altri popolari linguaggi orientati agli oggetti come C++, Java non supporta l'ereditarietà di classi multiple. Non lo sostiene a causa della probabilità di incontrare il “problema del diamante” e preferisce invece fornire una sorta di approccio globale per risolverlo, utilizzando le migliori opzioni che possiamo ottenere per ottenere un risultato di eredità simile.

"Il problema dei diamanti"

Per comprendere più semplicemente il problema del diamante, supponiamo che Java sia supportata dall'ereditarietà multipla. In questo caso possiamo ottenere classi con la gerarchia mostrata nella figura seguente. gerarchia delle classi dei diamantiSupponiamo che SuperClasssia una classe astratta che descrive un determinato metodo e che le classi ClassAe ClassBsiano classi reali. 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("Какая-то реализация класса A");
    }
  //собственный метод класса  ClassA
    public void methodA(){
    }
}
Supponiamo ora che la classe ClassCerediti da ClassAe ClassBallo stesso tempo e allo stesso tempo abbia la seguente implementazione:
package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
    public void test(){
        //вызов метода родительского класса
        doSomething();
    }
}
Si noti che il metodo test()chiama un metodo doSomething()della classe genitore, il che porterà ad ambiguità perché il compilatore non sa quale metodo della superclasse dovrebbe essere chiamato. A causa della forma del diagramma di ereditarietà delle classi in questa situazione, che ricorda il contorno di un diamante sfaccettato, il problema è chiamato “problema del diamante”. Questo è il motivo principale per cui Java non supporta l'ereditarietà di classi multiple. Si noti che questo problema con l'ereditarietà di più classi può verificarsi anche con tre classi che hanno almeno un metodo comune.

Ereditarietà multipla e interfacce

Potresti aver notato che dico sempre "l'ereditarietà multipla non è supportata tra le classi", ma è supportata tra le interfacce. Di seguito è mostrato un semplice esempio: 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 hanno un metodo con lo stesso nome. Ora supponiamo di avere un'interfaccia che eredita da entrambe le interfacce. InterfaceC.java
package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

    //метод, с тем же названием описан в  InterfaceA и InterfaceB
    public void doSomething();
Qui tutto è ideale, poiché le interfacce sono solo una prenotazione/descrizione di un metodo e l'implementazione del metodo stesso avverrà nella classe concreta che implementa queste interfacce, quindi non c'è possibilità di incontrare ambiguità con l'ereditarietà multipla delle interfacce. Questo è il motivo per cui le classi in Java possono ereditare da più interfacce. Mostriamolo con l'esempio qui sotto. InterfacesImpl.java
package com.journaldev.inheritance;

public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {

    @Override
    public void doSomething() {
        System.out.println("doSomething реализация реального класса ");
    }

    public static void main(String[] args) {
        InterfaceA objA = new InterfacesImpl();
        InterfaceB objB = new InterfacesImpl();
        InterfaceC objC = new InterfacesImpl();

        //все вызываемые ниже методы получат одинаковую реализацию конкретного класса

        objA.doSomething();
        objB.doSomething();
        objC.doSomething();
    }
}
Potresti aver notato che ogni volta che sovrascrivo un metodo descritto in una superclasse o in un'interfaccia, utilizzo l'annotazione @Override. Questa è una delle tre annotazioni Java integrate e dovresti sempre usarla quando esegui l'override dei metodi.

La composizione come salvezza

E se volessimo utilizzare una methodA()classe ClassAe una funzione methodB()di classe ClassBin ClassС? Una soluzione a questo potrebbe essere la composizione, una versione riscritta ClassCche implementa entrambi i metodi della classe ClassAe ClassBha anche un'implementazione doSomething()per 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 o eredità?

È buona pratica di programmazione Java sfruttare la composizione rispetto all'ereditarietà. Esamineremo alcuni aspetti a favore di questo approccio.
  1. Supponiamo di avere la seguente combinazione di classi genitore-erede:

    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 viene compilato e funziona bene, ma cosa succederebbe se ClassCfosse implementato diversamente:

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

    Tieni presente che il metodo test()esiste già nella classe discendente, ma restituisce un risultato di tipo diverso. Ora ClassD, se stai utilizzando un IDE, non verrà compilato. Ti verrà consigliato di modificare il tipo restituito nel discendente o nella superclasse.

    Ora immaginiamo una situazione in cui esiste un'ereditarietà multilivello delle classi e la superclasse non è disponibile per le nostre modifiche. Ora, per eliminare l'errore di compilazione, non abbiamo altra scelta che cambiare la firma o il nome del metodo della sottoclasse. Dovremo anche apportare modifiche a tutti i luoghi in cui è stato chiamato questo metodo. Pertanto, l’ereditarietà rende il nostro codice fragile.

    Il problema sopra descritto non si verifica mai nel caso della composizione, e rende quindi quest'ultima preferibile all'ereditarietà.

  2. Il prossimo problema con l'ereditarietà è che esponiamo tutti i metodi del genitore al client. E se la superclasse non è progettata in modo molto corretto e contiene buchi di sicurezza. Quindi, anche se ci prendiamo la massima cura della sicurezza nell’implementazione della nostra sottoclasse, dipenderemo comunque dall’implementazione difettosa della classe genitore.

    La composizione ci aiuta a fornire un accesso controllato ai metodi di una superclasse, mentre l'ereditarietà non mantiene alcun controllo sui suoi metodi. Questo è anche uno dei principali vantaggi della composizione rispetto all’ereditarietà.

  3. Un altro vantaggio della composizione è che aggiunge flessibilità quando si chiamano metodi. L'implementazione della classe ClassCsopra descritta non è ottimale e utilizza l'associazione anticipata al metodo chiamato. Modifiche minime ci consentiranno di rendere flessibile la chiamata del metodo e consentire l'associazione tardiva (associazione in fase di esecuzione).

    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();
        }
    }

    Il programma sopra mostrerà:

    doSomething implementation of A
    doSomething implementation of B

    Questa flessibilità nella chiamata del metodo non si riscontra con l'ereditarietà, il che rende la composizione l'approccio migliore.

  4. Il test unitario è più semplice nel caso della composizione perché sappiamo che per tutti i metodi che vengono utilizzati nella superclasse possiamo eseguire lo stub dei test, mentre nell'ereditarietà dipendiamo molto dalla superclasse e non sappiamo come funzionano i metodi della classe genitore sarà usato. Quindi, a causa dell'ereditarietà, dovremo testare tutti i metodi della superclasse, il che è un lavoro non necessario.

    Idealmente, l’ereditarietà dovrebbe essere utilizzata solo quando la relazione “ is-a ” è vera per le classi genitore e figlio, altrimenti si dovrebbe preferire la composizione.

Articolo originale
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION