当我们已经有了抽象类时,为什么要将默认方法和静态方法添加到Java 8的接口中?


99

在Java 8中,接口可以包含已实现的方法,静态方法和所谓的“默认”方法(实现类不需要重写)。

以我的观点(可能是幼稚的),不需要违反这样的接口。接口一直是您必须履行的合同,这是一个非常简单而纯粹的概念。现在,它是几件事情的混合体。在我看来:

  1. 静态方法不属于接口。它们属于实用程序类。
  2. 根本不应该在接口中允许使用“默认”方法。为此,您可以始终使用抽象类。

简而言之:

在Java 8之前:

  • 您可以使用抽象和常规类来提供静态和默认方法。接口的作用很明显。
  • 接口中的所有方法都应通过实现类来覆盖。
  • 您不能在不修改所有实现的情况下在接口中添加新方法,但这实际上是一件好事。

在Java 8之后:

  • 接口和抽象类之间几乎没有区别(除了多重继承)。实际上,您可以使用接口模拟常规类。
  • 在对实现进行编程时,程序员可能会忘记覆盖默认方法。
  • 如果类试图实现两个或多个具有默认方法且具有相同签名的接口,则会出现编译错误。
  • 通过向接口添加默认方法,每个实现类都会自动继承此行为。这些类中的某些类可能并未在设计时考虑到该新功能,因此可能会引起问题。例如,如果有人将新的默认方法添加default void foo()到interface Ix,则不会编译Cx实现Ix并具有具有foo相同签名的私有方法的类。

进行此类重大更改的主要原因是什么?它们会带来哪些新的好处(如果有)?


30
额外的问题:为什么他们不引入类的多重继承呢?

2
静态方法不属于接口。它们属于实用程序类。不,他们属于这个@Deprecated类别!由于无知和懒惰,静态方法是Java中使用最广泛的构造之一。许多静态方法通常意味着程序员能力不足,将耦合提高了几个数量级,并且当您意识到它们为什么是个坏主意时,对于单元测试和重构是一场噩梦!

11
@JarrodRoberson您能否提供更多提示(链接会很棒),有关“当您意识到为什么这是个坏主意时,它是单元测试和重构的噩梦!”?我从没想过,我想了解更多。
水汪汪的

11
@Chris状态的多重继承会引起很多问题,尤其是在内存分配方面(经典的菱形问题)。但是,行为的多重继承仅取决于实现是否满足接口已经设置的约定(接口可以调用它声明和要求的其他方法)。一个细微但非常有趣的区别。
ssube 2014年

1
您想将方法添加到现有接口,这很麻烦,或者在java8之前几乎不可能,现在您可以将其添加为默认方法。
VdeX

Answers:


59

Java标准库中提供了一个默认方法的激励示例,您现在可以在其中找到它

list.sort(ordering);

代替

Collections.sort(list, ordering);

如果没有多个相同的实现,我认为他们不可能做到这一点List.sort


19
C#通过扩展方法克服了这个问题。
罗伯特·哈维2014年

5
并且它允许链表使用O(1)额外空间和O(n log n)时间mergesort,因为链表可以在适当的位置合并,在Java 7中,它转储到外部数组,然后进行排序
棘轮怪胎

5
我在Goetz解释问题的地方找到了这篇论文。因此,我现在将此答案标记为解决方案。
史密斯先生2014年

1
@RobertHarvey:创建2亿个List <Byte>,使用IEnumerable<Byte>.Append它们加入它们,然后调用Count,然后告诉我扩展方法如何解决该问题。如果CountIsKnownCount是的成员IEnumerable<T>,则从的返回值Append可以宣传CountIsKnown组成集合是否这样做,但是如果没有这种方法,则不可能。
2014年

6
@supercat:我一点也不知道你在说什么。
罗伯特·哈维

48

实际上,可以在Java文档中找到正确的答案,其中指出:

[d] efault方法使您可以向库的接口添加新功能,并确保与为这些接口的较早版本编写的代码二进制兼容。

这一直是Java长期以来的痛苦之源,因为一旦接口公开,它们往往就不可能发展。(文档中的内容与您在注释中链接到的文章有关:通过虚拟扩展方法进行接口演变。)此外,只有通过扩展扩展现有的集合接口并提供默认实现。破坏二进制兼容性或引入新的API意味着Java 8的最重要功能将被普遍使用还需要几年的时间。

该文档再次揭示了在接口中允许使用静态方法的原因:[t]他使您可以更轻松地在库中组织帮助程序方法;您可以将特定于接口的静态方法保留在同一接口中,而不是在单独的类中。换句话说,像这样的静态实用程序类java.util.Collections现在(最终)可以被视为一般的反模式(当然并不总是)。我的猜测是,一旦实现了虚拟扩展方法,为这种行为增加支持是微不足道的,否则可能就不会完成。

类似地,这些新功能如何受益的一个例子是考虑一个最近让我烦恼的课程java.util.UUID。它实际上并没有提供对UUID类型 1、2或5的支持,因此也不能轻易对其进行修改。它也被固定了一个不能被覆盖的随机生成器。为不受支持的UUID类型实现代码需要直接依赖于第三方API而不是接口,或者维护转换代码以及随之而来的额外垃圾收集成本。如果使用静态方法,则UUID可以将其定义为接口,从而允许对缺失部分进行真正的第三方实现。(如果UUID最初定义为接口,我们可能会有些笨拙UuidUtil 大量的Java核心API由于无法基于接口而退化,但是从Java 8开始,这种不良行为的借口数量已大大减少。

接口和抽象类实际上没有区别是不正确的,因为抽象类可以具有状态(即声明字段),而接口则不能。因此,它不等同于多重继承,甚至不等同于混合类型继承。正确的mixin(例如Groovy 2.3的traits)可以访问状态。(Groovy还支持静态扩展方法。)

在我看来,遵循Doval的例子也不是一个好主意。一个接口应该定义一个合同,但是不应该强制执行该合同。(无论如何都不是Java。)对实现的正确验证是测试套件或其他工具的责任。可以使用批注来定义合同,而OVal是一个很好的例子,但是我不知道它是否支持在接口上定义的约束。即使目前尚不存在这样的系统,也是可行的。(策略包括javac通过注释处理器进行编译时自定义API和运行时字节码生成。)理想情况下,合同应在编译时执行,最坏的情况下使用测试套件执行,但我的理解是,运行时执行不受欢迎。另一个可能有助于Java合同编程的有趣工具是Checker Framework


1
为进一步跟进我的最后一段(即不强制在接口的合同),这是值得指出的是,default方法不能覆盖equalshashCodetoString。关于为什么不允许这样做的非常有用的成本/收益分析,可以在这里找到:mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…–
ngreen

Java仅具有一个虚函数equals和一个hashCode方法是很糟糕的,因为集合可能需要测试两种不同的相等性,而实现多个接口的项目可能会遇到相互矛盾的合同要求。能够使用不会更改的列表作为hashMap键是有帮助的,但是有时将存储存储在hashMap其中的集合基于等价而不是当前状态进行匹配会很有帮助[等价意味着匹配状态和不变性 ] 。
2014年

好吧,Java的Comparator和Comparable接口都有解决方法。但是我认为这些有点丑陋。
2014年

这些接口仅支持某些集合类型,它们会带来自己的问题:比较器本身可能会封装状态(例如,专用字符串比较器可能会忽略每个字符串开头的可配置字符数,在这种情况下,要忽略的字符将成为比较器状态的一部分),而这反过来又成为按其排序的任何集合状态的一部分,但是没有定义的机制来询问两个比较器是否等效。
2014年

哦,是的,我感到比较器的痛苦。我正在研究一个应该很简单的树结构,但是并不是因为很难使比较器正确。我可能要编写一个自定义树类,以使问题消失。
2014年

44

因为您只能继承一个类。如果您有两个接口,其实现足够复杂以至于需要一个抽象基类,那么这两个接口实际上是互斥的。

另一种方法是将这些抽象基类转换为静态方法的集合,并将所有字段转换为参数。这将允许该接口的任何实现者调用静态方法并获得功能,但是用一种已经太冗长的语言来说,这是很多样板。


作为为什么可以在接口中提供实现的有用示例,请考虑以下Stack接口:

public interface Stack<T> {
    boolean isEmpty();

    T pop() throws EmptyException;
 }

无法保证当有人实现该接口时,pop如果堆栈为空,则将引发异常。我们可以通过pop分为两种方法来强制执行此规则:public final强制执行合同的protected abstract方法和执行实际弹出操作的方法。

public abstract class Stack<T> {
    public abstract boolean isEmpty();

    protected abstract T pop_implementation();

    public final T pop() throws EmptyException {
        if (isEmpty()) {
            throw new EmptyException();
        else {
            return pop_implementation();
        }
    }
 }

我们不仅确保所有实现都遵守合同,而且还使他们不必检查堆栈是否为空并引发异常。这是一个巨大的胜利!...除了必须将接口更改为抽象类这一事实。在具有单一继承的语言中,这会大大降低灵活性。它使您可能的接口互斥。能够提供仅依赖于接口方法本身的实现将解决该问题。

我不确定Java 8向接口添加方法的方法是否允许添加最终方法或受保护的抽象方法,但是我知道D语言允许它并提供对Design by Contract的本地支持。由于该技术pop是最终的,因此该技术没有危险,因此没有实现类可以覆盖它。

对于可覆盖方法的默认实现,我假设添加到Java API的任何默认实现都仅依赖于它们添加到的接口的协定,因此,正确实现该接口的任何类也将在默认实现下正常运行。

此外,

接口和抽象类之间几乎没有区别(除了多重继承)。实际上,您可以使用接口模拟常规类。

这不是很正确,因为您不能在接口中声明字段。您在接口中编写的任何方法都不能依赖任何实现细节。


作为支持接口中使用静态方法的示例,请考虑Java API中的实用程序类,例如Collections。该类仅存在是因为这些静态方法无法在其各自的接口中声明。Collections.unmodifiableList可以在List接口中进行声明,也很容易找到。


4
反论点:由于静态方法(如果正确编写的话)是自包含的,因此它们在单独的静态类中更有意义(可以通过类名对其进行收集和分类),而在接口中则较无意义,从本质上讲它们很方便这会导致滥用,例如在对象中保持静态或引起副作用,从而使静态方法不可测试。
罗伯特·哈维

3
@RobertHarvey如果静态方法在类中,那么是什么阻止您做同样愚蠢的事情呢?同样,接口中的方法可能根本不需要任何状态。您可能只是试图执行合同。假设您有一个Stack接口,并且想要确保在pop使用空堆栈调用时会引发异常。给定抽象方法boolean isEmpty()protected T pop_impl(),您可以实现final T pop() { isEmpty()) throw PopException(); else return pop_impl(); }这将对所有实现者实施合同。
2014年

等一下 堆栈上的Push和Pop方法不再static
罗伯特·哈维2014年

@RobertHarvey如果不是因为注释的字符数限制,我会更加清楚,但是我正在考虑接口中的默认实现,而不是静态方法。
2014年

8
我认为默认接口方法是一种hack,它已经被引入以能够扩展标准库,而无需基于它来适应现有代码。
乔治

2

可能的目的是通过取代通过依赖项注入静态信息或功能的需求,从而提供创建混合类的能力。

这个想法似乎与如何在C#中使用扩展方法向接口添加实现的功能有关。


1
扩展方法不会向接口添加功能。扩展方法只是使用方便list.sort(ordering);形式在类上调用静态方法的语法糖。
罗伯特·哈维

如果您查看IEnumerableC#中的接口,则可以看到如何实现对该接口的扩展方法(就像这样LINQ to Objects做)如何为实现的每个类添加功能IEnumerable。这就是我添加功能的意思。
rae1

2
扩展方法很棒。它们给人一种错觉,即您正在将功能附加到类或接口上。只是不要在向类中添加实际方法时感到困惑;类方法可以访问对象的私有成员,而扩展方法则不能(因为它们实际上只是调用静态方法的另一种方式)。
罗伯特·哈维

2
的确如此,这就是为什么我看到在Java接口中具有静态或默认方法的某些关系的原因。实现基于接口可用的内容,而不是类本身。
rae1

1

我在default方法中看到的两个主要目的(某些用例同时满足两个目的):

  1. 语法糖。实用程序类可以达到这个目的,但是实例方法更好。
  2. 现有接口的扩展。该实现是通用的,但有时效率不高。

如果这只是第二个目的,您将不会在全新的界面中看到它Predicate。所有带@FunctionalInterface注释的接口都必须具有一种准确的抽象方法,以便lambda可以实现它。,,等已添加的default方法仅是实用程序,您不应覆盖它们。但是,有时静态方法会更好andornegate

至于现有接口的扩展-即使在那里,一些新方法也只是语法糖。Collectionstream,,- 这样的方法forEachremoveIf基本上,它只是您不需要重写的实用程序。然后有类似的方法spliterator。默认实现不是最优的,但是,至少代码可以编译。如果您的界面已经发布并被广泛使用,则只能诉诸此方法。


至于static方法,我想其他方法可以很好地覆盖它:它允许接口成为其自己的实用程序类。也许我们可以摆脱CollectionsJava的未来?Set.empty()会摇滚。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.