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

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

我知道有一个“基类总是赢”的规则,所以默认情况下(双关语;) ,Object方法的任何 default实现都会被来自 Object的方法覆盖。但是,我认为没有理由不在规范中对来自 Object的方法进行异常处理。特别是对于 toString来说,有一个默认的实现可能非常有用。

那么,为什么 Java 设计者决定不允许 default方法重写来自 Object的方法呢?

23028 次浏览

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

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

来自 Oracle 的 BrianGoetz 在这个 邮件列表邮件中提供了一些关于设计决策的更多细节。

我不了解 Java 语言作者的头脑,所以我们只能猜测。但在这个问题上,我看到了许多理由,并完全同意他们的观点。

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

这些都不适用于 toString 和 Object 的其他方法。简单地说,缺省方法被设计为在没有其他定义的情况下提供 违约行为。不提供将与其他现有实现“竞争”的实现。

“基类永远是赢家”的规则也有其充分的理由。假设类定义 真的实现,而接口定义 违约实现,这些实现稍微弱一些。

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

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

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

这封邮件有很多关于这个主题的内容(还有其他主题)有几种设计力量汇聚在一起,把我们带到了现在的设计:

  • 希望保持继承模型的简单性;
  • 事实上,一旦你忽略了那些显而易见的例子(例如,将 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 方法,那就不是真的了。

因此,虽然它看起来是一个无害的特性,但实际上它是相当有害的: 它为很少的增量表现力增加了很多复杂性,而且它使得对单独编译的接口进行有意的、看起来无害的更改来破坏实现类的预期语义变得太容易了。

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

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

  1. 六个 Object方法不能有 default方法,因为它们是 final,根本不能被覆盖: getClass()notify()notifyAll()wait()wait(long)wait(long, int)
  2. 由于上述 Brian Goetz 给出的原因,三种 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();
    }
    }
    }