这是另一个看起来“显然是个好主意”的语言设计问题,直到您开始挖掘并意识到这实际上是一个坏主意。
这封邮件有很多关于这个主题(以及其他主题)。有几种设计力量汇聚在一起,将我们带到了当前的设计中:
- 保持继承模型简单的愿望;
- 事实上,一旦你回顾了明显的例子(例如,
AbstractList
变成一个接口),你就会意识到继承 equals/hashCode/toString 与单一继承和状态密切相关,而接口是多重继承和无状态的;
- 它可能为一些令人惊讶的行为打开了大门。
您已经触及“保持简单”的目标;继承和冲突解决规则被设计得非常简单(类胜过接口,派生接口胜过超接口,任何其他冲突都由实现类解决。)当然,这些规则可以调整为例外,但是我想当你开始拉弦时你会发现,增量的复杂性并不像你想象的那么小。
当然,有一定程度的好处可以证明更复杂是合理的,但在这种情况下它不存在。我们这里讨论的方法是equals、hashCode和toString。这些方法本质上都是关于对象状态的,拥有状态的是类,而不是接口,谁最有资格确定平等对那个类意味着什么(尤其是平等的契约非常强;参见有效Java 带来了一些令人惊讶的后果);界面编写者离得太远了。
抽出AbstractList
例子很容易;如果我们能摆脱AbstractList
并将行为放入List
界面中,那就太好了。但是一旦你超越了这个明显的例子,就找不到很多其他好的例子了。在根目录下,AbstractList
是为单一继承而设计的。但是接口必须为多重继承而设计。
此外,假设您正在编写这个类:
class Foo implements com.libraryA.Bar, com.libraryB.Moo {
// Implementation of Foo, that does NOT override equals
}
作者Foo
查看了超类型,没有看到 equals 的实现,并得出结论,要获得引用相等性,他需要做的就是继承 equals from Object
。然后,下周,Bar 的库维护者“有帮助地”添加了一个默认equals
实现。哎呀!Foo
现在,另一个维护域中的接口“有用地”打破 了语义,为常用方法添加了默认值。
默认值应该是默认值。向没有的接口(层次结构中的任何位置)添加默认值不应影响具体实现类的语义。但是,如果默认值可以“覆盖” Object 方法,那将是不正确的。
因此,虽然它看起来像是一个无害的特性,但实际上却是相当有害的:它增加了很多复杂性,只需要很少的增量表现力,而且它使得对单独编译的接口的善意、看似无害的更改很容易破坏实现类的预期语义。