Многократное наследование Java

168

В попытке полностью понять, как решить проблемы множественного наследования в Java, у меня есть классический вопрос, который мне нужно прояснить.

Допустим , у меня есть класс Animalэто имеет вложенные классы Birdи Horseи мне нужно сделать класс , Pegasusкоторый простирается от Birdи Horseтак Pegasusявляется как птица и лошадь.

Я думаю, что это классическая проблема с бриллиантами. Из того, что я могу понять , классический способ решить это сделать Animal, Birdи Horseклассы интерфейсов и осуществлять Pegasusиз них.

Мне было интересно, есть ли другой способ решить проблему, в которой я все еще могу создавать объекты для птиц и лошадей. Если бы существовал способ создавать животных, это было бы замечательно, но не обязательно.

Sheli
источник
6
Я думаю, что вы можете создавать классы вручную и хранить их как члены (состав вместо наследования). Для класса Proxy ( docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html ) это может быть вариантом, хотя вам также понадобятся интерфейсы.
Габор Бакос
4
@ РАМ тогда он не должен расширять Птицу, а должен иметь поведение, которое позволит ему совершить полет. : D проблема решена
Йогеш
11
Именно. Как насчет интерфейса CanFly. :-)
Удивленный кокос
28
Я думаю, что это неправильный подход. У вас есть животные - Лошади, Птицы. А у тебя есть свойства - Летающий, Плотоядный. Пегас - это не лошадь, а лошадь, которая может летать. Свойства должны в интерфейсах. Так public class Pegasus extends Horse implements Flying.
Борис Паук
12
Я понимаю, почему вы считаете, что это неправильно, не придерживаетесь правил биологии и цените вашу заботу, но в отношении программы, которую мне нужно разработать, которая на самом деле связана с банками, это был лучший подход для меня. Поскольку я не хотел публиковать свои настоящие проблемы, поскольку это противоречило бы правилам, я немного изменил пример. Спасибо, хотя ...
Шели

Ответы:

115

Вы можете создать интерфейсы для классов животных (класс в биологическом смысле), например public interface Equidaeдля лошадей и public interface Avialaeптиц (я не биолог, поэтому термины могут быть неправильными).

Тогда вы все еще можете создать

public class Bird implements Avialae {
}

и

public class Horse implements Equidae {}

а также

public class Pegasus implements Avialae, Equidae {}

Добавление из комментариев:

Чтобы уменьшить количество дублирующегося кода, вы можете создать абстрактный класс, содержащий большую часть общего кода животных, которые вы хотите реализовать.

public abstract class AbstractHorse implements Equidae {}

public class Horse extends AbstractHorse {}

public class Pegasus extends AbstractHorse implements Avialae {}

Обновить

Я хотел бы добавить еще одну деталь. Как отмечает Брайан , это уже известно ОП.

Тем не менее, я хочу подчеркнуть, что я предлагаю обойти проблему «мульти-наследования» с интерфейсами и что я не рекомендую использовать интерфейсы, которые представляют уже конкретный тип (например, Bird), а больше поведения (другие ссылаются на типирование утки, что тоже хорошо, но я имею в виду просто: биологический класс птиц, Avialae). Я также не рекомендую использовать имена интерфейсов, начинающиеся с заглавной буквы «я», например IBird, которая ничего не говорит о том, зачем вам нужен интерфейс. В этом отличие вопроса: создайте иерархию наследования с использованием интерфейсов, используйте абстрактные классы, когда это полезно, реализуйте конкретные классы, где это необходимо, и используйте делегирование, если это необходимо.

Мориц Петерсен
источник
9
Что ... это именно то, что, по словам ОП, они знают, что вы можете сделать в Q.
Брайан Роуч
4
Поскольку Pegasus - это уже лошадь (которая летает), я думаю, вы могли бы использовать больше кода, если расширяет Horse и реализует Avialae.
Пабло Лосано
8
Я не уверен, я еще не видел Пегаса. Тем не менее, в этом случае я бы предпочел использовать AbstractHorse, который также может быть использован для создания зебр или других животных, похожих на лошадей.
Мориц Петерсен
5
@MoritzPetersen Если вы действительно хотите повторно использовать абстракции и давать значимые имена, вероятно AbstractEquidae, будет более подходящим, чем AbstractHorse. Было бы странно, чтобы зебра расширяла абстрактную лошадь. Хороший ответ, кстати.
afsantos
3
Реализация утиной типизации тоже будет включать Duckреализацию классов Avialae?
mgarciaisaia
88

Существует два основных подхода к объединению объектов:

  • Первое - это Наследование . Как вы уже определили, ограничения наследования означают, что вы не можете делать то, что вам нужно здесь.
  • Второе - Композиция . Поскольку наследование не удалось, вам нужно использовать композицию.

Это работает так, что у вас есть объект Animal. Внутри этого объекта вы затем добавляете дополнительные объекты, которые задают необходимые вам свойства и поведение.

Например:

  • Птица расширяет Животные орудия IFlier
  • Лошадь расширяет Животные орудия труда IHerbivore, IQuadruped
  • Пегас расширяет Animal реализует IHerbivore, IQuadruped, IFlier

Теперь IFlierпросто выглядит так:

 interface IFlier {
     Flier getFlier();
 }

Так Birdвыглядит вот так:

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

Теперь у вас есть все преимущества наследования. Вы можете повторно использовать код. Вы можете иметь коллекцию IFliers и использовать все остальные преимущества полиморфизма и т. Д.

Однако у вас также есть все гибкость от композиции. Вы можете применять столько разных интерфейсов и составных классов поддержки, сколько вам нужно, к каждому типу Animal- с таким количеством контроля, который вам необходим для настройки каждого бита.

Стратегия Pattern альтернативный подход к композиции

Альтернативный подход в зависимости от того, что и как вы делаете, заключается в том, чтобы Animalбазовый класс содержал внутреннюю коллекцию для хранения списка различных вариантов поведения. В этом случае вы используете что-то ближе к шаблону стратегии. Это дает преимущества с точки зрения упрощения кода (например Horse, не нужно ничего знать о Quadrupedили Herbivore), но если вы также не будете использовать интерфейсный подход, вы потеряете много преимуществ полиморфизма и т. Д.

Тим Б
источник
Аналогичной альтернативой может быть также использование классов типов, хотя они не так естественны для использования в Java (необходимо использовать методы конвертера и т. Д.), Это введение может быть полезно для понимания
Gábor Bakos
1
Это гораздо лучшее решение, чем подход, рекомендованный в принятом ответе. Например, если позже я захочу добавить «цвет клюва» в интерфейс Bird, у меня возникнет проблема. Пегас - это комплекс, в котором собраны элементы от лошадей и птиц, но не полностью лошади и птицы. Использование композиции имеет смысл в этом сценарии.
JDB до сих пор помнит Монику
Но getFlier()должно быть переопределено для каждого вида птиц.
SMUsamaShah
1
@ LifeH2O Разделите их на блоки общей функциональности и дайте им это. то есть вы можете иметь MathsTeacherи EnglishTeacherнаследовать Teacher, ChemicalEngineerи MaterialsEngineerт.д. наследовать Engineer. Teacherи Engineerоба реализуют Component. PersonТогда просто есть список Componentс, и вы можете дать им правильные ComponentS для этого Person. то есть person.getComponent(Teacher.class), person.getComponent(MathsTeacher.class)и т.д.
Тим B
1
Это лучший ответ. Как правило, наследование представляет «есть», а интерфейс - «может». Пегас - это животное, которое может летать и ходить. Птица - это животное, которое умеет летать. Лошадь - это животное, которое может ходить.
Robear
43

У меня глупая идея

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
Это сработает, но мне не нравится такой подход (Wrappper), потому что тогда кажется, что у Пегаса есть лошадь, а не лошадь.
Пабло Лосано
Спасибо. Я не мог сдержать себя отправкой идеи. Я вроде знаю, что это глупо, но я не видел, ПОЧЕМУ это глупо ...
Павел Яничек
3
Это почти как неопределенный вариант композиционного подхода из ответа Тима Б.
Стефан
1
Это более или менее приемлемый способ сделать это, хотя у вас также должно быть что-то вроде IJumps с методом «прыжка», который реализован Horse и Pegasus и IFlies с методом «fly», который реализован Bird и Pegasus.
Матс
2
@ Пабло нет, в Пегасе есть ЛОШАДЬ и Птица. +1 за ответ, потому что он сохраняет код простым, в отличие от более громоздких, порождающих классы, правильных Java-решений.
JaneGoodall
25

Могу ли я предложить концепцию Duck-typing ?

Скорее всего, вы бы хотели, чтобы Pegasus расширял интерфейс Bird и Horse, но типирование утки фактически предполагает, что вы должны скорее наследовать поведение . Как уже говорилось в комментариях, пегас не птица, но может летать. Так что ваш Пегас должен наследовать Flyable-интерфейс и, скажем,Gallopable -интерфейс.

Этот вид концепции используется в шаблоне стратегии . Приведенный пример показывает , на самом деле, как утка наследует FlyBehaviourи QuackBehaviourи по- прежнему могут быть утки, например RubberDuck, которые не могут летать. Они могли бы также сделать Duckрасширение Bird-классом, но тогда они отказались бы от некоторой гибкости, потому что каждый Duckмог бы летать, даже бедные RubberDuck.

snrlx
источник
19

С технической точки зрения, вы можете расширять только один класс за раз и реализовывать несколько интерфейсов, но при разработке программного обеспечения я предпочел бы предложить решение для конкретной проблемы, которое обычно не отвечает. Кстати, это хорошая ОО-практика - не расширять конкретные классы / расширять только абстрактные классы для предотвращения нежелательного наследования - нет такого понятия, как «животное» и использование объекта животного, а только конкретных животных.

Smutje
источник
13

В Java 8, которая все еще находится на стадии разработки по состоянию на февраль 2014 года, вы можете использовать методы по умолчанию для достижения своего рода C ++ - как множественное наследование. Вы также можете взглянуть на этот учебник, который показывает несколько примеров, с которыми легче начать работать, чем с официальной документацией.

GOTO 0
источник
1
Имейте в виду, что если ваш Bird и Horse оба имеют метод по умолчанию, вы все равно столкнетесь с проблемой алмаза и должны будете реализовать ее отдельно в своем классе Pegasus (или получите ошибку компилятора).
Миккель Лёкке
@Mikkel Løkke: класс Pegasus должен определять переопределения для неоднозначных методов, но может реализовывать их, просто делегируя один из супер методов (или оба в выбранном порядке).
Хольгер
12

Держать лошадь в конюшне с половиной двери безопасно, так как лошадь не может пройти через половину двери. Поэтому я настраиваю службу содержания лошадей, которая принимает любой предмет типа лошади и помещает его в конюшню с половиной двери.

Так похожа ли лошадь на животное, способное управлять даже лошадью?

Раньше я много думал о множественном наследовании, однако теперь, когда я программирую более 15 лет, я больше не беспокоюсь о реализации множественного наследования.

Чаще всего, когда я пытался справиться с дизайном, который указывал на множественное наследование, я позже приходил к выводу, что я упустил понимание проблемной области.

ИЛИ

Если он выглядит как утка и крякает как утка, но ему нужны батарейки, возможно, у вас неправильная абстракция .

Ян Рингроз
источник
Если я правильно прочитал вашу аналогию, вы говорите, что вначале интерфейсы кажутся великолепными, поскольку они позволяют вам обойти проблемы проектирования, например, вы можете использовать чужой API, проталкивая свои классы через их интерфейсы. Но через несколько лет вы поняли, что проблема в плохом дизайне с самого начала.
CS
8

У Java нет проблемы множественного наследования, поскольку у нее нет множественного наследования. Это сделано для того, чтобы решить реальную проблему множественного наследования (проблема алмаза).

Существуют разные стратегии смягчения проблемы. Наиболее быстро достижимым является объект Composite, который предлагает Павел (по сути, как C ++ справляется с этим). Я не знаю, есть ли множественное наследование через линеаризацию C3 (или подобное) на картах для будущего Java, но я сомневаюсь в этом.

Если ваш вопрос носит академический характер, то правильное решение состоит в том, что Птица и Лошадь более конкретны, и неверно полагать, что Пегас - это просто Птица и Лошадь вместе взятые. Правильнее будет сказать, что у Пегаса есть определенные внутренние свойства, общие с Птицами и Лошадями (то есть у них могут быть общие предки). Это может быть достаточно смоделировано, как указывает ответ Морица.

Миккель Лёкке
источник
6

Я думаю, что это очень сильно зависит от ваших потребностей и того, как ваши классы животных будут использоваться в вашем коде.

Если вы хотите использовать методы и функции ваших реализаций Horse и Bird внутри вашего класса Pegasus, то вы можете реализовать Pegasus как композицию Bird и Horse:

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

Другая возможность - использовать Entity-Component-System. подход вместо наследования для определения ваших животных. Конечно, это означает, что у вас не будет отдельных классов Java животных, но вместо этого они определяются только их компонентами.

Некоторый псевдокод для подхода Entity-Component-System может выглядеть следующим образом:

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

вы можете иметь иерархию интерфейсов, а затем расширять ваши классы из выбранных интерфейсов:

public interface IAnimal {
}

public interface IBird implements IAnimal {
}

public  interface IHorse implements IAnimal {
}

public interface IPegasus implements IBird,IHorse{
}

и затем определите ваши классы по мере необходимости, расширяя определенный интерфейс:

public class Bird implements IBird {
}

public class Horse implements IHorse{
}

public class Pegasus implements IPegasus {
}
richardtz
источник
1
Или он может просто: Общественный класс Пегас расширяет Лошадь орудий для животных, Птицу
Бэтмена
ОП уже знает об этом решении, он ищет альтернативный способ сделать это
Йогеш
@ Бэтмен, конечно, он может, но если он хочет расширить иерархию, он должен будет следовать этому подходу
Richardtz
IBirdи IHorseследует реализовать IAnimalвместоAnimal
oliholz
@ Йогеш, ты прав. Я упустил из виду место, где он это заявляет. Как «новичку», что мне теперь делать, удалить ответ или оставить его там? Спасибо.
Ричардц
4

Э-э, ваш класс может быть подклассом только для одного другого, но, тем не менее, вы можете реализовать столько интерфейсов, сколько пожелаете.

Пегас на самом деле является лошадью (это особый случай лошади), которая способна летать (что является «навыком» этой особой лошади). С другой стороны, вы можете сказать, что Пегас - это птица, которая может ходить, и она легкая - все зависит от того, как вам будет легче писать код.

Как и в вашем случае вы можете сказать:

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

Интерфейсы не имитируют множественное наследование. Создатели Java считали множественное наследование неправильным, поэтому в Java такого нет.

Если вы хотите объединить функциональность двух классов в один - используйте композицию объектов. Т.е.

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

А если вы хотите предоставить определенные методы, определите их и позвольте им делегировать вызов соответствующему контроллеру.

Здесь интерфейсы могут пригодиться - если Component1реализует интерфейс Interface1и Component2реализует Interface2, вы можете определить

class Main implements Interface1, Interface2

Так что вы можете использовать объекты взаимозаменяемо, где это позволяет контекст.

Так что, на мой взгляд, вы не можете попасть в проблему алмазов.

Картик Сурианараянан
источник
Это не так, как и прямые указатели памяти, неподписанные типы и перегрузка операторов; это просто не нужно, чтобы сделать работу. Java был разработан как простой язык, который легко освоить. Не придумывайте и надейтесь на лучшее, это область знаний, а не догадки.
Джимби
3

Как вы уже знаете, множественное наследование классов в Java невозможно, но возможно с интерфейсами. Вы также можете рассмотреть возможность использования шаблона проектирования композиции.

Я написал очень полную статью о композиции несколько лет назад ...

/codereview/14542/multiple-inheritance-and-composition-with-java-and-c-updated

Мэтью Лейтон
источник
3
  1. Определите интерфейсы для определения возможностей. Вы можете определить несколько интерфейсов для нескольких возможностей. Эти возможности могут быть реализованы конкретным животным или птицей .
  2. Использовать наследование для установления отношений между классами путем обмена нестатическими и непубличными данными / методами.
  3. Используйте Decorator_pattern для динамического добавления возможностей. Это позволит вам сократить количество классов и комбинаций наследования.

Посмотрите на пример ниже для лучшего понимания

Когда использовать шаблон декоратора?

Равиндра Бабу
источник
2

Чтобы уменьшить сложность и упростить язык, множественное наследование не поддерживается в Java.

Рассмотрим сценарий, в котором A, B и C - три класса. Класс C наследует классы A и B. Если классы A и B имеют один и тот же метод, и вы вызываете его из дочернего объекта класса, возникнет неоднозначность при вызове метода класса A или B.

Поскольку ошибки времени компиляции лучше, чем ошибки времени выполнения, java отображает ошибку времени компиляции, если вы унаследовали 2 класса. Так что, будь у вас тот же метод или другой, теперь будет ошибка времени компиляции.

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?  
    }
} 
Syeful Ислам
источник
2

Для решения проблемы множественного наследования в Java → используется интерфейс

J2EE (ядро JAVA) Примечания г-на KVR Стр. 51

День - 27

  1. Интерфейсы в основном используются для разработки пользовательских типов данных.
  2. Что касается интерфейсов, мы можем реализовать концепцию множественного наследования.
  3. С помощью интерфейсов мы можем реализовать концепцию полиморфизма, динамического связывания и, следовательно, мы можем улучшить производительность JAVA-программы за счет увеличения объема памяти и времени выполнения.

Интерфейс - это конструкция, которая содержит набор чисто неопределенных методов, или интерфейс - это набор чисто абстрактных методов.

[...]

День - 28:

Синтаксис-1 для повторного использования функций интерфейса (ов) для класса:

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

В приведенном выше синтаксисе clsname представляет имя класса, который наследует функции от числа интерфейсов 'n'. «Implements» - это ключевое слово, которое используется для наследования функций интерфейса (ов) для производного класса.

[...]

Синтаксис-2, наследующий n-ное количество интерфейсов другому интерфейсу:

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

[...]

Синтаксис-3:

[abstract] class <derived class name> extends <base class name> implements <intf 1>,<intf 2>.........<intf n>
{
  variable declaration;
  method definition or declaration;
};
Потрясающие
источник