为什么Java不允许多重继承,但允许使用默认实现符合多个接口

Asa*_*nka 47 java inheritance interface abstract java-8

我不问这个 - > 为什么Java中没有多重继承,但允许实现多个接口?

在Java中,不允许多重继承,但是,在Java 8之后,Interfaces可以拥有默认方法(可以自己实现方法),就像抽象类一样.在此上下文中,还应允许多重继承.

interface TestInterface 
{ 
    // abstract method 
    public void square(int a); 

    // default method 
    default void show() 
    { 
      System.out.println("Default Method Executed"); 
    } 
} 
Run Code Online (Sandbox Code Playgroud)

dav*_*xxx 36

事情并非如此简单.
如果一个类实现了多个定义具有相同签名的默认方法的接口,则编译器将强制您为该类重写此方法.

例如,使用这两个接口:

public interface Foo {
    default void doThat() {
        // ...
    }
}

public interface Bar {    
    default void doThat() {
        // ...
    }       
}
Run Code Online (Sandbox Code Playgroud)

它不会编译:

public class FooBar implements Foo, Bar{
}
Run Code Online (Sandbox Code Playgroud)

您应该定义/覆盖方法以消除歧义.
例如,您可以委托给Bar实现,例如:

public class FooBar implements Foo, Bar{    
    @Override
    public void doThat() {
        Bar.super.doThat();
    }    
}
Run Code Online (Sandbox Code Playgroud)

或委托Foo执行如下:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        Foo.super.doThat();
    }
}
Run Code Online (Sandbox Code Playgroud)

或者仍然定义另一种行为:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        // ... 
    }
}
Run Code Online (Sandbox Code Playgroud)

该约束表明,即使对于接口默认方法,Java也不允许多重继承.


我认为我们不能对多重继承使用相同的逻辑,因为可能会出现多个问题,主要是:

  • 如果类在内部依赖于此方法,则重写继承类的方法可能会引入副作用并更改继承类的整体行为.
  • 如何继承多个字段?即使语言允许,你也会遇到与之前引用的完全相同的问题:继承类行为的副作用:int foo在a ABclass中定义的字段你想要子类不具有相同的含义和意图.

  • @UbuntuCore可以消除歧义这些事情,例如通过发出错误并强制程序员明确选择其中一个,或者(更糟糕的是)定义一个隐含的规则来挑选,但它会导致棘手的问题.经典钻石表壳.很容易混淆并使用这样的结构做出错误的选择.语言设计的一个目的是使错误的代码看起来错误,并且多重继承(如果不是非常精心设计)可以很容易地编写错误的正确代码. (3认同)

Eug*_*ene 25

语言设计者已经考虑过这一点,所以这些东西都是由编译器强制执行的.所以如果你定义:

interface First {
    default void go() {
    }
}

interface Second {
    default void go() {
    }
}
Run Code Online (Sandbox Code Playgroud)

并为两个接口实现一个类:

static class Impl implements First, Second {

}
Run Code Online (Sandbox Code Playgroud)

你会得到一个编译错误; 并且您需要覆盖go以不在其周围创建歧义.

但你可能会认为你可以通过这样做来欺骗编译器:

interface First {
    public default void go() {
    }
}

static abstract class Second {
    abstract void go();
}

static class Impl extends Second implements First {
}
Run Code Online (Sandbox Code Playgroud)

你可以认为First::go已经提供了一个实现Second::go,它应该没问题.这太过于谨慎,因此也无法编译.

JLS 9.4.1.3:类似地,当继承具有匹配签名的抽象和默认方法时,我们会产生错误.在这种情况下,可以优先考虑一个或另一个 - 也许我们假设默认方法也为抽象方法提供了合理的实现.但这是有风险的,因为除了巧合的名称和签名之外,我们没有理由相信默认方法与抽象方法的契约一致 - 默认方法在最初开发子接口时可能不存在.在这种情况下,要求用户主动声明默认实现是合适的(通过覆盖声明)更安全.

我要引入的最后一点是,即使在java中添加了新的东西,也不允许多重继承,这是接口的静态方法不会被继承.静态方法默认是继承的:

static class Bug {
    static void printIt() {
        System.out.println("Bug...");
    }
}

static class Spectre extends Bug {
    static void test() {
        printIt(); // this will work just fine
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,如果我们为接口更改它(并且您可以实现多个接口,不像类):

interface Bug {
    static void printIt() {
        System.out.println("Bug...");
    }
}

static class Spectre implements Bug {
    static void test() {
        printIt(); // this will not compile
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,编译器JLS也禁止这样做:

JLS 8.4.8:类不从其超接口继承静态方法.


Pet*_*rey 13

Java不允许对字段进行多重继承.这在JVM中很难支持,因为您只能引用标题所在的对象的开头,而不是任意的内存位置.

在Oracle/Openjdk中,对象有一个标题,后跟最超类的字段,然后是下一个超级类的字段等.允许类的字段出现在相对于标题的不同偏移处是一个重大变化不同子类的对象.最有可能的对象引用必须成为对象头的引用和对字段的引用以支持它.


S.K*_*.K. 5

default 接口中的方法带来了一个问题:

如果两个实现的接口都定义了一个具有相同方法签名的默认方法,那么实现类不知道使用哪个默认方法。

实现类应该明确指定要使用的默认方法或定义它自己的方法。

因此default,Java-8 中的方法不利于多重继承。默认方法背后的主要动机是,如果在某个时候我们需要向现有接口添加一个方法,我们可以在不更改现有实现类的情况下添加一个方法。这样,接口仍然兼容旧版本。但是,我们应该记住使用默认方法的动机,并应该保持接口和实现的分离。


mar*_*cus 5

多重继承的主要问题是排序(用于覆盖和调用super)、字段构造函数;接口没有字段或构造函数,因此不会引起问题。

如果你看看其他语言,它们通常分为两大类:

  1. 具有多重继承的语言以及一些消除特殊情况歧义的功能:虚拟继承 [C++]、直接调用最底层派生类中的所有超级构造函数 [C++]、超类的线性化 [Python]、超级的复杂规则[ Python] 等。

  2. 具有不同概念的语言,通常称为接口特征混合模块等,它们施加一些限制,例如:没有构造函数 [Java] 或没有带参数的构造函数 [Scala 直到最近]、没有可变字段 [Java]、特定的重写规则(例如 mixin 优先于基类 [Ruby],因此当您需要一堆实用方法时可以包含它们)等。Java 已经成为这样的语言。

为什么仅仅通过禁止字段和构造函数就可以解决与多重继承相关的许多问题?

  • 重复的基类中不能有重复的字段。
    • 主要类层次结构仍然是线性的。
  • 您不能以错误的方式构建基础对象。
    • 想象一下,如果对象有公共/受保护的字段,并且所有子类都有设置这些字段的构造函数。当您从多个类继承(所有类都派生自 Object)时,哪一个类可以设置字段?最后一堂课?他们成为等级制度中的兄弟姐妹,因此彼此一无所知。您是否应该拥有对象的多个副本来避免这种情况?所有类都能正确互操作吗?
  • 请记住,Java 中的字段不是虚拟的(可重写的),它们只是数据存储。
    • 您可以创建一种语言,其中字段的行为类似于方法并且可以被覆盖(实际存储始终是私有的),但这将是一个更大的变化,并且可能不再被称为 Java。
  • 接口不能自行实例化。
    • 您应该始终将它们与具体的类结合起来。这消除了对构造函数的需要,并使程序员的意图也更加清晰(即,什么是具体类,什么是附件接口/混合)。这也提供了一个定义明确的地方来解决所有歧义:具体类。