问题

默认方法是我们的​​Java工具箱中一个不错的新工具。但是,我尝试编写一个定义了toString方法的default版本的接口。 Java告诉我这是禁止的,因为在java.lang.Object中声明的方法可能不是2777840087。为什么会这样?

我知道有"基类永远胜利"规则,所以默认情况下(双关语),anObject方法的任何default实现都会被Object的方法覆盖。但是,我认为没有理由说明规范中Object的方法不应该有例外。特别是fortoStringit可能对默认实现非常有用。

那么,Java设计者决定不允许default方法覆盖Object方法的原因是什么?


#1 热门回答(161 赞)

这是另一个语言设计问题,看起来"显然是一个好主意",直到你开始挖掘,你意识到它实际上是一个坏主意。

This mail在这个主题上有很多(以及其他主题)。有几种设计力量汇聚在一起,使我们进入当前的设计:

  • 保持继承模型简单的愿望;
  • 一旦你看过明显的例子(例如,将AbstractList转换为接口),你会发现继承equals / hashCode / toString与单继承和状态紧密相关,接口是多重继承和无状态的;
  • 它可能为一些令人惊讶的行为打开了大门。

你已经触及了"保持简单"的目标;继承和冲突解决规则设计得非常简单(类胜过接口,派生接口胜过超接口,任何其他冲突都由实现类解决。)当然这些规则可以调整为异常,但是我认为当你开始使用该字符串时,你会发现增量复杂性并不像你想象的那么小。

当然,有一定程度的好处可以证明更复杂,但在这种情况下它并不存在。我们在这里讨论的方法是equals,hashCode和toString。这些方法本质上都是关于对象状态的,并且它是拥有状态而不是接口的类,它最有能力确定该等级对于该类的意义(特别是当相等的契约非常强时;请参阅有效Java有一些令人惊讶的后果);接口编写器太远了。

拔出AbstractList例子很容易;如果我们可以摆脱AbstractList并将行为放入List接口,那将是可爱的。但是,一旦你超越了这个明显的例子,就没有很多其他好的例子可以找到。在root,AbstractList是为单继承而设计的。但接口必须设计为多重继承。

此外,假设你正在编写此课程:

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

Foo编写器查看超类型,看不到equals的实现,并得出结论要获得引用相等,他所需要做的就是继承等于fromObject。然后,下周,Bar"帮助"的库维护者添加了一个默认的equals实现。哎呀!现在,已经通过另一个维护域中的接口"帮助"为常用方法添加默认值来破坏了Foo的语义。

默认值应该是默认值。将缺省值添加到没有的接口(层次结构中的任何位置)不应该影响具体实现类的语义。但是,如果默认值可以"覆盖"Object方法,那就不是真的。

因此,虽然它看起来像一个无害的功能,但它实际上是非常有害的:它增加了很少的增量表达性的复杂性,并且它使得单独编译的接口的好意,无害的改变太容易破坏实现类的预期语义。


#2 热门回答(26 赞)

禁止在java.lang.Object中的方法的接口中定义默认方法,因为默认方法永远不会"可达"。

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

Oracle的Brian Goetz在mailing list post中提供了有关设计决策的更多细节。


#3 热门回答(3 赞)

我没有看到Java语言作者的头脑,所以我们可能只是猜测。但我在这个问题上看到了很多理由并且完全同意这些理由。

引入默认方法的主要原因是能够在不破坏旧实现的向后兼容性的情况下向接口添加新方法。默认方法也可用于提供"便利"方法,而无需在每个实现类中定义它们。

这些都不适用于toString和Object的其他方法。简单地说,默认方法被设计为提供默认的行为,其中没有其他定义。不提供将与其他现有实现"竞争"的实现。

"基类永远胜利"规则也有其坚实的理由。假设类定义了实现,而接口则定义了多个实现,这些实现稍微弱一些。

此外,对一般规则引入任何例外会导致不必要的复杂性并引发其他问题。对象(或多或少)是一个类,所以为什么它应该有不同的行为?

总而言之,你提出的解决方案可能会带来比专业人士更多的缺点。


原文链接