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

m0rkklqb  于 2023-01-11  发布在  Java
关注(0)|答案(7)|浏览(152)

我不是在问-〉Why is there no multiple inheritance in Java, but implementing multiple interfaces is allowed?
在Java中,多重继承是不允许的,但是在Java8之后,接口可以有 * 默认方法 *(可以实现方法本身),就像抽象类一样。在这个上下文中,多重继承也应该是允许的。

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

    // default method 
    default void show() 
    { 
      System.out.println("Default Method Executed"); 
    } 
}
dfty9e19

dfty9e191#

事情没那么简单。
如果一个类实现了多个接口,这些接口定义了具有相同签名的默认方法,则编译器将强制您重写该类的此方法。
例如,使用以下两个接口:

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

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

它不会编译:

public class FooBar implements Foo, Bar{
}

您应该定义/重写该方法以消除歧义。
例如,您可以委托Bar实现,如:

public class FooBar implements Foo, Bar{    
    @Override
    public void doThat() {
        Bar.super.doThat();
    }    
}

或委托给Foo实现,例如::

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        Foo.super.doThat();
    }
}

或者仍然定义另一行为:

public class FooBar implements Foo, Bar {
    @Override
    public void doThat() {
        // ... 
    }
}

这个约束说明Java不允许多重继承,即使是接口的默认方法。
我认为,我们不能对多重继承适用同样的逻辑,因为可能会出现多重问题,主要有:

  • 如果继承类内部依赖于一个方法,那么覆盖/删除继承类中该方法的二义性可能会引入副作用,并改变继承类的整体行为。使用默认接口时,这种风险也存在,但它应该不太罕见,因为默认方法不是设计来引入复杂的处理(如类内部的多个内部调用)或有状态的(实际上接口不能承载示例字段)。
  • 如何继承多个字段?即使语言允许,你也会遇到和前面引用的完全相同的问题:继承类行为的副作用:在您想要子类化的AB类中定义的int foo字段不具有相同的含义和意图。
cedebl8k

cedebl8k2#

语言设计者已经考虑过了,所以编译器会强制执行这些操作,如果你定义:

interface First {
    default void go() {
    }
}

interface Second {
    default void go() {
    }
}

然后为两个接口实现一个类:

static class Impl implements First, Second {

}

你会得到一个编译错误并且您需要覆盖go以避免在其周围产生歧义。
但您可能认为可以通过以下操作来欺骗编译器:

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

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

static class Impl extends Second implements First {
}

你可能会认为First::go已经提供了Second::go的实现,这应该没问题。这是太小心了,因此这也没有编译。
JLS9.4.1.3:类似地,当一个抽象方法和一个具有匹配签名的默认方法被继承时,我们会产生一个错误。在这种情况下,可以给其中一个给予优先级--也许我们会假设默认方法也为抽象方法提供了一个合理的实现。但这是有风险的,因为除了名称和签名的巧合之外,我们没有理由相信默认方法的行为与抽象方法的契约一致--在子接口最初开发时,默认方法甚至可能还不存在.在这种情况下,让用户主动Assert默认实现是合适的(通过重写声明)会更安全。
最后一点我想强调的是,多重继承是不允许的,即使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
    }
}

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

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

static class Spectre implements Bug {
    static void test() {
        printIt(); // this will not compile
    }
}

现在,编译器和JLS也禁止这样做:
JLS 8.4.8:类不从其超接口继承静态方法。

1dkrff03

1dkrff033#

Java不允许字段的多重继承,这在JVM中很难支持,因为你只能引用一个对象的头所在的开始,而不是任意的内存位置。
在Oracle/Openjdk中,对象有一个标头,后面跟着最大超类的字段,然后是下一个最大超类的字段,等等。允许一个类的字段相对于不同子类的对象标头出现在不同的偏移量处,这将是一个重大的变化。最有可能的是,对象引用必须成为对对象标头的引用和对字段的引用,以支持这一点。

oogrdqng

oogrdqng4#

接口中的default方法会带来以下问题:
如果两个实现的接口都定义了一个具有相同方法签名的默认方法,则实现类不知道使用哪个默认方法。
实现类应该显式地指定要使用的默认方法或定义自己的方法。
因此,Java-8中的default方法不便于多重继承。默认方法背后的主要动机是,如果在某个时候我们需要向现有接口添加一个方法,我们可以在不更改现有实现类的情况下添加一个方法。通过这种方式,接口仍然与旧版本兼容。然而,我们应该记住使用默认方法的动机,并且应该保持接口和实现的分离。

yqlxgs2m

yqlxgs2m5#

多重继承的主要问题是排序(用于重写和调用 super)、字段构造函数;接口没有字段或构造函数,因此它们不会导致问题。
如果你看看其他语言,它们通常可以分为两大类:
1.具有多重继承的语言加上一些消除特殊情况歧义的特性:虚继承[C++]、直接调用最大派生类中的所有超构造函数[C++]、超类的线性化[Python]、super [Python]的复杂规则等。
1.具有不同概念的语言,通常称为 * 接口 特性 混合 模块 * 等,它们施加了一些限制,例如:没有构造函数[Java]或没有带参数的构造函数[Scala直到最近]、没有可变字段[Java]、特定的重写规则(例如,mixin优先于基类[Ruby],因此当您需要一堆实用方法时可以包含它们)等。Java已经成为这样的语言。

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

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

e5njpo686#

我认为这主要与“钻石问题”有关。现在如果你用同一个方法实现多个接口,编译器会强迫你重写你想实现的方法,因为它不知道使用哪个。我猜Java的创建者想在接口不能使用默认方法时消除这个问题。现在他们想出了一个主意,能够在接口中实现方法是很好的,因为你仍然可以在流/ lambda表达式中使用它们作为函数接口,并在处理中使用它们的默认方法。你不能在类中这样做,但钻石问题仍然存在。这是我的猜测:)

wgeznvg7

wgeznvg77#

Java确实支持多重继承。如果你对编程语言Java做一个全面的比较,那么你就会知道我是正确的。
Java的顶层类或祖先层次结构中的根类是Object类。该类是所有其他类的超类。因此,我们在Java中声明或在API中预定义的每个类都继承了这个Object类。
此外,Java还允许我们继承另一个类。
因此,我们可以说,我们正在执行联锁,但多重继承。
第二种方式
Java支持接口的多重继承。所以你可以使用任意多的接口实现。但是注意,实现一个接口并不定义IS A关系,因为类的继承是可能的。

相关问题