Eredità multipla Java


168

Nel tentativo di comprendere appieno come risolvere i molteplici problemi di ereditarietà di Java, ho una domanda classica che devo chiarire.

Diciamo che ho una classe che Animalha delle sottoclassi Birde Horseche devo creare una classe Pegasusche si estende da Birde Horsepoiché Pegasusè sia un uccello che un cavallo.

Penso che questo sia il classico problema dei diamanti. Da quello che posso capire il modo classico per risolvere questo è quello di rendere il Animal, Birde Horseinterfacce classi e implementare Pegasusda loro.

Mi chiedevo se ci fosse un altro modo per risolvere il problema in cui posso ancora creare oggetti per uccelli e cavalli. Se ci fosse un modo per essere in grado di creare anche animali sarebbe fantastico ma non necessario.


6
Penso che sia possibile creare manualmente le classi e memorizzarle come membri (composizione anziché ereditarietà). Con la classe Proxy ( docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html ) questa potrebbe essere un'opzione, anche se avrai bisogno anche delle interfacce.
Gábor Bakos,

4
@RAM quindi non dovrebbe estendere Bird piuttosto che avere un comportamento che gli consentirà di prendere un volo. : Problema D risolto
Yogesh,

11
Esattamente. Che ne dici di un'interfaccia CanFly. :-)
Cocco sorpreso

28
Penso che questo sia l'approccio sbagliato. Hai animali: cavalli, uccelli. E tu hai proprietà: volare, carnivoro. Un Pegasus non è un HorseBird, è un cavallo che può volare. Le proprietà dovrebbero nelle interfacce. Così public class Pegasus extends Horse implements Flying.
Boris the Spider,

12
Capisco perché pensi che sia sbagliato e non aderisca alle regole della biologia e apprezzo la tua preoccupazione, ma per quanto riguarda il programma che devo costruire, che in realtà ha a che fare con le banche, questo è stato l'approccio migliore per me. Dato che non volevo pubblicare i miei problemi reali, poiché ciò sarebbe contrario alle regole, ho cambiato un po 'l'esempio. Grazie comunque ...
Sheli,

Risposte:


115

Potresti creare interfacce per le classi di animali (classe nel significato biologico), come public interface Equidaeper i cavalli e public interface Avialaeper gli uccelli (non sono un biologo, quindi i termini potrebbero essere sbagliati).

Quindi puoi ancora creare un

public class Bird implements Avialae {
}

e

public class Horse implements Equidae {}

e anche

public class Pegasus implements Avialae, Equidae {}

Aggiungendo dai commenti:

Al fine di ridurre il codice duplicato, è possibile creare una classe astratta che contenga la maggior parte del codice comune degli animali che si desidera implementare.

public abstract class AbstractHorse implements Equidae {}

public class Horse extends AbstractHorse {}

public class Pegasus extends AbstractHorse implements Avialae {}

Aggiornare

Vorrei aggiungere un altro dettaglio. Come osserva Brian , questo è qualcosa che l'OP già sapeva.

Tuttavia, voglio sottolineare che suggerisco di aggirare il problema della "multi-ereditarietà" con le interfacce e che non consiglio di usare interfacce che rappresentano già un tipo concreto (come Bird) ma più un comportamento (altri si riferiscono a dattilografia, che è buona, ma intendo solo: la classe biologica degli uccelli, Avialae). Inoltre, non consiglio di usare i nomi delle interfacce che iniziano con una "I" maiuscola, ad esempio IBird, che non dice nulla sul perché hai bisogno di un'interfaccia. Questa è la differenza con la domanda: costruisci la gerarchia dell'ereditarietà usando le interfacce, usa le classi astratte quando utile, implementa le classi concrete dove necessario e usa la delega se appropriato.


9
Il che ... è esattamente ciò che l'OP afferma di sapere che puoi fare nel Q.
Brian Roach,

4
Dato che Pegasus è già un cavallo (che vola), penso che potresti riutilizzare più codice se estende Horse e implementa Avialae.
Pablo Lozano,

8
Non sono sicuro, non ho ancora visto un Pegasus. Tuttavia, in quel caso preferirei usare un AbstractHorse, che può anche essere usato per costruire zebre o altri animali simili a cavalli.
Moritz Petersen,

5
@MoritzPetersen Se vuoi davvero riutilizzare le astrazioni e dare nomi significativi, probabilmente AbstractEquidaesarebbe più adatto di AbstractHorse. Sarebbe strano avere una zebra che estende un cavallo astratto. Bella risposta, a proposito.
afsantos,

3
L'implementazione della tipizzazione anatra comporterebbe anche Duckun'implementazione di classe Avialae?
Margarciaisaia,

88

Esistono due approcci fondamentali alla combinazione di oggetti:

  • Il primo è Eredità . Come hai già identificato, i limiti dell'eredità significano che non puoi fare ciò di cui hai bisogno qui.
  • Il secondo è Composizione . Poiché l'ereditarietà non è riuscita, devi utilizzare la composizione.

Il modo in cui funziona è che hai un oggetto Animale. All'interno di tale oggetto si aggiungono quindi ulteriori oggetti che forniscono le proprietà e i comportamenti richiesti.

Per esempio:

  • Bird estende animali implementa IFlier
  • Il cavallo estende gli animali implementa IHerbivore, IQuadruped
  • Pegasus estende Animal implementa IHerbivore, IQuadruped, IFlier

Ora IFliersembra proprio così:

 interface IFlier {
     Flier getFlier();
 }

Quindi Birdassomiglia a questo:

 class Bird extends Animal implements IFlier {
      Flier flier = new Flier();
      public Flier getFlier() { return flier; }
 }

Ora hai tutti i vantaggi dell'ereditarietà. Puoi riutilizzare il codice. Puoi avere una collezione di IFlier e utilizzare tutti gli altri vantaggi del polimorfismo, ecc.

Tuttavia hai anche tutta la flessibilità di Composizione. Puoi applicare tutte le diverse interfacce e la classe di supporto composita che desideri a ciascun tipo di Animal- con tutto il controllo di cui hai bisogno su come ogni bit è impostato.

Strategia Modello approccio alternativo alla composizione

Un approccio alternativo a seconda di cosa e come stai facendo è avere la Animalclasse base contenente una raccolta interna per mantenere l'elenco di comportamenti diversi. In tal caso, si finisce per usare qualcosa di più vicino al modello di strategia. Ciò offre vantaggi in termini di semplificazione del codice (ad esempio Horse, non è necessario conoscere nulla Quadrupedo Herbivore), ma se non si utilizza anche l'approccio dell'interfaccia si perdono molti vantaggi del polimorfismo, ecc.


Un'alternativa simile potrebbe anche essere l'uso delle classi di tipi, sebbene quelle non siano così naturali da usare in Java (devi usare metodi di conversione e così via), questa introduzione potrebbe essere utile per avere l'idea: typeclassopedia.bitbucket.org
Gábor Bakos

1
Questa è una soluzione molto migliore rispetto all'approccio raccomandato nella risposta accettata. Ad esempio, se in seguito voglio aggiungere "colore becco" all'interfaccia Bird, ho un problema. Pegasus è un composito, che prende elementi da cavalli e uccelli, ma né completamente cavallo né uccello. L'uso della composizione ha perfettamente senso in questo scenario.
JDB ricorda ancora Monica il

Ma getFlier()deve essere reimplementato per ogni tipo di uccello.
SMUsamaShah,

1
@ LifeH2O Suddividili in blocchi di funzionalità condivisa e daglielo. per esempio, si potrebbe avere MathsTeachere EnglishTeachersia ereditare Teacher, ChemicalEngineer, MaterialsEngineerecc ereditare Engineer. Teachered Engineerentrambi implementano Component. L' Personallora appena ha una lista di Components, e si può dare loro il diritto Components per quella Person. cioè person.getComponent(Teacher.class), person.getComponent(MathsTeacher.class)ecc.
Tim B,

1
Questa è la risposta migliore Come regola generale, l'eredità rappresenta "è" e un'interfaccia rappresenta "possibile". Un Pegasus È un animale che può volare e camminare. Un uccello è un animale che può volare. Un cavallo è un animale che può camminare.
Robear,

43

Ho un'idea stupida:

public class Pegasus {
    private Horse horseFeatures; 
    private Bird birdFeatures; 

   public Pegasus(Horse horse, Bird bird) {
     this.horseFeatures = horse;
     this.birdFeatures = bird;
   }

  public void jump() {
    horseFeatures.jump();
  }

  public void fly() {
    birdFeatures.fly();
  }
}

24
Funzionerà, ma non mi piace quel tipo di approccio (Wrappper) perché allora sembra che Pegasus abbia un cavallo, invece è un cavallo.
Pablo Lozano,

Grazie. Non ho potuto fare a meno di pubblicare l'idea. So che è stupido, ma non ho visto PERCHÉ è stupido ...
Pavel Janicek,

3
È quasi come una versione non riproposta dell'approccio alla composizione dalla risposta di Tim B.
Stephan,

1
Questo è più o meno il modo accettato per farlo, anche se dovresti avere qualcosa come IJumps con un metodo "jump" implementato da Horse e Pegasus e IFlies con un metodo "fly" implementato da Bird e Pegasus.
MatsT,

2
@Pablo no, un Pegasus HA caratteristiche cavallo e HA caratteristiche uccello. +1 per la risposta perché mantiene il codice semplice, a differenza delle soluzioni Java più clunkier, che generano classi.
JaneGoodall,

25

Posso suggerire il concetto di Duck-typing ?

Molto probabilmente tenderai a far estendere a Pegasus un'interfaccia Bird e Horse ma la tipizzazione di anatre suggerisce in realtà che dovresti piuttosto ereditare il comportamento . Come già affermato nei commenti, un pegasus non è un uccello ma può volare. Quindi il tuo Pegasus dovrebbe piuttosto ereditare Flyableun'interfaccia e dire Gallopableun'interfaccia.

Questo tipo di concetto viene utilizzato nel modello di strategia . L'esempio dato mostra in realtà come un'anatra eredita FlyBehavioure QuackBehavioure ancora ci possono essere anatre, ad esempio RubberDuck, che non possono volare. Avrebbero anche potuto Duckestendere una Birdclasse ma poi avrebbero rinunciato a una certa flessibilità, perché tutti Ducksarebbero stati in grado di volare, anche i poveri RubberDuck.


19

Tecnicamente parlando, è possibile estendere solo una classe alla volta e implementare più interfacce, ma quando si mettono le mani sull'ingegneria del software, preferirei piuttosto suggerire una soluzione specifica del problema non generalmente rispondibile. A proposito, è buona pratica OO, non estendere le classi concrete / estendere solo le classi astratte per prevenire comportamenti ereditari indesiderati - non esiste un "animale" e nessun uso di un oggetto animale ma solo animali concreti.


13

In Java 8, che è ancora in fase di sviluppo a partire da febbraio 2014, è possibile utilizzare i metodi predefiniti per ottenere una sorta di C ++, come l'ereditarietà multipla. Puoi anche dare un'occhiata a questo tutorial che mostra alcuni esempi che dovrebbero essere più facili da iniziare a lavorare rispetto alla documentazione ufficiale.


1
Tieni presente, tuttavia, che se Bird e Horse hanno entrambi un metodo predefinito, incontrerai comunque il problema del diamante e dovrai implementarlo separatamente nella tua classe Pegasus (o ottenere un errore del compilatore).
Mikkel Løkke,

@Mikkel Løkke: la classe Pegasus deve definire le sostituzioni per i metodi ambigui, ma può implementarle semplicemente delegando a entrambi i metodi super (o entrambi in un ordine scelto).
Holger,

12

È sicuro tenere un cavallo in una stalla con mezza porta, poiché un cavallo non può superare una mezza porta. Pertanto ho installato un servizio di custodia per cavalli che accetta qualsiasi oggetto di tipo cavallo e lo mette in una stalla con una mezza porta.

Quindi un cavallo come un animale può volare anche un cavallo?

Pensavo molto all'ereditarietà multipla, tuttavia ora che sto programmando da oltre 15 anni, non mi interessa più implementare l'ereditarietà multipla.

Il più delle volte, quando ho cercato di far fronte a un progetto che puntava verso l'eredità multipla, in seguito sono arrivato al rilascio che mi mancava capire il dominio del problema.

O

Se sembra un'anatra e ciondola come un'anatra ma ha bisogno di batterie, probabilmente hai l'astrazione sbagliata .


Se leggo correttamente la tua analogia, stai dicendo che all'inizio le interfacce sembrano grandi, in quanto ti consentono di aggirare i problemi di progettazione, ad esempio puoi usare l'API di qualcun altro forzando le tue classi attraverso le loro interfacce. Ma dopo alcuni anni, ti sei reso conto che il problema era inizialmente un cattivo design.
CS

8

Java non ha un problema di ereditarietà multipla, poiché non ha ereditarietà multipla. Questo è di progettazione, al fine di risolvere il vero problema dell'ereditarietà multipla (il problema del diamante).

Esistono diverse strategie per mitigare il problema. Il più immediatamente raggiungibile è l'oggetto composito che Pavel suggerisce (essenzialmente come C ++ lo gestisce). Non so se l'ereditarietà multipla tramite linearizzazione C3 (o simile) sia in gioco per il futuro di Java, ma ne dubito.

Se la tua domanda è accademica, la soluzione corretta è che Bird e Horse sono più concreti, ed è falso supporre che un Pegasus sia semplicemente un Bird e un Horse combinati. Sarebbe più corretto affermare che un Pegaso ha alcune proprietà intrinseche in comune con Uccelli e Cavalli (cioè hanno forse antenati comuni). Ciò può essere sufficientemente modellato come sottolinea la risposta di Moritz.


6

Penso che dipenda molto dalle tue esigenze e da come le tue classi di animali debbano essere usate nel tuo codice.

Se vuoi essere in grado di utilizzare metodi e funzionalità delle tue implementazioni Horse and Bird all'interno della tua classe Pegasus, allora potresti implementare Pegasus come una composizione di un uccello e un cavallo:

public class Animals {

    public interface Animal{
        public int getNumberOfLegs();
        public boolean canFly();
        public boolean canBeRidden();
    }

    public interface Bird extends Animal{
        public void doSomeBirdThing();
    }
    public interface Horse extends Animal{
        public void doSomeHorseThing();
    }
    public interface Pegasus extends Bird,Horse{

    }

    public abstract class AnimalImpl implements Animal{
        private final int numberOfLegs;

        public AnimalImpl(int numberOfLegs) {
            super();
            this.numberOfLegs = numberOfLegs;
        }

        @Override
        public int getNumberOfLegs() {
            return numberOfLegs;
        }
    }

    public class BirdImpl extends AnimalImpl implements Bird{

        public BirdImpl() {
            super(2);
        }

        @Override
        public boolean canFly() {
            return true;
        }

        @Override
        public boolean canBeRidden() {
            return false;
        }

        @Override
        public void doSomeBirdThing() {
            System.out.println("doing some bird thing...");
        }

    }

    public class HorseImpl extends AnimalImpl implements Horse{

        public HorseImpl() {
            super(4);
        }

        @Override
        public boolean canFly() {
            return false;
        }

        @Override
        public boolean canBeRidden() {
            return true;
        }

        @Override
        public void doSomeHorseThing() {
            System.out.println("doing some horse thing...");
        }

    }

    public class PegasusImpl implements Pegasus{

        private final Horse horse = new HorseImpl();
        private final Bird bird = new BirdImpl();


        @Override
        public void doSomeBirdThing() {
            bird.doSomeBirdThing();
        }

        @Override
        public int getNumberOfLegs() {
            return horse.getNumberOfLegs();
        }

        @Override
        public void doSomeHorseThing() {
            horse.doSomeHorseThing();
        }


        @Override
        public boolean canFly() {
            return true;
        }

        @Override
        public boolean canBeRidden() {
            return true;
        }
    }
}

Un'altra possibilità è quella di utilizzare un approccio Entity-Component-System anziché l'ereditarietà per definire i tuoi animali. Ovviamente questo significa che non avrai singole classi Java degli animali, ma invece sono definite solo dai loro componenti.

Alcuni pseudo codici per un approccio Entity-Component-System potrebbero apparire così:

public void createHorse(Entity entity){
    entity.setComponent(NUMER_OF_LEGS, 4);
    entity.setComponent(CAN_FLY, false);
    entity.setComponent(CAN_BE_RIDDEN, true);
    entity.setComponent(SOME_HORSE_FUNCTIONALITY, new HorseFunction());
}

public void createBird(Entity entity){
    entity.setComponent(NUMER_OF_LEGS, 2);
    entity.setComponent(CAN_FLY, true);
    entity.setComponent(CAN_BE_RIDDEN, false);
    entity.setComponent(SOME_BIRD_FUNCTIONALITY, new BirdFunction());
}

public void createPegasus(Entity entity){
    createHorse(entity);
    createBird(entity);
    entity.setComponent(CAN_BE_RIDDEN, true);
}

4

puoi avere una gerarchia di interfaccia e quindi estendere le tue classi dalle interfacce selezionate:

public interface IAnimal {
}

public interface IBird implements IAnimal {
}

public  interface IHorse implements IAnimal {
}

public interface IPegasus implements IBird,IHorse{
}

e quindi definire le classi in base alle esigenze, estendendo un'interfaccia specifica:

public class Bird implements IBird {
}

public class Horse implements IHorse{
}

public class Pegasus implements IPegasus {
}

1
Oppure può solo: Classe pubblica Pegasus estende Animal Implements Cavallo, Uccello
Batman,

OP è già a conoscenza di questa soluzione che sta cercando un modo alternativo per farlo
Yogesh,

@Batman, certo che può farlo, ma se vuole estendere la gerarchia avrebbe bisogno di seguire questo approccio
richardtz,

IBirde IHorsedovrebbe implementare IAnimalanzichéAnimal
oliholz ​​il

@Yogesh, hai ragione. Ho trascurato il luogo in cui lo afferma. Come "principiante" cosa devo fare ora, eliminare la risposta o lasciarla lì? Grazie.
richardtz,

4

Ehm, la tua classe può essere la sottoclasse solo per un'altra, ma puoi implementare tutte le interfacce che desideri.

Un Pegaso è in realtà un cavallo (è un caso speciale di un cavallo), che è in grado di volare (che è l '"abilità" di questo cavallo speciale). D'altra parte, si può dire, il Pegasus è un uccello, che può camminare ed è a 4 zampe - tutto dipende da come è più facile scrivere il codice.

Come nel tuo caso puoi dire:

abstract class Animal {
   private Integer hp = 0; 
   public void eat() { 
      hp++; 
   }
}
interface AirCompatible { 
   public void fly(); 
}
class Bird extends Animal implements AirCompatible { 
   @Override
   public void fly() {  
       //Do something useful
   }
} 
class Horse extends Animal {
   @Override
   public void eat() { 
      hp+=2; 
   }

}
class Pegasus extends Horse implements AirCompatible {
   //now every time when your Pegasus eats, will receive +2 hp  
   @Override
   public void fly() {  
       //Do something useful
   }
}

3

Le interfacce non simulano l'ereditarietà multipla. I creatori di Java hanno considerato l'ereditarietà multipla errata, quindi non esiste nulla di simile in Java.

Se si desidera combinare la funzionalità di due classi in una, utilizzare la composizione dell'oggetto. ie

public class Main {
    private Component1 component1 = new Component1();    
    private Component2 component2 = new Component2();
}

E se vuoi esporre determinati metodi, definiscili e consenti loro di delegare la chiamata al controller corrispondente.

Qui le interfacce possono tornare utili: se Component1implementa interfaccia Interface1e Component2strumenti Interface2, puoi definirlo

class Main implements Interface1, Interface2

In modo da poter usare gli oggetti in modo intercambiabile dove il contesto lo consente.

Quindi dal mio punto di vista, non puoi entrare nel problema dei diamanti.


Non è sbagliato nello stesso modo in cui i puntatori di memoria diretta, i tipi senza segno e il sovraccarico dell'operatore non sono sbagliati; semplicemente non è necessario per portare a termine il lavoro. Java è stato progettato come un linguaggio snello facile da imparare. Non inventare cose e sperare per il meglio, questo è un campo di conoscenza, non ipotesi.
Gimby,


3
  1. Definire le interfacce per la definizione delle funzionalità. È possibile definire più interfacce per più funzionalità. Queste capacità possono essere implementate da specifici animali o uccelli .
  2. Usa l' ereditarietà per stabilire relazioni tra le classi condividendo dati / metodi non statici e non pubblici.
  3. Usa Decorator_pattern per aggiungere funzionalità in modo dinamico. Ciò consentirà di ridurre il numero di classi e combinazioni di ereditarietà.

Dai un'occhiata all'esempio seguente per una migliore comprensione

Quando utilizzare il motivo decorativo?


2

Per ridurre la complessità e semplificare la lingua, java non supporta l'ereditarietà multipla.

Considera uno scenario in cui A, B e C sono tre classi. La classe C eredita le classi A e B. Se le classi A e B hanno lo stesso metodo e lo chiami da oggetto di classe figlio, ci sarà ambiguità nel chiamare il metodo di classe A o B.

Poiché gli errori di compilazione sono migliori degli errori di runtime, java esegue il rendering dell'errore di compilazione se si ereditano 2 classi. Quindi, sia che tu abbia lo stesso metodo o diverso, ora ci sarà un errore di compilazione.

class A {  
    void msg() {
        System.out.println("From A");
    }  
}

class B {  
    void msg() {
        System.out.println("From B");
    }  
}

class C extends A,B { // suppose if this was possible
    public static void main(String[] args) {  
        C obj = new C();  
        obj.msg(); // which msg() method would be invoked?  
    }
} 

2

Per risolvere il problema dell'ereditarietà multipla nell'interfaccia Java → viene utilizzato

Note J2EE (core JAVA) di Mr. KVR Pagina 51

Giorno - 27

  1. Le interfacce sono sostanzialmente utilizzate per sviluppare tipi di dati definiti dall'utente.
  2. Rispetto alle interfacce possiamo realizzare il concetto di eredità multiple.
  3. Con le interfacce possiamo realizzare il concetto di polimorfismo, legame dinamico e quindi possiamo migliorare le prestazioni di un programma JAVA a turno di spazio di memoria e tempo di esecuzione.

Un'interfaccia è un costrutto che contiene la raccolta di metodi puramente indefiniti o un'interfaccia è una raccolta di metodi puramente astratti.

[...]

Giorno - 28:

Sintassi-1 per riutilizzare le funzionalità dell'interfaccia (s) per la classe:

[abstract] class <clsname> implements <intf 1>,<intf 2>.........<intf n>
{
    variable declaration;
    method definition or declaration;
};

Nella sintassi sopra clsname rappresenta il nome della classe che eredita le funzionalità dal numero 'n' di interfacce. 'Implements' è una parola chiave che viene utilizzata per ereditare le funzionalità dell'interfaccia (s) a una classe derivata.

[...]

Sintassi-2 che eredita 'n' numero di interfacce su un'altra interfaccia:

interface <intf 0 name> extends <intf 1>,<intf 2>.........<intf n>
{     
    variable declaration cum initialization;
    method declaration;
};

[...]

Sintassi-3:

[abstract] class <derived class name> extends <base class name> implements <intf 1>,<intf 2>.........<intf n>
{
  variable declaration;
  method definition or declaration;
};
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.