Java 8中抽象类和接口有什么区别?

68
在Java中,抽象类和接口曾经存在一个微妙但重要的区别:默认实现。抽象类可以有它们,而接口则不行。然而,Java 8为接口引入了默认实现,这意味着这不再是接口和抽象类之间的关键区别。
那么,现在的区别是什么呢?
据我所知,唯一剩下的区别(除了一些底层效率问题)是抽象类遵循传统的Java单继承,而接口可以具有多重继承(或多重实现)。这又引出了另一个问题 -
新的Java 8接口如何避免钻石问题

12
你读过钻石问题是什么?吗? - Reimeus
5
在你提供的链接中,它指出:“Java 8 在接口上引入了默认方法。如果 A、B、C 是接口,B 和 C 可以为 A 的抽象方法提供不同的实现,从而导致钻石问题。类 D 要么重新实现该方法(方法体只需将调用转发到其中一个超级实现),要么歧义性将被拒绝为编译错误。” - Sirko
@Reimeus 我没有,谢谢! - David says Reinstate Monica
5个回答

70

接口不能有与之关联的状态。

抽象类可以有与之关联的状态。

此外,接口中的默认方法不需要被实现。这样,它就不会破坏已经存在的代码,因为尽管接口接收到更新,但实现类并不需要实现它。
因此,你可能会得到次优的代码,但如果你想要更优化的代码,那么你的任务就是重写默认实现。

最后,在出现“钻石问题”时,编译器将会警告你,需要选择你想要实现的接口。

为了更好地展示“钻石问题”,请考虑以下代码:

interface A {
    void method();
}

interface B extends A {
    @Override
    default void method() {
        System.out.println("B");
    }
}

interface C extends A { 
    @Override
    default void method() {
        System.out.println("C");
    }
}

interface D extends B, C {

}

interface D extends B, C上我遇到了编译器错误,错误信息如下:

interface D inherits unrelated defaults for method() form types B and C

解决方法:

interface D extends B, C {
    @Override
    default void method() {
        B.super.method();
    }
}

如果我想要继承B中的method()方法,也是同样的道理。
如果D是一个class,情况也是一样的。

为了更好地展示Java 8中接口和抽象类之间的区别,考虑以下Team案例:

interface Player {

}

interface Team {
    void addPlayer(Player player);
}

从理论上讲,您可以提供addPlayer的默认实现,以便将玩家添加到例如玩家列表中。
但是等等...?
我该如何存储玩家列表呢?
答案是,您不能在接口中这样做,即使您有可用的默认实现。


6
覆盖方法的例子并不仅限于钻石问题。如果删除接口 _A_,则不会发生任何变化。 - nosid

19

一些非常详细的答案已经给出,但它们似乎缺少我至少认为是拥有抽象类的极少数正当理由之一的一个要点:

抽象类可以拥有受保护成员(以及默认可见性的成员)。接口中的方法隐式地是公共的


哦,是的,也没错。然而自Java 7以来这并没有改变。 - David says Reinstate Monica
2
@Dgrin91 是的。但是现在,在Java 8中,“默认”方法可以在许多可能使用抽象类(并且该方法正好为非抽象)的地方使用。以前,拥有抽象类的一个重要理由是要实现常见的可重用方法。这现在可以在许多(而不是所有)情况下使用“默认”方法来解决。因此,我认为* 可见性 *方面变得更加重要。 - Marco13

9

钻石问题的定义比较模糊。在多重继承时,可能会出现各种问题。幸运的是,大部分问题都可以在编译时轻松检测到,并且编程语言支持简单的解决方案来避免这些问题。这些问题大多数甚至与钻石问题无关。例如,方法定义的冲突也可能在没有钻石的情况下出现:

interface Bar {
    default int test() { return 42; }
}
interface Baz {
    default int test() { return 6 * 9; }
}
class Foo implements Bar, Baz { }

问题在于钻石继承中的包容和排他问题。如果你有一个类型层次结构,其中B和C派生自A,D派生自B和C,那么问题是:
  • D是A类型中的B和C吗?还是
  • D是A类型中的B或C吗?
在Java 8中,类型A必须是接口。因此没有区别,因为接口没有状态。它们可以定义默认方法,但是它们也没有状态。它们可以调用具有直接访问状态的方法。然而,这些方法总是基于单一继承来实现。

@LeonardBrünings,另一个答案解决了我的疑问。谢谢。 - Pradeep Simha
9
没有成员变量并不解决钻石问题 - 即使没有内部状态,仍然可能存在两个相互冲突的方法实现。要解决钻石问题,只需简单地不允许一个类实现具有冲突默认实现的两个接口即可。 - Philipp
1
“doubt”有时候可以表示“question”吗? - Peter Mortensen
@Philipp:你也可以在没有“菱形继承问题”的情况下拥有两个冲突的方法。OP特别询问了“菱形继承问题”。而关于“菱形继承问题”的重要之处在于状态。 - nosid
@PradeepSimha,你能否提供澄清此问题的答案链接?我仍然看到引入默认方法可能会破坏现有代码的方式。 - James_D

5

现在接口可以包含可执行代码,很多抽象类的用例都被接口取代了。但是抽象类仍然可以拥有成员变量,而接口则不行。

钻石问题可以通过简单地不允许一个类实现两个接口来避免,当这两个接口提供相同签名的默认实现时。


4
Java 8引入了接口的默认实现,这意味着这不再是接口和抽象类之间的关键区别。但是还有一些更为重要的区别。请参考此文章:Interface with default methods vs Abstract class in Java 8
新的Java 8接口如何避免钻石问题? 情况1:您正在实现两个具有相同default方法的接口,您必须在您的实现类中解决冲突。
interface interfaceA{
    default public void foo(){
        System.out.println("InterfaceA foo");
    }
}
interface interfaceB{
    default public void foo(){
        System.out.println("InterfaceB foo");
    }
}
public class DiamondExample implements interfaceA,interfaceB{
    public void foo(){
        interfaceA.super.foo();
    }
    public static void main(String args[]){
        new DiamondExample().foo();
    }
} 

上面的例子产生以下输出:
InterfaceA foo

案例2:您正在扩展一个基类并实现一个具有默认方法的接口。编译器会为您解决钻石问题,您无需像第一个示例那样进行解决。
interface interfaceA{
    default public void foo(){
        System.out.println("InterfaceA foo");
    }
}

class DiamondBase {
    public void foo(){
        System.out.println("Diamond base foo");
    }
}

public class DiamondExample extends DiamondBase implements interfaceA{

    public static void main(String args[]){
        new DiamondExample().foo();
    }
}

上面的例子产生下面的输出:
Diamond base foo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接