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

在我的(可能是幼稚的)视图中,无需违反此类接口。接口一直是您必须履行的合同,这是一个非常简单而纯粹的概念。现在,它是几件事情的混合体。我认为:


静态方法不属于接口。它们属于实用程序类。
完全不应在接口中使用“默认”方法。为此,您可以始终使用抽象类。

简而言之:

Java 8之前:


您可以使用抽象类和常规类来提供静态和默认方法。接口的作用很明确。
实现类应该覆盖接口中的所有方法。
不修改所有实现就不能在接口中添加新方法,但这实际上是好东西。

Java 8之后:


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

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

评论

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

静态方法不属于接口。它们属于实用程序类。不,它们属于@Deprecated类别!由于无知和懒惰,静态方法是Java中使用最广泛的构造之一。许多静态方法通常意味着程序员能力不强,将耦合提高了几个数量级,并且当您意识到它们为什么是个坏主意时,对于单元测试和重构是一场噩梦!
@JarrodRoberson您能否提供更多提示(链接会很棒),有关“当您意识到为什么这样做是个坏主意时,它是单元测试和重构的噩梦!”?我从没想过,我想了解更多。

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

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

#1 楼

Java标准库中有一个很好的激励默认方法的示例,您现在在其中有了

list.sort(ordering);


而不是

Collections.sort(list, ordering);


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

评论


C#通过扩展方法克服了这个问题。

–罗伯特·哈维(Robert Harvey)
2014年3月20日15:50

并且它允许链表使用O(1)额外空间和O(n log n)时间mergesort,因为链表可以就地合并,在Java 7中它会转储到外部数组,然后对其进行排序

–棘轮怪胎
2014年3月20日16:32

我在Goetz解释问题的地方找到了这篇论文。因此,我现在将此答案标记为解决方案。

–史密斯先生
2014年3月21日在10:01



@RobertHarvey:创建2亿个List ,使用IEnumerable .Append加入它们,然后调用Count,然后告诉我扩展方法如何解决问题。如果CountIsKnown和Count是IEnumerable 的成员,则如果组成集合确实这样做,则从Append返回的内容可以播发CountIsKnown,但是没有这种方法是不可能的。

–超级猫
2014年6月12日22:30在

@supercat:我丝毫不知道你在说什么。

–罗伯特·哈维(Robert Harvey)
2014年6月12日在22:35



#2 楼

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

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

这一直是Java痛苦的根源,因为一旦接口公开,它们往往就不可能发展。 (文档中的内容与您在注释中链接到的文章有关:通过虚拟扩展方法进行接口演化。)此外,只有通过扩展以下功能才能快速采用新功能(例如lambda和新的流API)。现有集合接口并提供默认实现。破坏二进制兼容性或引入新的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的特征)可以访问状态。 (Groovy还支持静态扩展方法。)
在我看来,遵循Doval的榜样也不是一个好主意。一个接口应该定义一个合同,但是不应该强制执行该合同。 (无论如何都不是Java。)正确验证实现是测试套件或其他工具的责任。可以通过注释来定义合同,而OVal是一个很好的例子,但是我不知道它是否支持在接口上定义的约束。即使目前尚不存在这样的系统,也是可行的。 (策略包括通过注释处理器API编译javac的编译时自定义和运行时字节码的生成。)理想情况下,合同应在编译时执行,在最坏的情况下使用测试套件执行,但我的理解是运行时执行是皱眉。另一个可能有助于Java合同编程的有趣工具是Checker Framework。

评论


为了进一步了解我的最后一段(即,不要在接口中强制执行合同),值得指出的是,默认方法不能覆盖equals,hashCode和toString。关于为什么不允许这样做的非常有益的成本/收益分析,可以在这里找到:mail.openjdk.java.net/pipermail/lambda-dev/2013-March/…

–ngreen
2014年10月6日在17:13

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

–超级猫
2014年10月11日17:12

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

–ngreen
2014年10月14日,2:50

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

–超级猫
2014年10月14日,下午2:56

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

–ngreen
2014年10月14日3:00

#3 楼

因为您只能继承一个类。如果您有两个接口,其实现足够复杂以至于需要一个抽象基类,那么这两个接口在实践中是互斥的。静态方法并将所有字段转换为参数。这将允许该接口的任何实现者调用静态方法并获得功能,但是用一种已经太冗长的语言来说,这是一个很大的样板。


关于为什么能够在接口中提供实现的有用性,请考虑以下Stack接口:

如果堆栈为空,则为异常。我们可以通过将pop分为两种方法来强制执行此规则:执行合同的pop方法和执行实际弹出操作的public final方法。

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

    T pop() throws EmptyException;
 }


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

我不确定Java 8向接口添加方法的方法是否允许添加最终方法或受保护的抽象方法,但是我知道D语言允许这样做,并为合同设计提供本地支持。由于protected abstract是最终的,因此此技术没有危险,因此没有实现类可以覆盖它。

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


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


由于您无法在接口中声明字段,因此情况并非如此。您在接口中编写的任何方法都不能依赖任何实现细节。


作为支持接口中静态方法的示例,请考虑Java API中的工具类(如Collections)。该类仅存在是因为这些静态方法无法在其各自的接口中声明。同样可以在pop接口中声明Collections.unmodifiableList,这样会更容易找到。

评论


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

–罗伯特·哈维(Robert Harvey)
2014年3月20日在16:23

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

–Doval
2014年3月20日在16:44



等一下堆栈上的Push和Pop方法不会是静态的。

–罗伯特·哈维(Robert Harvey)
2014年3月20日在16:46

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

–Doval
2014年3月20日在16:47

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

–乔治
2014年3月20日在17:37

#4 楼

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

评论


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

–罗伯特·哈维(Robert Harvey)
2014年3月20日在16:15



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

–rae1
2014年3月20日在16:24



扩展方法很棒。它们给人一种幻觉,即您正在将功能附加到类或接口上。只是不要将实际方法添加到类中而混淆;类方法可以访问对象的私有成员,而扩展方法则不能(因为它们实际上只是调用静态方法的另一种方式)。

–罗伯特·哈维(Robert Harvey)
2014年3月20日在16:29

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

–rae1
2014年3月20日在16:46



#5 楼

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


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

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

对于现有接口的扩展-甚至在那里,一些新方法也只是语法糖。 Collection的方法,例如streamforEachremoveIf-基本上,这只是实用程序,您无需重写。
然后有类似spliterator的方法。默认实现不是最优的,但是,至少代码可以编译。仅当您的接口已经发布并得到广泛使用时,才诉诸于此。



对于static方法,我想其他方法可以很好地覆盖它:它允许接口成为自己的实用程序类。也许我们可以在Java的未来中摆脱CollectionsSet.empty()会晃动。