Java8:为什么禁止为 java.lang.Object 中的方法定义默认方法

2022-08-31 07:31:07

默认方法是我们 Java 工具箱中一个很好的新工具。但是,我试图编写一个定义该方法版本的接口。Java告诉我这是被禁止的,因为中声明的方法可能不会被ed。为什么会这样?defaulttoStringjava.lang.Objectdefault

我知道有“基类总是赢”规则,所以默认情况下(双关语;),任何方法的实现都会被该方法覆盖。但是,我认为没有理由为什么规范中的方法不应该有例外。特别是对于具有默认实现可能非常有用。defaultObjectObjectObjecttoString

那么,Java设计人员决定不允许方法覆盖来自的方法的原因是什么?defaultObject


答案 1

这是另一个语言设计问题,似乎“显然是一个好主意”,直到你开始挖掘,你意识到它实际上是一个坏主意。

这封邮件有很多关于这个主题(以及其他主题)。有几个设计力量汇聚在一起,将我们带到了当前的设计:

  • 希望保持继承模型简单;
  • 事实上,一旦你忽略了明显的例子(例如,变成一个接口),你就会意识到继承equals/hashCode/toString与单继承和状态密切相关,接口是多重继承和无状态的;AbstractList
  • 它可能会为一些令人惊讶的行为打开大门。

您已经谈到了“保持简单”的目标;继承和冲突解决规则被设计得非常简单(类胜过接口,派生接口胜过超接口,任何其他冲突都由实现类解决。当然,这些规则可以调整为例外,但我认为当你开始拉动这个字符串时,你会发现增量的复杂性并不像你想象的那么小。

当然,有一定程度的好处可以证明更复杂的合理性,但在这种情况下,它不存在。我们在这里谈论的方法是equals,hashCode和toString。这些方法本质上都是关于对象状态的,拥有状态的类,而不是接口,处于最有利的位置来确定平等对该类意味着什么(特别是因为平等的契约非常强大;参见有效的Java了解一些令人惊讶的后果);接口编写器距离太远了。

很容易拉出这个例子;如果我们能够摆脱并将行为放入界面中,那就太好了。但是,一旦你超越了这个明显的例子,就没有多少其他好的例子可以找到了。在根,是为单一继承而设计的。但接口必须设计为多重继承。AbstractListAbstractListListAbstractList

此外,假设您正在编写以下类:

class Foo implements com.libraryA.Bar, com.libraryB.Moo { 
    // Implementation of Foo, that does NOT override equals
}

作者查看了超类型,没有看到 equals 的实现,并得出结论,要获得引用相等,他需要做的就是从 继承 equals。然后,下周,Bar的库维护者“有帮助地”添加了一个默认实现。哎呀!现在,语义已被另一个维护域中的接口“有益地”破坏,为通用方法添加了默认值。FooObjectequalsFoo

默认值应该是默认值。将默认值添加到没有默认值的接口(层次结构中的任何位置)不应影响具体实现类的语义。但是,如果默认值可以“重写”Object 方法,那就不是这样了。

因此,虽然它似乎是一个无害的功能,但实际上它非常有害:它增加了很多复杂性,几乎没有增量表达能力,并且它使对单独编译的接口进行善意,无害的更改太容易破坏实现类的预期语义。


答案 2

禁止在 接口中为 中的方法定义默认方法,因为默认方法永远不会是“可访问的”。java.lang.Object

默认接口方法可以在实现接口的类中被覆盖,并且方法的类实现具有比接口实现更高的优先级,即使该方法是在超类中实现的也是如此。由于所有类都继承自 ,因此 中的方法将优先于接口中的默认方法,并被调用。java.lang.Objectjava.lang.Object

Oracle的Brian Goetz在这篇邮件列表帖子中提供了有关设计决策的更多详细信息。