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

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

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

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

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

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

好的,现在说如果Java支持多重inheritance,并且如果有一个类是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 ? } } 

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

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

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

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

我是否可以理解为什么“Java不支持多重inheritance”只有3个类,如上所述,或者我必须要有4个类(Diamond结构)来理解这个问题。

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

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

但是,如果AB有一个共同的祖先,它们都是inheritance的状态,那么你就会遇到严重的麻烦。 你是否存储了这个共同祖先的两个独立副本? 这更像是构图而不是inheritance。 或者你只存储AB共享的A ,当他们操纵他们inheritance的共享状态时会导致奇怪的交互?

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

注意如果A类不存在并且BC声明了它们自己的foo字段,上面的问题将不会是如此大的问题。 仍然存在冲突名称的问题,但可以通过一些命名空间构造解决( B.this.fooC.this.foo也许,就像我们对内部类一样?)。 另一方面,真正的钻石问题不仅仅是命名冲突,而是当两个不相关的DBC )超级类共享它们都从Ainheritance的相同状态时,如何维护类不变量。 这就是为什么需要所有四个类来展示问题的全部范围的原因。

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

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

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

C ++使用虚拟inheritance解决了菱形类问题:

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

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

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

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

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

Java只是跳过了多重inheritance。 (我个人认为在尝试合理化这个决定时很难不对其设计师进行侮辱。当然,当你认为的语言不支持它时,很难想到一件事。)

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

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

通过添加定义add的第四个类,很明显AB都实现了相同的 add方法。

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

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

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

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

  • 深度优先搜索 ,可能会创建线性化D, B, A, C 。 但是这样,在C::foo()之前考虑C::foo()尽管C::foo() 会覆盖 A::foo() ! 这不是我们想要的。 使用DFS的语言示例是Perl。

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

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

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

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

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

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

你引用的钻石问题是不支持多重inheritance的一个原因(也是一个非常好的原因)。 还有其他原因,你可以在这里阅读一下 。

很多原因归结为复杂性(做正确的事情非常复杂),使用它的相对罕见的合法需求,以及它所创造的调度(除了钻石问题之外)的各种其他问题。