为什么带有共同祖先的钻石案例用于解释Java多重继承问题,而不是两个不相关的父类?

Ros*_*Jha 26 java multiple-inheritance diamond-problem

对于Java人来说,这个问题可能听起来很奇怪,但如果你试图解释这个问题,那就太好了.

在这些日子里,我正在清除Java的一些非常基本的概念.所以我来谈谈Java的继承和接口主题.

在阅读本文时,我发现Java不支持多重继承,并且也明白,我无法理解为什么到处都会出现Diamond图形问题(至少有4个类来创建钻石)来解释这种行为,我们不能仅使用3个类来理解此问题.

说,我有A类和B类,这两个类是不同的(它们不是普通类的子类)但是它们有一个共同的方法,它们看起来像:

class A {
    void add(int a, int b) {

    }
}

class B {
    void add(int a, int b) {

    }
}
Run Code Online (Sandbox Code Playgroud)

好的,现在说Java是否支持多重继承,如果有一个类是A和B的子类,如下所示: -

class C extends A,B{ //If this was possible
    @Override
    void add(int a, int b) { 
        // TODO Auto-generated method stub
        super.add(a, b); //Which version of this, from A or B ?
    }
 }
Run Code Online (Sandbox Code Playgroud)

然后编译器将无法找到从A或B调用哪个方法,这就是Java不支持多重继承的原因.那么这个概念有什么问题吗?

当我读到这个主题时,我能够理解钻石问题,但是我无法理解为什么人们没有给出三个类的例子(如果这是有效的一个,因为我们只使用了3个类来演示问题所以它很容易通过将其与钻石问题进行比较来理解.)

让我知道这个例子是否不适合解释问题,或者这也可以参考理解问题.

编辑: 我在这里得到一个近距离投票,说明问题不明确.这是一个主要问题: -

我能理解为什么"Java不支持多重继承"只有3个类,如上所述,或者我必须要有4个类(Diamond结构)来理解这个问题.

biz*_*lop 46

钻石继承的问题不是共享行为,而是共享状态.如您所见,Java实际上始终支持多重继承,但只支持多种类型的继承.

只有三个类,通过引入一个像super.A或的简单结构,可以相对容易地解决问题super.B.虽然你只关注被覆盖的方法,但无论你是拥有共同的祖先还是只有基本的三个类,这都无关紧要.

然而,如果A并且B有一个共同的祖先,他们都继承的状态,那么你就会遇到严重的麻烦.你是否存储了这个共同祖先的两个独立副本?这更像是构图而不是继承.或者你只存储一个由两者共享的,AB在他们操纵他们继承的共享状态时引起奇怪的交互?

class A {
  protected int foo;
}

class B extends A {
  public B() {
    this.foo = 42;
  }
}

class C extends A {
  public C() {
    this.foo = 0xf00;
  }
}

class D extends B,C {
  public D() {
    System.out.println( "Foo is: "+foo ); //Now what?
  }
}
Run Code Online (Sandbox Code Playgroud)

注意如何上面也不会这么大一个问题,如果类A并不存在,都BC宣布自己的foo领域.仍然会有冲突名称的问题,但可能与一些命名空间结构来解决(B.this.fooC.this.foo可能,因为我们做的内部类?).另一方面,真正的钻石问题不仅仅是命名冲突,而是当两个不相关的D(BC)超类共享它们都继承的相同状态时,如何维护类不变量A.这就是为什么需要所有四个类来展示问题的全部范围的原因.

多继承中的共享行为不会出现同样的问题.以至于最近推出的默认方法就是这样做的.这意味着现在也允许实现多重继承.关于调用哪个实现的解决方案仍然存在一些复杂因素,但由于接口是无状态的,因此避免了最大的错误.

  • @Svante你可以这样说,但你可能会以错误的方式看待它.多重继承(状态)不是目标.这是一个你想要使用或不想使用的工具.他们所说的是"从国家的多重继承中获得的收益很少但却会引发很多问题".另一方面,类型的多重继承是好的,我们想要那样.因此,在您获得多重继承的大部分好处而没有任何带有多重继承状态的包袱的情况下,已经达成了妥协. (8认同)
  • @Svante这并不简单,因为无论你选择哪种顺序,你都打破了"B"或"C"的不变量.或者你复制了'A`的状态以保留不变量,但是打破了Liskov替换原则,因为`D`与`A`不是'是'的关系,而是与'是'的关系.无论如何,关键不在于问题无法解决,当然可以.关键是要强调为什么只有当你有4个班而不是3个班时才会出现这个问题. (2认同)

vz0*_*vz0 12

Java不支持多重继承,因为该语言的设计者以这种方式设计Java.其他语言(如C++)支持多重继承,因此它不是技术问题,只是设计标准.

多重继承的问题在于,并不总是清楚从哪个类调用哪个方法,以及要访问的实例变量.不同的人对它的解释不同,Java设计者当时认为最好完全跳过多重继承.

C++通过虚拟继承解决了菱形类问题:

虚拟继承是面向对象编程中使用的一种技术,其中声明继承层次结构中的特定基类与其他派生类中的同一基础的任何其他包含共享其成员数据实例.例如,如果类A通常(非虚拟地)派生自类X(假设包含数据成员),同样类B,并且类C继承自类A和B,则它将包含两组数据成员与X类相关联(可以独立访问,通常使用合适的消除歧义的限定符).但是,如果类A实际上是从类X派生而来,那么类C的对象将只包含来自类X的一组数据成员.实现此功能的最着名的语言是C++.

与Java相反,在C++中,您可以通过在调用前加上类的名称来消除要调用的实例方法的歧义:

class X {
  public: virtual void f() { 

  } 
};

class Y : public X {
  public: virtual void f() { 

  } 
};

class Z : public Y {
  public: virtual void f() { 
    X::f();
  } 
};
Run Code Online (Sandbox Code Playgroud)


Sva*_*nte 7

这只是您在语言中进行多重继承时必须解决的一个难题.由于存在具有多个内容的语言(例如Common Lisp,C++,Eiffel),它显然不是不可克服的.

Common Lisp定义了对继承图进行优先级排序的确切策略,因此在实际中它很重要的极少数情况下没有歧义.

C++使用虚拟继承(我还没有努力去理解这意味着什么).

Eiffel允许指定您想要继承的方式,可能在子类中重命名方法.

Java刚跳过多重继承.(我个人认为,这是很难不被侮辱它的设计者试图理顺这个决定.当然,这是很难想到的事情时,语言,你觉得不支持它.)


kap*_*pex 6

四个类的钻石问题比问题中描述的三个类问题简单.

三个类的问题增加了另一个必须首先解决的问题:由add具有相同签名的两个不相关方法引起的命名冲突.它实际上并不难解决,但它增加了不必要的复杂性.它可能在Java中被允许(就像它已经允许使用相同的签名实现多个不相关的接口方法),但是可能存在一些语言,它们只是禁止在没有共同祖先的情况下对类似方法进行多重继承.

通过添加第四类定义add,显然这两个AB正在实施的相同 add方法.

因此钻石问题更加清晰,因为它使用简单继承而不是仅使用具有相同签名的方法来显示问题.它是众所周知的,可能适用于更广泛的编程语言,因此它与三个类的变化(这增加了额外的命名冲突问题)没有多大意义,因此没有被采用.

  • "冲突"是两种方法无关,但巧合地共享相同的签名 - 语言_could_处理这种方式有多种方式.使用允许或禁止具有相同签名但没有共同基础的两个方法可以在类中存在的规范很容易解决.但是java中没有这样的规范.如果我们假设它是允许的(授予很可能),那么我认为可以使用3类示例而不是钻石.钻石虽然更普遍,但不需要任何关于如何处理冲突的假设. (2认同)

amo*_*mon 5

如果您找到一个理智的方法解决方案,多重继承是没有问题的.当您在对象上调用方法时,方法解析是选择应该使用哪个类的方法版本的行为.为此,继承图是线性化的.然后,选择提供所请求方法的实现的该序列中的第一个类.

为了说明冲突解决策略的以下示例,我们将使用此钻石继承图:

    +-----+
    |  A  |
    |=====|
    |foo()|
    +-----+
       ^
       |
   +---+---+
   |       |
+-----+ +-----+
|  B  | |  C  |
|=====| |=====|
|foo()| |foo()|
+-----+ +-----+
   ^       ^
   |       |
   +---+---+
       |
    +-----+
    |  D  |
    |=====|
    +-----+
Run Code Online (Sandbox Code Playgroud)
  • 最灵活的策略是要求程序员在创建不明确的类时明确地选择实现,方法是明确地覆盖冲突的方法.这种变体禁止多重继承.如果程序员想要从多个类继承行为,则必须使用组合,并编写许多代理方法.然而,天真明确解决的继承冲突具有与......相同的缺点.

  • 深度优先搜索,可能会创建线性化D, B, A, C.但这种方式,虽然覆盖了A::foo()之前被考虑过!这不是我们想要的.使用DFS的语言示例是Perl.C::foo()C::foo() A::foo()

  • 使用一个聪明的算法来保证if X是子类Y,它将始终在线性Y化之前.这样的算法将无法解开所有继承图,但它在大多数情况下提供了理智的语义:如果一个类重写一个方法,它将始终优先于重写方法.该算法存在并称为C3.这将创建线性化D, B, C, A.C3于1996年首次出现.不幸的是,Java于1995年发布 - 因此在最初设计Java时不知道C3.

  • 使用组合,而不是继承 - 重访.多继承的一些解决方案建议摆脱"类继承"位,而是提出其他组合单元.一个例子是mixins,它将方法定义"复制并粘贴"到您的类中.这非常粗糙.

    mixins的想法已被细化为特征(2002年呈现,对Java而言也为时已晚).特征是类和接口的更一般情况.当您"继承"特征时,定义将嵌入到您的类中,这样就不会使方法解析复杂化.与mixins不同,traits提供了更加细微的策略来解决冲突.特别是,特征组成的顺序很重要.Traits在Perl的"Moose"对象系统(称为角色)和Scala中扮演着重要角色.

Java更喜欢单继承,原因是另一个原因:如果每个类只能有一个超类,我们就不必应用复杂的方法解析算法 - 继承链已经是方法解析顺序.这使方法更有效.

Java 8引入了与traits类似的默认方法.但是,Java方法解析的规则使得使用默认方法的接口的能力远远低于特征.仍然是朝着使现代解决方案适应多重继承问题的方向迈出的一步.

在大多数多继承方法解析方案中,超类的顺序很重要.也就是说,class D extends B, C和之间存在差异class D extends C, B.由于订单可用于简单的消歧,因此三类示例无法充分展示与多重继承相关的问题.你需要一个完整的四级钻石问题,因为它显示了天真的深度优先搜索导致一个不直观的方法解析顺序.