171

为了完全理解如何解决 Java 的多重继承问题,我有一个经典问题需要澄清。

可以说我有一个类,Animal它有子类BirdHorse我需要创建一个类Pegasus,它既是鸟又是BirdHorsePegasus

我认为这是经典的钻石问题。据我所知,解决这个问题的经典方法是制作Animal,BirdHorseclasses 接口并Pegasus从中实现。

我想知道是否有另一种方法可以解决我仍然可以为鸟和马创建对象的问题。如果有一种方法可以创造动物,那也很好,但不是必需的。

4

17 回答 17

118

您可以为动物类(生物学意义上的类)创建接口,例如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 {}

更新

我想再补充一个细节。正如布赖恩所说,这是 OP 已经知道的。

但是,我想强调的是,我建议绕过接口的“多继承”问题,并且我不建议使用表示已经是具体类型(例如 Bird)但更多是行为(其他人指鸭打字,这也很好,但我的意思是:鸟类的生物学类别,Avialae)。我也不建议使用以大写“I”开头的接口名称,例如IBird,它不能说明为什么需要接口。这就是问题的不同之处:使用接口构造继承层次结构,在有用时使用抽象类,在需要时实现具体类,并在适当时使用委托。

于 2014-02-17T08:52:09.410 回答
88

将对象组合在一起有两种基本方法:

  • 第一个是继承。正如您已经确定了继承的局限性,这意味着您不能在这里做您需要的事情。
  • 第二个是组合。由于继承失败,您需要使用组合。

它的工作方式是你有一个 Animal 对象。然后在该对象中添加更多对象,以提供所需的属性和行为。

例如:

  • Bird扩展Animal工具IFlier
  • Horse扩展了Animal工具IHerbivore、IQuadruped
  • Pegasus扩展了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 的集合,并且可以使用多态性的所有其他优点等。

但是,您还可以从 Composition 中获得所有灵活性。您可以根据需要对每种类型应用尽可能多的不同接口和复合支持类Animal- 对每个位的设置方式进行尽可能多的控制。

策略模式替代组合方法

取决于您正在做什么和如何做的另一种方法是让Animal基类包含一个内部集合以保留不同行为的列表。在这种情况下,您最终会使用更接近策略模式的东西。这确实在简化代码方面具有优势(例如Horse,不需要了解有关Quadrupedor的任何信息Herbivore),但如果您不使用接口方法,您将失去多态性等的许多优势。

于 2014-02-17T09:35:19.423 回答
45

我有一个愚蠢的想法:

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();
  }
}
于 2014-02-17T09:09:51.470 回答
26

我可以建议Duck-typing的概念吗?

很可能您倾向于让 Pegasus 扩展 Bird 和 Horse 接口,但鸭子类型实际上建议您应该继承行为。正如评论中已经说过的,飞马不是鸟,但它可以飞。所以你的 Pegasus 应该继承一个Flyable-interface 并说一个Gallopable-interface。

策略模式中使用了这种概念。给定的示例实际上向您展示了鸭子如何继承FlyBehaviourandQuackBehaviour并且仍然可以有鸭子,例如RubberDuck不会飞的鸭子。他们也可以将DuckextendBird设为-class,但随后他们将放弃一些灵活性,因为每个Duck人都可以飞行,即使是穷人RubberDuck

于 2014-02-17T09:17:07.893 回答
19

从技术上讲,您一次只能扩展一个类并实现多个接口,但是在接触软件工程时,我宁愿提出一个通常无法回答的特定问题的解决方案。顺便说一句,这是一个很好的面向对象实践,扩展具体类/只扩展抽象类以防止不需要的继承行为——没有“动物”之类的东西,也没有使用动物对象,只有具体的动物。

于 2014-02-17T08:55:32.953 回答
14

在 Java 8 及更高版本中,您可以使用默认方法来实现一种类似 C++ 的多重继承。您还可以查看本教程,其中显示了一些示例,这些示例应该比官方文档更容易开始使用。

于 2014-02-17T10:03:04.240 回答
12

把马放在半扇门的马厩里是安全的,因为马不能越过半扇门。因此,我设置了一个马房服务,可以接受任何类型的马匹,并将其放在带半扇门的马厩中。

那么像马一样可以飞的动物呢?

我曾经对多重继承思考过很多,但是现在我已经编程超过 15 年了,我不再关心实现多重继承。

通常情况下,当我试图处理一个指向多重继承的设计时,我后来发布了我错过了理解问题域的版本。

或者

如果它看起来像鸭子,叫起来像鸭子,但它需要电池,那么你可能有错误的抽象

于 2014-02-18T12:33:27.200 回答
8

Java 没有多重继承问题,因为它没有多重继承。这是设计使然,以解决真正的多重继承问题(钻石问题)。

有不同的策略来缓解这个问题。最直接可实现的一个是 Pavel 建议的 Composite 对象(本质上是 C++ 处理它的方式)。我不知道通过 C3 线性化(或类似方法)的多重继承是否适用于 Java 的未来,但我对此表示怀疑。

如果您的问题是学术性的,那么正确的解决方案是鸟和马更具体,并且假设飞马只是鸟和马的组合是错误的。说飞马与鸟和马有某些共同的内在特性会更正确(也就是说,它们可能有共同的祖先)。正如莫里茨的回答所指出的那样,这可以充分建模。

于 2014-02-17T09:35:42.143 回答
7

我认为这在很大程度上取决于您的需求,以及如何在代码中使用您的动物类。

如果您希望能够在 Pegasus 类中使用 Horse 和 Bird 实现的方法和特性,那么您可以将 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;
        }
    }
}

另一种可能性是使用实体-组件-系统方法而不是继承来定义您的动物。当然,这意味着您不会拥有动物的单独 Java 类,而是它们仅由它们的组件定义。

实体-组件-系统方法的一些伪代码可能如下所示:

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);
}
于 2014-02-17T09:22:53.207 回答
4

嗯,您的类只能是其他 1 个的子类,但是,您仍然可以实现任意数量的接口,只要您愿意。

飞马实际上是一匹马(它是马的特例),它能够飞行(这是这种特殊马的“技能”)。另一方面,你可以说飞马是一只鸟,它可以走路,而且是四足的——这一切都取决于你如何更容易编写代码。

就像你的情况一样,你可以说:

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
   }
}
于 2014-02-17T08:59:43.550 回答
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 {
}
于 2014-02-17T09:00:02.747 回答
3

接口不模拟多重继承。Java 的创建者认为多重继承是错误的,所以 Java 中没有这样的事情。

如果您想将两个类的功能合二为一 - 使用对象组合。IE

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

如果您想公开某些方法,请定义它们并让它们将调用委托给相应的控制器。

这里接口可能会派上用场-如果Component1实现接口Interface1Component2实现Interface2,您可以定义

class Main implements Interface1, Interface2

这样您就可以在上下文允许的情况下互换使用对象。

所以在我看来,你不能陷入钻石问题。

于 2014-02-17T08:59:07.063 回答
3

正如您已经知道的那样,Java 中类的多重继承是不可能的,但使用接口却是可能的。您可能还想考虑使用组合设计模式。

几年前我写了一篇关于作文的非常全面的文章......

https://codereview.stackexchange.com/questions/14542/multiple-inheritance-and-composition-with-java-and-c-updated

于 2014-02-17T13:04:11.650 回答
3
  1. 定义用于定义功能的接口。您可以为多种功能定义多个接口。这些功能可以通过特定的AnimalBird来实现。
  2. 使用继承通过共享非静态和非公共数据/方法来建立类之间的关系。
  3. 使用Decorator_pattern动态添加功能。这将允许您减少继承类和组合的数量。

请看下面的示例以更好地理解

何时使用装饰器模式?

于 2016-12-15T09:11:31.187 回答
2

解决Java中的多继承问题→使用接口

J2EE(核心 JAVA)KVR 先生的笔记 第 51 页

天 - 27

  1. 接口主要用于开发用户定义的数据类型。
  2. 关于接口,我们可以实现多重继承的概念。
  3. 通过接口,我们可以实现多态性、动态绑定的概念,因此我们可以在内存空间和执行时间方面提高 JAVA 程序的性能。

接口是包含纯粹未定义方法的集合的构造,或者接口是纯粹抽象方法的集合。

[...]

第 28 天:

Syntax-1 用于重用接口的特性到类:

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

在上述语法中,clsname 表示从“n”个接口继承特性的类的名称。“实现”是一个关键字,用于将接口的特性继承到派生类。

[...]

语法 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;
};
于 2014-02-19T17:49:14.080 回答
2

为了降低复杂度和简化语言,java 中不支持多重继承。

考虑一个场景,其中 A、B 和 C 是三个类。C 类继承 A 和 B 类。如果 A 类和 B 类具有相同的方法,并且您从子类对象调用它,那么调用 A 类或 B 类的方法就会有歧义。

由于编译时错误优于运行时错误,因此如果您继承 2 个类,java 会呈现编译时错误。因此,无论您使用相同或不同的方法,现在都会出现编译时错误。

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?  
    }
} 
于 2015-02-24T14:33:10.753 回答
-1

问题没有解决。为了充分模拟这一点并防止代码复制,您需要多重继承或混合。具有默认函数的接口是不够的,因为您不能在接口中保存成员。接口建模会导致子类或静态代码中的代码复制,这都是邪恶的。

您所能做的就是使用自定义构造并将其拆分为更多组件并将它们组合在一起......

玩具语言

于 2020-09-21T09:17:59.367 回答