147

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

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

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

4

5 回答 5

203

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

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

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

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

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

于 2014-06-03T22:53:51.510 回答
31

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

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

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

于 2014-06-03T14:04:31.240 回答
4

给出一个非常迂腐的答案,只禁止为来自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();
            }
        }
    }
    
于 2017-09-02T01:25:28.303 回答
3

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

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

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

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

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

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

于 2014-06-03T14:20:02.607 回答
2

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

于 2016-04-15T08:19:37.953 回答