为什么Java 8接口方法不允许使用"final"?

376
Java 8 最有用的功能之一是接口上的新 default 方法。它们被引入有两个主要原因(可能还有其他原因): 从 API 设计师的角度来看,我希望能够在接口方法上使用其他修饰符,例如 final。这在添加便利方法并防止实现类中的“意外”覆盖时非常有用:
interface Sender {

    // Convenience method to send an empty message
    default final void send() {
        send(null);
    }

    // Implementations should only implement this method
    void send(String message);
}

上述做法已经是常见的实践,如果Sender是一个类的话:
abstract class Sender {

    // Convenience method to send an empty message
    final void send() {
        send(null);
    }

    // Implementations should only implement this method
    abstract void send(String message);
}

现在,“default”和“final”显然是矛盾的关键字,但默认关键字本身不是必需的,所以我认为这种矛盾是有意的,以反映“具有主体的类方法”(仅为方法)和“具有主体的接口方法”(默认方法)之间的微妙差异,即我尚未理解的差异。
在某个时刻,像“static”和“final”这样的修饰符在接口方法上的支持还没有得到充分的探索,引用Brian Goetz的话
引述:

另一个问题是我们将在接口中为类构建工具提供的支持,例如final方法、private方法、protected方法、static方法等等。答案是:我们还不知道。

自2011年末以来,显然,在接口中支持static方法。很明显,这为JDK库本身增加了很多价值,例如Comparator.comparing()

问题:

final(以及static final)为什么没有出现在Java 8接口中?


15
很抱歉要泼冷水,但是按照Stack Overflow(SO)的限制条件,在标题中所表达的问题唯一能够得到回答的方式就是引用Brian Goetz或JSR专家组的话语。我知道BG已经要求进行公开讨论,但这恰恰违反了SO的条款,因为它是“主观性很强的”。在我看来,此处正在逃避责任。激发讨论并提出合理解释是专家组以及更广泛的Java社区过程的任务,而不是SO的任务。因此,我投票将其关闭为“主观性很强的”。 - user207421
3
众所周知,final 可以防止方法被覆盖,但由于必须重写从接口继承的方法,因此我不明白为什么将其设为 final。除非它是用来表示在重写一次后方法是最终版本的。如果我的理解不正确,请告诉我。这似乎很有趣。 - Dioxin
27
"如果我能做到,那么你也可以,并回答你自己的问题,因此就不需要提问了。" 这句话几乎适用于这个论坛上的所有问题,对吧?我总是可以自己花5个小时在谷歌上搜索某个主题,然后学到一些每个人都必须像我一样辛苦学习的东西。或者我们等待另外几分钟,让其他人给出甚至更好的答案,让将来的每个人(包括目前12个赞和8颗星)都能从中受益,因为SO在谷歌上有很好的参考资料。所以,是的。这个问题 确实 可以完美地适应SO的问答形式。" - Lukas Eder
6
在Java 8中,你不必在实现类中实现从接口继承的方法,因为Java 8允许你在接口中实现方法。在这种情况下,使用 final 可以防止实现类覆盖接口方法的默认实现。 - awksp
31
@EJP 你永远不知道:Brian Goetz可能会回复 - assylias
显示剩余15条评论
5个回答

483
This question is, to some degree, related to 为什么Java 8不允许在接口方法中使用“synchronized”? 默认方法的关键是理解其主要设计目标是接口演化,而不是将接口变成(平庸的)特征。虽然两者有些重叠,在前者不妨碍后者的情况下,我们试图对后者进行适应,但是最好从这个角度来看待这些问题。(还要注意类方法与接口方法不同,无论意图如何,由于接口方法可以被多重继承,所以它们会有所不同。)
默认方法的基本思想是:它是带有默认实现的接口方法,派生类可以提供更具体的实现。因为设计中心是接口演化,所以默认方法的关键设计目标是能够以源代码兼容和二进制兼容的方式在事后向接口添加默认方法。

“为什么不能使用最终默认方法”的太过简单的答案是,因为这样的话,方法体就不再只是默认实现,而是唯一的实现。虽然这个答案有点过于简单,但它给我们一个线索,表明这个问题已经朝着一个值得怀疑的方向发展了。

另一个原因是最终接口方法会给实现者带来无法解决的问题。例如,假设你有:


interface A { 
    default void foo() { ... }
}

interface B { 
}

class C implements A, B { 
}

在这里,一切都很好;CA继承了foo()。现在假设B被改变以拥有一个默认的foo方法:
interface B { 
    default void foo() { ... }
}

现在,当我们重新编译C时,编译器会告诉我们它不知道继承foo()的行为应该是什么,所以C必须覆盖它(如果它想保留相同的行为,它可以选择委托给A.super.foo())。但是,如果B将其默认设置为final,并且A不在C的作者控制之下呢?现在,C已经无法修复了;它不能在没有覆盖foo()的情况下编译,但如果它在B中被标记为final,它也不能覆盖foo()。这只是一个例子,但关键是方法的最终性确实是一种更适合于单继承类(通常将状态与行为耦合)的工具,而不适用于仅贡献行为并且可以多重继承的接口。很难推断“最终实现者可能混入哪些其他接口”,并且允许接口方法是final可能会导致这些问题(并且它们会在尝试实现它的可怜用户身上爆发)。
禁止默认方法的另一个原因是它们可能不会像您想象的那样起作用。仅当类(或其超类)未提供方法的声明(具体或抽象)时,才会考虑默认实现。如果默认方法是final的,但是超类已经实现了该方法,则默认方法将被忽略,这可能不是默认作者在声明为final时期望的结果。(这种继承行为反映了默认方法的设计中心-接口演化。应该能够向已有实现添加默认方法(或将默认实现添加到现有接口方法),而不更改实现接口的现有类的行为,从而保证在添加默认方法之前已经工作的类在存在默认方法的情况下仍然能够正常工作。)

101
看到你回答关于新语言特性的问题非常棒!当我们正在弄清楚如何使用新功能时,得到设计意图和细节方面的澄清非常有帮助。其他参与设计的人也在 Stack Overflow 上做出贡献吗?还是只有你自己在回答问题?我会在 java-8 标签下关注你的答案 - 我想知道是否有其他人也在这样做,这样我就可以关注他们了。 - Shorn
11
@Shorn Stuart Marks 在Java-8标签中很活跃。Jeremy Manson 以前曾经发布过帖子。我还记得看到过 Joshua Bloch 的留言,但现在找不到了。 - assylias
3
恭喜你提出了默认接口方法,这是一种比 C# 扩展方法更为优雅的实现方式。至于这个问题,关于无法解决名称冲突的答案已经解决了问题,但其他提供的原因在语言学上并不令人信服。(如果我希望接口方法是 final 的,那么你应该认为我必须有自己相当充分的理由来禁止任何人提供与我的实现不同的实现方式。) - Mike Nakis
22
抽象类仍然是引入状态或实现核心对象方法的唯一方式。默认方法用于纯行为,而抽象类用于与状态相关的行为。 - Brian Goetz
1
添加默认方法仍然可能“为实现者创建不可能的问题”。如果A声明int foo(),而B添加default String foo(),则C无法编译。禁止final default方法可以_减少_接口不兼容的风险,但不能消除它。话虽如此,我完全同意不应允许final default。对我来说,最有说服力的论点是“这将有效地使接口成为抽象类,通过允许它们指定实现。多重继承的抽象类已经被禁止,有很好的理由。” - Clement Cherlin
显示剩余3条评论

47
在Lambda邮件列表中有很多关于此的讨论。其中似乎包含了大量关于所有这些内容的讨论之一是:关于各种接口方法可见性(即最终防御者)
在这个讨论中,提出原问题的作者Talden问了与您的问题非常相似的问题:
决定使所有接口成员公开确实是一个不幸的决定。在内部设计中使用接口会暴露实现私有细节,这是一个大问题。没有添加某些晦涩或兼容性破坏细节的情况下,很难解决这个问题。破坏那样的兼容性和潜在的微妙之处似乎是不可原谅的,因此必须存在一种不破坏现有代码的解决方案。重新引入“package”关键字作为访问限定符是否可行?它在接口中缺少限定符将意味着公共访问,在类中缺少限定符则意味着包访问。哪些限定符在接口中是有意义的尚不清楚,特别是如果我们要确保访问限定符在类和接口中的含义相同(如果它们存在),以最小化开发人员的知识负担。在默认方法不存在的情况下,我曾推测接口成员的限定符至少要与接口本身一样可见(因此接口可以在所有可见上下文中实际实现)-但具有默认方法后就不那么确定了。是否已经明确表明这是否是一个可能的范围内讨论?如果没有,是否应该在其他地方进行讨论。

最终,Brian Goetz的回答是:

是的,这已经在研究中了。

然而,请让我设定一些现实的期望——语言/虚拟机功能需要很长时间来开发,即使是看似微不足道的功能也是如此。Java SE 8 提出新语言功能的时间已经基本过去了。

因此,最有可能的原因是它从未被实现,因为它从未涉及到范围内。它从未在适当的时间提出以供考虑。

在另一场关于final defender methods的激烈讨论中,Brian再次表示

你已经得到了你所希望的。这正是这个功能增加的——行为的多重继承。当然,我们明白人们会将它们用作特征。我们努力确保它们提供的继承模型足够简单和清晰,以便人们可以在各种情况下获得良好的结果。同时,我们选择不将它们推向超出简单和清晰范围的边界,这在某些情况下会引起“啊,你没有走得够远”的反应。但是,实际上,这个线程的大部分内容似乎只是在抱怨玻璃仅仅填满了98%。我会接受那98%,并继续前进! 因此,这加强了我的理论,即它根本不是其范围或设计的一部分。他们所做的就是提供足够的功能来处理API演变的问题。

4
今早我在Google搜索中应该包含旧名称“防御者方法”。感谢您挖掘出这个信息。 - Marco13
1
很好的挖掘历史事实。你的结论与官方答案非常一致。 - Lukas Eder
我不明白为什么这会破坏向后兼容性。之前是不允许final的,现在可以用于private,但不能应用于protected。嗯...private无法实现...扩展另一个接口的接口可能会实现父接口的某些部分,但它必须将重载能力暴露给其他人... - mjs

19
很难找到和确定“THE”答案,正如@EJP在评论中提到的原因:全世界大约只有2个(+/- 2)人可以给出明确的答案。而且怀疑的话,答案可能只是类似于“支持最终默认方法似乎不值得重构内部调用解析机制”的东西。当然这只是猜测,但至少有微妙的证据支持,比如OpenJDK邮件列表中的Statement (by one of the two persons)
“我想如果允许“final default”方法,它们可能需要从内部invokespecial重写为用户可见的invokeinterface。”
此外还有一些琐碎的事实,例如当一个方法是一个default方法时,它就不被认为是(真正的)最终方法,正如在OpenJDK中当前实现的Method::is_final_method方法所示。

即使进行了大量的网络搜索并阅读提交日志,仍然很难找到真正“权威”的信息。我认为这可能与使用invokeinterface指令解决接口方法调用时可能存在的歧义有关,以及类方法调用对应的invokevirtual指令:对于invokevirtual指令,可能会进行简单的vtable查找,因为该方法必须从超类继承或由类直接实现。相比之下,invokeinterface调用必须检查相应的调用站点,以找出这个调用实际上是指哪个接口(这在HotSpot Wiki的InterfaceCalls页面中有更详细的解释)。然而,final方法要么根本不会插入vtable,要么替换vtable中的现有条目(请参见klassVtable.cpp. Line 333),同样,默认方法也会替换vtable中的现有条目(请参见klassVtable.cpp, Line 202)。因此,实际原因(和答案)必须深藏在(相当复杂的)方法调用解析机制的内部,但也许这些参考资料仍然被认为是有帮助的,即使只是对于那些能够从中推导出实际答案的人。


谢谢你提供的有趣见解。John Rose的文章是一篇非常有趣的追溯文章。尽管如此,我仍然不同意@EJP的观点。作为一个反例,可以查看我的回答Peter Lawrey提出的非常有趣、非常类似的问题。挖掘历史事实是可行的,我总是很高兴在Stack Overflow(还有哪里?)上找到它们。当然,你的答案仍然是推测性的,我并不完全相信JVM实现细节会成为JLS以某种方式编写的最终原因(双关语)。 - Lukas Eder
@LukasEder 当然,这些问题很有趣,而且我认为它们适合于问答模式。我认为引起争议的两个关键点是:第一,您要求“原因”。在许多情况下,这可能只是没有正式记录。例如,在JLS中没有提到为什么没有无符号的int,但请参见https://dev59.com/YXRC5IYBdhLWcg3wCMnX... ... - Marco13
第二个问题是你只要求“权威引用”,这会将敢于回答的人数从“几十个”降至“大约为零”。除此之外,我不确定JVM的开发和JLS的编写是如何交织在一起的,即开发对写入JLS的影响有多大,但是……我会避免任何猜测;-) - Marco13
1
我仍然坚持我的立场。看看谁回答了我的其他问题 :-) 现在,在 Stack Overflow 上,有一个权威的答案,永远清楚地解释了为什么决定不支持在default方法上使用synchronized - Lukas Eder
3
@LukasEder 我明白了,我也是这样认为的。谁能想到呢?这些原因相当有说服力,特别是对于这个“最终”的问题来说,而且令人有些自惭形秽的是,似乎没有人想到类似的例子(或者可能有些人考虑了这些例子,但不够有权威回答)。所以现在(很抱歉,我必须这样说:)最终的结论已经出来了。 - Marco13

4

我认为在方便的接口方法上指定final并不是必要的。但我同意,这样做可能有所帮助,但显然成本大于收益。

无论如何,你应该为默认方法编写适当的javadoc,明确显示该方法可执行和不可执行的操作。通过这种方式,实现接口的类“不允许”更改实现,尽管没有保证。

任何人都可以编写一个遵循该接口的Collection,然后在方法中执行绝对反常的操作,除了编写广泛的单元测试外,没有其他方法来保护你免受此类情况。


2
Javadoc合同是我在问题中列出的具体示例的有效解决方法,但问题实际上并不涉及方便接口方法用例。问题是关于为什么Java 8接口方法被决定不允许使用final的权威原因。成本/效益比不足是一个很好的候选,但到目前为止,那只是猜测。 - Lukas Eder

1
当我们知道实现接口的类可能会或可能不会覆盖我们的实现时,我们在接口内部的方法中添加“default”关键字。但是,如果我们想添加一个不希望任何实现类覆盖的方法呢?好吧,我们有两个选择:
  1. 添加“default”和“final”方法。
  2. 添加静态方法。
现在,Java表示,如果我们有一个类实现了两个或多个接口,这些接口具有相同的方法名称和签名(即它们是重复的)的默认方法,则我们需要在我们的类中提供该方法的实现。现在,在默认的final方法的情况下,我们无法提供实现,因此我们被卡住了。这就是为什么接口中不使用“final”关键字的原因。

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