为什么在Java 8接口方法中不允许“final”?
Java 8最有用的功能之一是接口上的新default
方法。 基本上有两个原因(可能还有其他原因)为什么被引入:
Iterator.remove()
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);
}
现在, default
和final
显然是矛盾的关键字,但默认关键字本身不会被严格要求,所以我假设这个矛盾是故意的,以反映“类方法与身体”(只是方法)和“接口方法与身体”(默认方法),即我还没有理解的差异。
在某些时候,对接口方法的static
和final
修饰符的支持还没有完全探索,引用Brian Goetz:
另一部分是我们将在多大程度上支持接口中的类构建工具,比如最终方法,私有方法,受保护方法,静态方法等。答案是:我们还不知道
从2011年晚些时候开始,很显然,在接口中增加了对static
方法的支持。 显然,这为JDK库自身增加了很多价值,例如Comparator.comparing()
。
题:
final
(以及static final
)从来没有将它用于Java 8接口,原因是什么?
这个问题在某种程度上与Java 8接口方法中不允许“同步”的原因有什么关系?
了解默认方法的关键在于主要设计目标是界面演进,而不是“将界面变成(平庸)特性”。 虽然两者之间有一些重叠,但我们试图在不妨碍前者的情况下适应后者,从这个角度来看这些问题是最好的理解。 (请注意,由于接口方法可以被多次继承,所以类方法将与接口方法有所不同,不管其意图如何。)
默认方法的基本思想是:它是一个带有默认实现的接口方法,派生类可以提供更具体的实现。 而且由于设计中心是界面演进,所以这是一个关键的设计目标,即默认方法能够以源兼容和二进制兼容的方式添加到接口之后。
对于“为什么不是最终默认方法”的太简单的回答是,那么身体不会仅仅是默认实现,它将是唯一的实现。 虽然这有点太简单了,但它给我们提供了一个线索,即问题已经朝着一个有问题的方向前进。
最终界面方法有问题的另一个原因是它们为实现者带来了不可能的问题。 例如,假设您有:
interface A {
default void foo() { ... }
}
interface B {
}
class C implements A, B {
}
在这里,一切都很好。 C从A继承foo()现在假设B被更改为具有foo方法,并且具有缺省值:
interface B {
default void foo() { ... }
}
现在,当我们去重新编译C时,编译器会告诉我们它不知道foo()
要继承什么行为,所以C必须重写它(并且可以选择委托给A.super.foo()
if它想要保持相同的行为。)但是,如果B已经做出默认的最终决定,并且A不在C的作者的控制之下呢? 现在C是无法挽回的破碎; 如果不重写foo()
,它就无法编译,但如果它在B
是最终的,则它不能覆盖foo()
。
这只是一个例子,但重点是最终方法真的是一个工具,在单继承类(通常将状态耦合到行为)的世界中更有意义,而不是仅仅贡献行为并且可以被多继承的接口。 很难推断“什么其他接口可能混入最终的实现者”,并且允许接口方法是最终的可能会导致这些问题(并且它们不会炸毁编写接口的人,而是在试图实现它的穷人。)
阻止他们的另一个原因是他们不代表你的想法。 仅当类(或其超类)不提供方法的声明(具体或抽象)时才考虑默认实现。 如果默认方法是最终的,但超类已经实现了该方法,那么默认方法将被忽略,这可能不是默认作者在声明最终方法时预期的内容。 (这种继承行为反映了默认方法的设计中心 - 接口进化,应该可以向已有实现的现有接口添加默认方法(或默认实现),而不必更改实现接口的现有类的行为,保证已经在缺省方法之前工作的类将在缺省方法的情况下以相同的方式工作。)
在lambda邮件列表中有很多关于它的讨论。 其中一个似乎包含了关于所有东西的大量讨论如下:在变化的界面方法可见性(是最后的防御者)。
在本次讨论中,原始问题的作者Talden提出了与你的问题非常相似的问题:
公开所有接口成员的决定确实是一个不幸的决定。 任何在内部设计中使用接口都暴露出实现私有细节是一个很大的问题。
这是一个难以修复的问题,而不会增加一些模糊或兼容性来突破语言的细微差别。 这种大小和潜在微妙的兼容性突破将是不合情理的,因此必须存在不破坏现有代码的解决方案。
可以重新引入“包”关键字作为访问说明符是可行的。 在接口中没有指定符会暗示公共访问,并且在类中缺少指定符意味着包访问。 哪些说明符在接口中有意义尚不清楚 - 特别是如果为了最大限度地减少开发人员的知识负担,我们必须确保访问说明符在类和接口中表示相同的东西(如果它们存在的话)。
在没有默认方法的情况下,我推测接口中成员的说明符必须至少与接口本身一样可见(所以接口实际上可以在所有可见的上下文中实现) - 默认方法不是如此确定。
关于这是否可能是范围内的讨论,是否有任何明确的交流? 如果不是,它应该在其他地方举行。
最终Brian Goetz的回答是:
是的,这已经在探索中。
但是,让我设定一些现实的期望 - 语言/虚拟机功能有很长的交付时间,甚至像这样的琐碎看似的。 为Java SE 8提出新的语言功能创意的时间已经过去了。
所以,很可能它从来没有实施过,因为它从来不是范围的一部分。 这是从来没有提出的时间来考虑。
在关于这个问题的最后辩护人方法的另一场激烈讨论中,布莱恩再次说道:
你已经得到了你想要的东西。 这正是这个功能所增加的 - 多重行为继承。 当然我们知道人们会用它们作为特征。 我们努力确保他们提供的继承模式简单而又干净,以至于人们可以在各种各样的情况下获得良好的结果。 与此同时,我们选择不要把它们推到一个简单而干净的作品的边界之外,在某些情况下会导致“aw,你没有做得足够多”的反应。 但是,真的,这个主题的大部分似乎都抱怨说玻璃只有98%满了。 我会采取这98%,并继续下去!
所以这强化了我的理论,即它不是它们设计范围或部分的一部分。 他们所做的就是提供足够的功能来处理API演进的问题。
对于@EJP评论中提到的答案,很难找到和识别“答案”:世界上大约有2(+/- 2)个人可以给出明确的答案。 毫无疑问,答案可能就像是“支持最终的默认方法似乎不值得重构内部呼叫解决机制”。 这当然是猜测,但它至少由微妙的证据支持,如OpenJDK邮件列表中的这个声明(由两个人之一):
“我想如果允许”最终默认“方法,他们可能需要从内部invokespecial重写到用户可见invokeinterface。”
以及像这样的简单事实,当它是一个default
方法时,方法不会被认为是一个(真正的)最终方法,正如目前在OpenJDK中的Method :: is_final_method方法中实现的那样。
进一步真正的“授权”信息确实很难找到,即使是过多的websearches和阅读提交日志。 我认为这可能接口方法的解析过程中与调用相关的潜在歧义invokeinterface
指令和类方法调用,对应invokevirtual
指令:对于invokevirtual
指令,有可能是一个简单的虚函数表的查找,因为该方法必须要么从超类继承,要么直接由类实现。 与此相反, invokeinterface
调用必须检查相应的调用站点,以确定该调用实际引用哪个接口(这在HotSpot Wiki的InterfaceCalls页面中有更详细的说明)。 然而, final
方法都既不会被插入到虚函数表的全部或替换虚函数表的现有条目(见klassVtable.cpp。线333),同样,默认的方法是在虚函数表替换现有的条目(见klassVtable.cpp, 202行)。 因此,实际的原因(以及答案)必须在(相当复杂的)方法调用解析机制中隐藏得更深,但是这些参考可能会被认为是有帮助的,不过对于那些设法得出实际答案的其他人从那。
上一篇: Why is "final" not allowed in Java 8 interface methods?