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


130

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

我知道有一个“基类总是赢”的规则,因此默认情况下(pun;),defaultObject方法的任何实现都会被该方法覆盖Object。但是,我认为没有理由为什么Object规范中的方法不应有例外。尤其是因为toString默认实现可能非常有用。

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


1
我现在对自己感觉很好,为此投票了100次,因此获得了金牌。好问题!
尤金(Eugene)

Answers:


186

这是语言设计中的另一个问题,在您开始挖掘并且意识到这实际上是一个坏主意之前,这似乎是“显然是个好主意”。

这封邮件涉及到很多主题(以及其他主题。)有多种设计力量融合在一起,使我们进入了当前的设计:

  • 保持继承模型简单的愿望;
  • 一旦您查看了明显的示例(例如,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作家着眼于超类型,认为没有实现平等的,并得出结论,得到参考平等,所有他所要做的就是继承平等Object。然后,下周,“有帮助”的Bar库维护者添加了默认equals实现。哎呀!现在,的语义Foo已被另一个维护域中的接口“有用地”破坏了,并为通用方法添加了默认值。

默认值应该是默认值。向没有接口(层次结构中的任何地方)的接口添加默认值不应影响具体实现类的语义。但是如果默认值可以“覆盖” Object方法,那将不是事实。

因此,尽管它看起来像是无害的功能,但实际上却是非常有害的:它增加了很多复杂性,几乎没有增量表达能力,而且对于原本精心设计,无害的更改单独编译的接口来说,破坏太容易了。实现类的预期语义。


13
我很高兴您花时间来解释这一点,并且我感谢所考虑的所有因素。我同意这对hashCode和都是危险的equals,但是我认为对它将很有用toString。例如,一些Displayable接口可以定义一个String display()方法,并且这将节省大量的样板,以便能够限定default String toString() { return display(); }Displayable,而不需要每一个,Displayable以实现toString()或扩展DisplayableToString基类。
Brandon

8
@Brandon您是正确的,允许继承toString()不会像equals()和hashCode()那样危险。另一方面,现在该功能将变得更加不规则-而且,对于这种继承方法,您仍然会产生继承规则的所有其他复杂性……似乎最好在我们做的地方清楚地划清界限。
Brian Goetz 2014年

5
@gexicide如果toString()仅基于接口方法,则可以简单地default String toStringImpl()向接口添加类似的内容,并toString()在每个子类中重写以调用接口实现-有点难看,但有效,总比没有好。:)另一种方式来做到这一点是让像Objects.hash()Arrays.deepEquals()Arrays.deepToString()。为@BrianGoetz的答案+1!
小清庞-明日香贤治

3
lambda的toString()的默认行为确实令人讨厌。我知道lambda工厂的设计非常简单且快速,但是吐出派生的类名确实没有帮助。在default toString()功能接口中具有重写将至少允许我们做类似吐出函数签名和实现者父类的事情。更好的是,如果我们可以采用一些递归的toString策略,我们可以遍历闭包以对lambda进行非常好的描述,从而大大改善lambda学习曲线。
Groostav

在任何类,子类,实例成员中对toString进行的任何更改都可能对实现类或类的用户产生影响。此外,对任何默认方法的任何更改也可能会影响所有实现类。那么当有人更改接口行为时,toString,hashCode有何特别之处?如果一个班级扩展了另一个班级,他们也可能会改变。或者,如果他们使用委托模式。使用Java 8接口的人必须通过升级来做到这一点。可以提供可以在子类上抑制的警告/错误。
毫米

30

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

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

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


3

我看不到Java语言作者的头,所以我们只能猜测。但是我看到很多原因,并且在这个问题上完全同意它们。

引入默认方法的主要原因是能够向接口添加新方法而不会破坏较早实现的向后兼容性。默认方法也可以用于提供“便利”方法,而不必在每个实现类中都定义它们。

这些都不适用于toString和其他Object方法。简而言之,默认方法旨在提供没有其他定义的默认行为。不提供将与其他现有实现“竞争”的实现。

“基层总是赢”的规则也有其扎实的理由。假定类定义了实际的实现,而接口定义了默认的实现,而后者则较弱。

另外,将ANY例外引入通用规则会导致不必要的复杂性并引发其他问题。对象(或多或少)是一个类,为什么它应该具有不同的行为?

总而言之,您提出的解决方案可能会带来比专家更多的弊端。


发布我的文章时,我没有注意到杀人凶手回答的第二段。它包含一个链接,可以更详细地说明问题。
Marwin 2014年

1

推理非常简单,这是因为Object是所有Java类的基类。因此,即使我们在某些接口中将Object的方法定义为默认方法,也将是无用的,因为将始终使用Object的方法。这就是为什么要避免混淆的原因,我们不能有覆盖对象类方法的默认方法。


1

为了给出一个非常古怪的答案,仅禁止从中default公共方法定义方法java.lang.Object。有11种方法可供考虑,可以通过三种方式进行分类来回答此问题。

  1. 六个Object方法不能有default方法,因为他们final和所有不能被覆盖:getClass()notify()notifyAll()wait()wait(long),和wait(long, int)
  2. 三种Object方法都没有default通过布赖恩戈茨由于上述原因的方法:equals(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();
            }
        }
    }

。重点是什么?您仍然没有回答WHY部分-“那么,Java设计者决定不允许默认方法覆盖Object中的方法的原因是什么?”
pro_cheats
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.