为啥 Java 8 接口方法中不允许使用“final”?

Posted

技术标签:

【中文标题】为啥 Java 8 接口方法中不允许使用“final”?【英文标题】:Why is "final" not allowed in Java 8 interface methods?为什么 Java 8 接口方法中不允许使用“final”? 【发布时间】:2014-06-20 14:56:12 【问题描述】:

Java 8 最有用的特性之一是接口上的新default 方法。引入它们的原因主要有两个(可能还有其他原因):

提供实际的默认实现。示例:Iterator.remove() 允许 JDK API 演进。示例:Iterable.forEach()

从 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);

现在,defaultfinal 显然是矛盾的关键字,但默认关键字本身 would not have been strictly required,所以我假设这种矛盾是故意的,以反映 " 类方法与body"(只是方法)和"interface methods with body"(默认方法),即我还没有理解的区别。

在某些时候,尚未完全探索对接口方法上的 staticfinal 等修饰符的支持,citing Brian Goetz:

另一部分是我们将在多大程度上支持班级建设 接口中的工具,例如最终方法、私有方法、受保护的 方法、静态方法等。答案是:我们还不知道

从 2011 年底开始,显然增加了对接口中 static 方法的支持。显然,这为 JDK 库本身增加了很多价值,例如 Comparator.comparing()

问题:

final(以及static final)从未进入 Java 8 接口的原因是什么?

【问题讨论】:

我很抱歉成为一个湿毯子,但标题中表达的问题要在 SO 的条款内得到回答的唯一方法是通过 Brian Goetz 或 JSR 专家组的引用.我了解 BG 已要求进行公开讨论,但这恰恰违反了 SO 的条款,因为它“主要基于意见”。在我看来,责任在这里被推卸了。专家组和更广泛的 Java 社区进程的工作是激发讨论并提出基本原理。不是这样的。所以我投票以“主要基于意见”的方式结束。 众所周知,final 防止方法被重写,并且看到您必须如何重写从接口继承的方法,我不明白为什么将其定为最终方法是有意义的。除非它表示该方法在覆盖它一次之后是最终的..在那种情况下,也许有困难?如果我不理解这一点,请让我知道。看起来很有趣 @EJP: “如果我能做到,你也能,回答你自己的问题,因此不需要问。”这个论坛上的所有问题,对吧?我总是可以自己用 5 个小时搜索某个主题,然后学习其他人必须以同样困难的方式学习的东西。或者我们再等几分钟,让某人给出一个更好的答案,让未来的每个人(包括到目前为止的 12 个赞成票和 8 个星)都可以从中受益,因为 SO 在 Google 上得到了很好的引用。所以,是的。这个问题可以完美地融入 SO 的问答形式。 @VinceEmigh "...看看您必须如何覆盖从接口继承的方法..." 在 Java 8 中并非如此。Java 8 允许您实现方法在接口中,这意味着您不必在实现类中实现它们。在这里,final 将用于防止实现类覆盖接口方法的默认实现。 @EJP 你永远不知道:Brian Goetz may reply! 【参考方案1】:

当我们知道扩展interface 的类可能会或可能不会override 我们的实现时,我们会在interface 内的方法中添加default 关键字。但是如果我们想添加一个我们不希望任何实现类覆盖的方法呢?好吧,我们有两种选择:

    添加一个default final 方法。 添加static 方法。

现在,Java 说,如果我们有一个 class 实现两个或多个 interfaces 使得它们有一个具有完全相同的方法名称和签名的 default 方法,即它们是重复的,那么我们需要提供一个实现我们班的那个方法。现在在defaultfinal 方法的情况下,我们无法提供实现并且我们被卡住了。这就是为什么在接口中不使用final 关键字的原因。

【讨论】:

【参考方案2】:

这个问题在某种程度上与What is the reason why “synchronized” is not allowed in Java 8 interface methods?有关

了解默认方法的关键在于,主要设计目标是接口进化,而不是“将接口变成(平庸的)特征”。虽然两者之间有一些重叠,我们试图适应后者而不妨碍前者,但从这个角度来看,这些问题是最好的理解。 (还要注意,类方法将不同于接口方法,无论其意图如何,因为接口方法可以被多次继承。)

默认方法的基本思想是:它是一个具有默认实现的接口方法,派生类可以提供更具体的实现。并且因为设计中心是接口演化,所以能够以源代码兼容和二进制兼容的方式将默认方法事后添加到接口是一个关键的设计目标。

“为什么不是最终的默认方法”的过于简单的答案是,那么主体将不仅仅是默认实现,它将是唯一的实现。虽然这个答案有点过于简单,但它为我们提供了一个线索,即问题已经在朝着有问题的方向发展。

最终接口方法存在问题的另一个原因是它们给实现者带来了不可能的问题。例如,假设您有:

interface A  
    default void foo()  ... 


interface B  


class C implements A, B  

在这里,一切都很好; CA 继承 foo()。现在假设 B 被更改为有一个 foo 方法,具有默认值:

interface B  
    default void foo()  ... 

现在,当我们重新编译C 时,编译器会告诉我们它不知道要为foo() 继承什么行为,所以C 必须重写它(并且可以选择委托给@ 987654334@ 如果它想保留相同的行为。)但是如果B 已将其设为默认final,并且A 不受C 的作者的控制呢?现在C 已经无法挽回地坏掉了;如果不覆盖foo(),它就无法编译,但如果它在B 中是最终的,它就不能覆盖foo()

这只是一个例子,但重点是方法的最终确定性实际上是一种工具,它在单继承类(通常将状态与行为耦合)的世界中比对仅贡献行为和行为的接口更有意义可以多重继承。很难推断“最终实现者中可能会混入哪些其他接口”,并且允许接口方法成为最终方法可能会导致这些问题(而且它们不会在编写接口的人身上爆炸,而是在试图实现它的可怜用户。)

另一个不允许它们的原因是它们的意思不是你认为的那样。仅当类(或其超类)不提供方法的声明(具体或抽象)时才考虑默认实现。如果默认方法是最终方法,但超类已经实现了该方法,则默认方法将被忽略,这可能不是默认作者在声明它为最终方法时所期望的。 (这种继承行为反映了默认方法的设计中心——接口进化。应该可以将默认方法(或现有接口方法的默认实现)添加到已有实现的现有接口,而无需更改实现接口的现有类的行为,保证在添加默认方法之前已经工作的类在默认方法存在的情况下以相同的方式工作。)

【讨论】:

很高兴看到您回答有关新语言功能的问题!在弄清楚我们应该如何使用新功能时,弄清设计的意图和细节非常有帮助。参与设计的其他人是否为 SO 做出了贡献——或者你只是自己做这件事?我会在 java-8 标签下关注你的答案——我想知道是否有其他人也在做同样的事情,所以我也可以关注他们。 @Shorn Stuart Marks 已在 java-8 标记中处于活动状态。 Jeremy Manson 过去发布过。我还记得看到来自 Joshua Bloch 的消息,但现在找不到。 多界面场景很有意思。当您有两个冲突的默认方法时会发生什么? 恭喜你提出了默认接口方法,作为一种更优雅的方式来完成 C# 使用其构思不当且实现相当丑陋的扩展方法所做的事情。至于这个问题,关于无法解决名称冲突的答案可以解决问题,但提供的其余原因是无法令人信服的语言学。 (如果我想要一个接口方法是最终的,那么你应该假设我必须有我自己非常好的理由想要禁止任何人提供与我不同的任何实现。) @Trying 抽象类仍然是引入状态或实现核心 Object 方法的唯一方法。默认方法用于纯行为;抽象类用于与状态耦合的行为。【参考方案3】:

在 lambda 邮件列表 there are plenty of discussions about it 中。其中一个似乎包含很多关于所有这些东西的讨论如下:在Varied interface method visibility (was Final defenders)。

在本次讨论中,original question 的作者 Talden 提出了与您的问题非常相似的问题:

将所有界面成员公开的决定确实是一个 不幸的决定。在内部设计中使用任何接口 公开实现私有细节是一个大问题。

在不增加一些晦涩或兼容性的情况下很难修复 打破语言的细微差别。兼容性中断 规模和潜在的微妙之处将被视为不合情理,因此 必须存在不破坏现有代码的解决方案。

是否可以重新引入“package”关键字作为访问说明符 可行的。接口中缺少说明符意味着 公共访问和类中没有说明符意味着 包访问。哪些说明符在接口中有意义尚不清楚 - 特别是如果,为了尽量减少开发人员的知识负担,我们必须确保访问说明符在两者中的含义相同 类和接口(如果存在)。

在没有默认方法的情况下,我推测 接口中成员的说明符必须至少与 接口本身(因此接口实际上可以在 所有可见的上下文)- 使用不太确定的默认方法。

是否有任何明确的沟通,这是否甚至是 可能的范围内讨论?如果没有,是否应该在其他地方举行。

最终Brian Goetz's answer 是:

是的,这已经在探索中了。

不过,让我设定一些现实的期望——语言/虚拟机 功能的交付周期很长,即使是像这样看似微不足道的功能。 为 Java SE 8 提出新的语言特性想法的时间已经 差不多过去了。

因此,很可能它从未实施过,因为它从来都不是范围的一部分。从来没有及时提出来考虑。

在另一个关于这个主题的激烈讨论about final defender methods,Brian said again:

你已经得到了你想要的。这正是 这个特性增加了——行为的多重继承。当然我们 了解人们会将它们用作特征。而且我们很努力 确保他们提供的继承模型简单且 足够干净,人们可以在广泛的范围内获得良好的结果 各种情况。同时,我们选择不推动 它们超出了简单而干净的工作范围,并且 在某些情况下会导致“哦,你做得不够远”的反应。但 真的,这个帖子的大部分似乎都在抱怨玻璃是 只有 98% 满。我会接受这 98% 并继续努力!

因此,这强化了我的理论,即它根本不是范围的一部分或他们设计的一部分。他们所做的是提供足够的功能来处理 API 演进的问题。

【讨论】:

我知道我应该在今天早上的谷歌奥德赛中加入旧名称“防御者方法”。 +1 挖掘这个。 很好地挖掘了历史事实。您的结论与the official answer 非常吻合 我不明白为什么它会破坏向后兼容性。 final 之前是不允许的。他们现在允许私有,但不受保护。 myeh ...私有无法实现...扩展另一个接口的接口可能实现父接口的一部分,但它必须向其他人公开重载能力...【参考方案4】:

我认为没有必要在方便的接口方法上指定final,我同意虽然它可能会有所帮助,但似乎成本超过了收益。

无论哪种方式,您都应该为默认方法编写适当的 javadoc,准确显示该方法是什么以及不允许做什么。这样,实现接口的类“不允许”更改实现,尽管不能保证。

任何人都可以编写一个遵循接口的Collection,然后以绝对违反直觉的方法执行操作,除了编写大量的单元测试之外,没有其他方法可以保护自己。

【讨论】:

Javadoc 合同是我在问题中列出的 concrete 示例的有效解决方法,但问题实际上与便利接口方法用例无关。问题是关于为什么 final 被决定不允许在 Java 8 interface 方法上使用的权威原因。成本/收益比不足是一个不错的选择,但到目前为止,这只是猜测。【参考方案5】:

对于@EJP 的 cmets 中提到的原因,很难找到和确定“THE”的答案:世界上大约有 2 (+/- 2) 人可以给出明确的答案 完全没有。毫无疑问,答案可能只是“支持最终默认方法似乎不值得为重构内部调用解决机制而付出努力”。这当然是猜测,但至少有微妙的证据支持,比如Statement (by one of the two persons) in the OpenJDK mailing list:

“我想如果允许“最终默认”方法,它们可能需要从内部调用特殊重写为用户可见的调用接口。”

像这样的琐碎事实,当它是 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)。因此,实际原因(因此,答案)必须隐藏在(相当复杂的)方法调用解析机制的更深处,但也许这些引用仍然会被认为是有帮助的,只是对其他人有用能够从中得出实际答案。

【讨论】:

感谢您提供有趣的见解。约翰·罗斯的作品是一个有趣的踪迹。不过,我仍然不同意@EJP。作为反例,请查看我对very interesting, very similar-style question by Peter Lawrey 的回答。 可以挖掘历史事实的,我总是很高兴在 Stack Overflow 上找到它们(还有其他地方吗?)。当然,你的回答仍然是推测性的,我不是 100% 相信 JVM 实现细节将是 JLS 以一种或另一种方式写下来的最终原因(双关语)...... @LukasEder 当然,这类问题很有趣,恕我直言,适合问答模式。我认为这里引起争议的关键有两点:第一,你问的是“原因”。在许多情况下,这可能只是没有正式记录在案。例如。 JLS 中没有提到为什么没有未签名的ints 的“原因”,但请参阅***.com/questions/430346 ... ... ... ...第二个是您要求“权威引用”,这减少了敢于从“几个一打”到……“大约为零”。除此之外,我不确定 JVM 的开发和 JLS 的编写是如何交织在一起的,即开发对 JLS 中写入的内容有多大影响,但是……我将避免在这里进行任何猜测; -) 我还在休息。看看谁回答了我的other question :-) 现在有了权威的答案就永远清楚了,在这里 Stack Overflow 为什么决定不支持 synchronized 上的 default 方法。跨度> @LukasEder 我明白了,这里也一样。谁能预料到呢?原因相当有说服力,特别是对于这个final 问题,而且似乎没有其他人想到类似的例子(或者,也许有些人考虑过这些例子,但觉得没有足够的权威来回答),这有点令人羞愧。所以现在(对不起,我必须这样做:)final 单词被说出。

以上是关于为啥 Java 8 接口方法中不允许使用“final”?的主要内容,如果未能解决你的问题,请参考以下文章

为什么Java 8接口方法中不允许“final”?

为啥接口方法不能是“静态的”和“最终的”?

为啥是 super.super.method();在 Java 中不允许?

为啥在java中不允许分配给'this'?

为啥在java中不允许没有括号的赋值和布尔运算符

为啥 WCF 中不允许方法重载?