【问题标题】:Java8: Why is it forbidden to define a default method for a method from java.lang.ObjectJava8:为什么禁止为 java.lang.Object 中的方法定义默认方法
【发布时间】:2014-07-23 21:28:34
【问题描述】:

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

我知道有“基类总是获胜”的规则,所以默认情况下(双关语;),Object 方法的任何default 实现都会被Object 中的方法覆盖。但是,我认为规范中 Object 中的方法不应该有例外。特别是对于toString,拥有一个默认实现可能非常有用。

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

【问题讨论】:

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

标签: java interface java-8 default-method


【解决方案1】:

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

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

  • 希望保持继承模型简单;
  • 事实上,一旦您查看了明显的示例(例如,将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 的实现,并得出结论,要获得引用相等,他需要做的就是从 Object 继承 equals。然后,下周,Bar 的库维护者“有帮助地”添加了一个默认的 equals 实现。哎呀!现在Foo 的语义已被另一个维护域中的接口破坏,“有用地”为常用方法添加了默认值。

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

因此,虽然它看起来像是一个无害的功能,但实际上却是相当有害的:它增加了很多复杂性,只需要很少的增量表现力,而且对于出于善意的、看似无害的更改单独编译来说太容易了接口破坏实现类的预期语义。

【讨论】:

  • 我很高兴您花时间解释这一点,我感谢您考虑的所有因素。我同意这对hashCodeequals 来说是危险的,但我认为这对toString 非常有用。例如,某些Displayable 接口可能定义了一个String display() 方法,并且可以节省大量样板文件以便能够在Displayable 中定义default String toString() { return display(); },而不是要求每个Displayable 都实现toString()或扩展 DisplayableToString 基类。
  • @Brandon 你说得对,允许继承 toString() 不会像 equals() 和 hashCode() 那样危险。另一方面,现在这个特性会更加不规则——而且你仍然会在继承规则方面产生同样的额外复杂性,为了这个方法......似乎更好地在我们所做的地方划清界限.
  • @gexicide 如果toString() 仅基于接口方法,您可以简单地将default String toStringImpl() 添加到接口中,并在每个子类中覆盖toString() 以调用接口实现-a有点难看,但有效,总比没有好。 :) 另一种方法是制作Objects.hash()Arrays.deepEquals()Arrays.deepToString() 之类的东西。为@BrianGoetz 的回答 +1!
  • lambda 的 toString() 的默认行为确实令人讨厌。我知道 lambda 工厂的设计非常简单和快速,但是吐出派生的类名确实没有帮助。在函数式接口中有一个default toString() 覆盖将允许我们——至少——做一些事情,比如吐出函数的签名和实现者的父类。更好的是,如果我们可以使用一些递归的 toString 策略,我们可以遍历闭包以获得对 lambda 的非常好的描述,从而大大改善 lambda 学习曲线。
  • 对任何类、子类、实例成员中的 toString 的任何更改都可能对实现类或类的用户产生影响。此外,对任何默认方法的任何更改也可能会影响所有实现类。那么当涉及到某人改变接口的行为时,toString、hashCode 有什么特别之处呢?如果一个类扩展了另一个类,他们也可以改变。或者,如果他们正在使用委托模式。使用 Java 8 接口的人必须通过升级来做到这一点。可以提供可以在子类上抑制的警告/错误。
【解决方案2】:

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

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

来自 Oracle 的 Brian Goetz 在mailing list post 中提供了有关设计决策的更多详细信息。

【讨论】:

    【解决方案3】:

    给出一个很迂腐的回答,只禁止为java.lang.Objectpublic方法定义default方法。有 11 种方法可供考虑,可以分为三种方式来回答这个问题。

    1. Object 方法中有六个不能有default 方法,因为它们是final 并且根本不能被覆盖:getClass()notify()notifyAll()wait()wait(long)wait(long, int)
    2. 三个Object 方法不能有default 方法,原因是Brian Goetz 给出的上述原因: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 正如明确指出的那样,他的观点非常迂腐。答案为已被接受的优秀答案添加了一些信息,这已经涵盖了 WHY 部分。
    【解决方案4】:

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

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

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

    “基类总是赢”的规则也有其坚实的理由。假设类定义了真正的实现,而接口定义了默认的实现,这有点弱。

    此外,在一般规则中引入任何例外都会导致不必要的复杂性并引发其他问题。对象(或多或少)和其他任何类一样,那么它为什么会有不同的行为呢?

    总而言之,您提出的解决方案可能弊大于利。

    【讨论】:

    • 发布我的时,我没有注意到 gexicide 答案的第二段。它包含一个更详细地解释该问题的链接。
    【解决方案5】:

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

    【讨论】:

      猜你喜欢
      • 2015-08-10
      • 1970-01-01
      • 1970-01-01
      • 2023-03-04
      • 2012-04-16
      • 1970-01-01
      • 2011-01-18
      相关资源
      最近更新 更多