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

gex*_*ide 119 java interface java-8 default-method

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

我知道存在"基类永远胜利"规则,因此默认情况下(pun;),方法的任何default实现Object都会被方法覆盖Object.但是,我认为没有理由说明Object规范中的方法不应该有例外.特别是对于toString具有默认实现可能非常有用.

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

Bri*_*etz 173

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

这封邮件有很多关于这个主题(以及其他主题).有几种设计力量融合在一起,使我们达到当前的设计:

  • 保持继承模型简单的愿望;
  • 事实上,一旦你看过明显的例子(例如,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
}
Run Code Online (Sandbox Code Playgroud)

Foo作家着眼于超类型,认为没有实现平等的,并得出结论,得到参考平等,所有他所要做的就是继承等于Object.然后,下周,Bar"帮助"的库维护者添加了一个默认equals实现.哎呀!现在,语义Foo已被另一个维护域中的接口破坏,"有用"地为常用方法添加默认值.

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

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

  • 我很高兴你花时间解释这一点,我很欣赏所考虑的所有因素.我同意这对于`hashCode`和`equals`会很危险,但我认为它对`toString`非常有用.例如,某些`Displayable`接口可能会定义一个`String display()`方法,并且它会节省大量的样板,以便能够定义`default String toString(){return display(); 在`Displayable`中,而不是要求每一个`Displayable`实现`toString()`或扩展`DisplayableToString`基类. (12认同)
  • @Brandon你是正确的,允许继承toString()不会是_dangerous_,就像它对equals()和hashCode()一样.另一方面,现在该功能将更加不规则 - 并且你仍然会产生关于继承规则的所有相同的额外复杂性,为了这个方法...似乎更好地在我们所做的地方干净地绘制线条. (8认同)
  • @gexicide如果`toString()`仅基于接口方法,你可以简单地向接口添加类似`default String toStringImpl()`的东西,并覆盖每个子类中的`toString()`来调用接口实现 - a有点难看,但有效,而且比没有好.:)另一种方法是制作类似`Objects.hash()`,`Arrays.deepEquals()`和[`Arrays.deepToString()`](https://docs.oracle.com/javase/ 8 /文档/ API/JAVA/util的/ Arrays.html#deepToString-java.lang.Object中:A-).@ BrianGoetz的回答为+ 1! (5认同)
  • lambda的toString()的默认行为确实是令人讨厌的.我知道lambda工厂的设计非常简单快捷,但是吐出一个起源的类名真的没有用.在函数接口中使用`default toString()`覆盖将允许我们 - 至少 - 执行诸如吐出函数的签名和实现者的父类之类的操作.更好的是,如果我们可以带来一些递归的toString策略,我们可以通过闭包来获得一个非常好的lambda描述,从而大大改善lambda学习曲线. (3认同)
  • @barneypitt 将各种“多重继承”放在同一个桶中是一个常见的错误,并天真地用“但它在 C++ 中工作得如此糟糕”的笔刷将它们全部打包。Java 总是具有_types_ 的多重继承,这没有问题。默认方法添加了_behavior_的多重继承,这同样没有问题;虽然您可以构造“钻石”,但它们不是问题,因为编译器会检测到它们并让您显式地解决它们。事实上,_state_ 的多重继承是有问题的,但接口和抽象类都不允许您这样做。 (3认同)

jar*_*bjo 29

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

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

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


jac*_*646 5

给出一个非常迂腐的答案,只禁止default为来自 的公共方法定义方法java.lang.Object。有 11 种方法可供考虑,可分为三种方式来回答这个问题。

  1. 其中六个Object方法不能有default方法,因为它们final根本不能被重写:getClass()notify()notifyAll()wait()wait(long)wait(long, int)
  2. 由于 Brian Goetz 上面给出的原因,其中三种Object方法不能有方法: 、和。defaultequals(Object)hashCode()toString()
  3. 其中两个Object方法可以default方法,尽管这种默认值充其量是有问题的:clone()finalize()

    public class Main {
        public static void main(String... args) {
            new FOO().clone();
            new FOO().finalize();
        }
    
        interface ClonerFinalizer {
            default Object clone() {System.out.println("default clone"); return this;}
            default void finalize() {System.out.println("default finalize");}
        }
    
        static class FOO implements ClonerFinalizer {
            @Override
            public Object clone() {
                return ClonerFinalizer.super.clone();
            }
            @Override
            public void finalize() {
                ClonerFinalizer.super.finalize();
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)