使用祖先接口的默认方法

5
我完全不明白为什么这样做行不通:
interface Test {

    default void doMagic() {
        System.out.println("Abracadabra");
    }
}

class TestImpl implements Test {

}

class SpecialTestImpl extends TestImpl {

    public void doMagic() {
        Test.super.doMagic(); // Error: No enclosing instance of the type Test is accessible in scope
    }
}

这是一条奇怪的Eclipse错误消息(Eclipse也无法处理Lambda表达式,所以Mars或许还没有完全准备好Java 8)。您可以通过让“SpecialTestImpl”直接实现“Test”接口来解决此问题(这会产生警告,因为它是不必要的),或者覆盖“TestImpl”中的方法(由于相同的原因,这也会产生警告)。
那么为什么我不能调用超级方法呢?
我的猜测是,因为如果我能够直接调用“Test.super.doMagic()”,则在“TestImpl”中实现该方法将会破坏“SpecialTestImpl”的API,尽管事实并非如此。但是,如果让“SpecialTestImpl”实现“Test”接口并以此方式调用默认方法,情况仍然如此。

2
doMagic 不是静态方法,因此您无法静态地调用它。 - Buhake Sindi
看一下这个链接,虽然不是Java 8,但它涉及到了同样的概念,也是你出现这个特定错误的原因。可以参考Tagir的解决方案。 - Amr
3
那不是静态方法调用语法。 - Marko Topolnik
4个回答

11

这不是Eclipse的错误,而是预期的行为。只需使用super.doMagic();即可,它可以正常工作。您不能调用Test.super.doMagic(),因为稍后doMagic()可能会在TestImpl超类中重新实现。在这种情况下,TestImpl实现必须完全遮盖Test实现,使其无法访问。


1
它适用于他的情况,但如果TestIImpl也实现了该方法,他将无法访问防御者方法。我认为这是一个明智的设计选择。 - Marko Topolnik
2
@MarkoTopolnik,没错!同样地,你也不能调用在超类中被覆盖的超超类方法。 - Tagir Valeev
2
专注于细节而忽略整体,因为Eclipse将TestImpl.super.doMagic()作为该方法的标准实现(如果你问我的话,这确实是一个bug)。 :) - Stefan S.
1
@SteffiS。在这里我会同意你的看法。我也在使用Eclipse Luna,它也让我感到烦恼。 - Tagir Valeev
3
如果TestImpl是一个接口,那么TestImpl.super.doMagic()是正确的写法。 - Holger

4
尽管现在已经清楚原始代码被正确地拒绝了,但这还没有得到解决。正如你已经写过的那样,让SpecialTestImpl 直接实现 Test 突然允许你调用default 方法。
interface Test {
    default void doMagic() {
        System.out.println("Abracadabra");
    }
}
class TestImpl implements Test {
}
class SpecialTestImpl extends TestImpl implements Test {
    public void doMagic() {
        Test.super.doMagic(); // look, I can invoke that method
    }
}

有趣的是,当你将doMagic()的具体实现插入到TestImpl类中时,编译器不再接受这种解决方法(至少在使用javac时如此)。因此,这个“特性”似乎有点毫无意义,只要它与super.doMagic()相比没有任何影响,就可以使用这种调用方式。这是有意为之吗?
我查阅了规范并找到了以下内容:

§15.12.1. 编译时步骤1:确定要搜索的类或接口

  • 如果形式为TypeName . super . [TypeArguments] Identifier,则:

    • 如果 TypeName 既不表示类也不表示接口,则会出现编译时错误。

    • 否则,TypeName 表示要搜索的接口 I。

      让 T 成为方法调用所紧密包含的类型声明。如果 I 不是 T 的直接超级接口,或者存在一些其他直接超类或直接超接口 T,J,使得 J 是 I 的子类型,则会出现编译时错误。

所以,这里的TSpecialTestImplITestT有一个直接的超级父类TestImpl,使得TestImplTest的子类型。

因此,无论TestImpl是否实际上有一个doMagic()实现,这种解决方法都不应该可行,而应该引起编译时错误。因此,这似乎是一个涵盖了所有现有编译器实现的错误。


你的第一句话似乎需要进行一些语法清理 :)。还有什么问题没有解决?我们知道什么? - Stephan Herrmann
@Stephan Herrmann:在回答本问题时,只有一些回答指出了代码第一个版本能正确被拒绝的事实,但是在那个时间点上还没有人提到接着语句发生的事情——让类直接实现接口后却突然可以编译。我承认这个句子并不容易读懂,因为插入语声称第二个事实已经被OP写过了。好吧,“know”应该是“now”,但是我不太想为了单个字母修正而编辑帖子。我稍后会更新一下…… - Holger

4

这是有意设计的;你可以调用你直接的父级方法,但不能调用你的祖父级方法。

把它想象成类的类比:

class A { 
    void m() { }
}

class B extends A { ... }

class C extends B { 
    void m() { 
        super.m(); // OK
    }
}

想象一下,如果C可以直接调用A中的实现,而绕过B的实现,那将是完全错误的!B无法强制执行其表示不变量。这就是重写的含义——如果B重写来自A的方法,则只能从B中访问该被重写的方法。
默认超级调用的限制试图追踪此目标(尽管多重继承使得这更加棘手)。您可以调用您的直接超类,但不能通过尝试直接调用您的祖先来规避超级类。这同样会破坏此处重写的含义。

3

那我为什么不能调用超类方法呢?

因为实际上它并不是super类。 super只在直接子类中起作用,它指向父类。

正如你所说,只需让SpecialTestImpl实现Test接口并调用默认方法或在其父类TestImpl中调用已实现的方法即可。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接