为什么Java不允许使用super.super.method();?

426
我阅读了这个问题,并认为如果能够编写以下代码,则可以轻松解决该问题(即使不使用此方法也可以解决):
@Override
public String toString() {
    return super.super.toString();
}

我不确定它在许多情况下是否有用,但我想知道为什么它不是这样,并且其他语言中是否存在类似的东西。

你们觉得呢?

编辑: 澄清一下:是的,我知道在Java中这是不可能的,我也不是很想要它。这不是我期望它能工作,而是惊讶于得到编译器错误。我只是有这个想法,想讨论一下。


8
想要调用super.super.toString()与你选择扩展一个类并接受它的所有特性的决定相矛盾,因此不建议这样做。 - DayaMoon
22个回答

540

这违反了封装原则。你不应该能够绕过父类的行为。有时候能够绕过自己类的行为是有意义的(特别是在同一个方法内),但不能绕过父类的行为。例如,假设我们有一个基础的“物品集合”,一个子类代表“红色物品的集合”,再有一个子类代表“大红色物品的集合”。这时,有以下情况是有意义的:

public class Items
{
    public void add(Item item) { ... }
}

public class RedItems extends Items
{
    @Override
    public void add(Item item)
    {
        if (!item.isRed())
        {
            throw new NotRedItemException();
        }
        super.add(item);
    }
}

public class BigRedItems extends RedItems
{
    @Override
    public void add(Item item)
    {
        if (!item.isBig())
        {
            throw new NotBigItemException();
        }
        super.add(item);
    }
}

很好 - RedItems始终可以自信地包含所有红色物品。现在假设我们能够调用super.super.add():

public class NaughtyItems extends RedItems
{
    @Override
    public void add(Item item)
    {
        // I don't care if it's red or not. Take that, RedItems!
        super.super.add(item);
    }
}

现在我们可以添加任何东西,而RedItems中的不变量被破坏了。

这有意义吗?


43
不错的例子。但是,我认为当基类接受项目而派生类拒绝时,这是不好的设计,因为派生类不能作为基类的替代品使用(违反了替代原则)。这种继承结构是否干净?我的想法正确吗? - Johannes Schaub - litb
5
你的意思是优先使用组合而不是继承吗?这个例子并不是避免使用继承的理由 - 虽然还有很多其他理由。 - Jon Skeet
12
我认为这违反了Liskov替换原则,因为如果按照Item的合同编码并使用RedItems的实例,则会收到意外的NotRedItemException。我一直被教导子类应该接受超集输入并返回子集输出。换句话说,子类不应拒绝超类有效的输入或产生超类无法产生的输出,这包括抛出超类未抛出的错误。 - Konstantin Tarashchanskiy
3
关于这个评论:“现在我们可以添加任何东西,而RedItems中的不变量被破坏了。”如果您在添加覆盖代码之前没有调用“super.add(item);”,那么这仍然是正确的。但我理解你的整体观点,并理解为什么允许从“add(item)”调用“super.super.add(item)”会是错误的。 - cosjav
5
有时是书籍,有时是博客文章,有时只是经验... - Jon Skeet
显示剩余21条评论

88

我认为Jon Skeet的答案是正确的。我只想补充一点,你可以通过将this进行类型转换来访问超类的超类中的被隐藏变量:

interface I { int x = 0; }
class T1 implements I { int x = 1; }
class T2 extends T1 { int x = 2; }
class T3 extends T2 {
        int x = 3;
        void test() {
                System.out.println("x=\t\t"          + x);
                System.out.println("super.x=\t\t"    + super.x);
                System.out.println("((T2)this).x=\t" + ((T2)this).x);
                System.out.println("((T1)this).x=\t" + ((T1)this).x);
                System.out.println("((I)this).x=\t"  + ((I)this).x);
        }
}

class Test {
        public static void main(String[] args) {
                new T3().test();
        }
}

以下是输出结果:

x=              3
super.x=        2
((T2)this).x=   2
((T1)this).x=   1
((I)this).x=    0

(以上示例摘自JLS)

然而,对于方法调用来说,这种方式并不适用,因为方法调用是基于对象的运行时类型确定的。


6
如果您在子类中有一个与父类中同名的变量,但由于某种原因您无法或不允许更改它,您仍然可以访问具有相同名称的父类变量。那么这有什么用呢?嗯……我从未使用过它。 - Michael Myers
这是一个很好的表达式文档链接,对于正在学习OCPJP的人来说有一些不错的例子。 - Gordon
T1类怎么会覆盖变量x?它默认是静态常量,对吧? - amarnath harish
@amarnathharish:它没有被覆盖,而且字段默认是包保护的非最终状态。 - Michael Myers
@MichaelMyers 是这样吗?那么为什么这个问题是有效的 https://dev59.com/TnI_5IYBdhLWcg3wJPZu - amarnath harish
显示剩余2条评论

47

我认为以下代码可以在大多数情况下使用super.super...super.method()。(即使这样做很丑陋)

简而言之

  1. 创建祖先类型的临时实例
  2. 原始对象复制字段的值到临时实例中
  3. 在临时对象上调用目标方法
  4. 将修改后的值复制回原始对象

用法:

public class A {
   public void doThat() { ... }
}

public class B extends A {
   public void doThat() { /* don't call super.doThat() */ }
}

public class C extends B {
   public void doThat() {
      Magic.exec(A.class, this, "doThat");
   }
}


public class Magic {
    public static <Type, ChieldType extends Type> void exec(Class<Type> oneSuperType, ChieldType instance,
            String methodOfParentToExec) {
        try {
            Type type = oneSuperType.newInstance();
            shareVars(oneSuperType, instance, type);
            oneSuperType.getMethod(methodOfParentToExec).invoke(type);
            shareVars(oneSuperType, type, instance);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    private static <Type, SourceType extends Type, TargetType extends Type> void shareVars(Class<Type> clazz,
            SourceType source, TargetType target) throws IllegalArgumentException, IllegalAccessException {
        Class<?> loop = clazz;
        do {
            for (Field f : loop.getDeclaredFields()) {
                if (!f.isAccessible()) {
                    f.setAccessible(true);
                }
                f.set(target, f.get(source));
            }
            loop = loop.getSuperclass();
        } while (loop != Object.class);
    }
}

10
通过反射,你可以做任何事情,没错 :) 你甚至可以使字符串可变。 - BalusC
26
做这种事情真是太可怕了!我给你点赞,只是因为你竟然想出了做法 :) - Larry Watanabe
6
这是一个不错的技巧,但它并不总是等同于调用不可用但需要的super.super,因为super.super调用将携带C(C + B + A)的上下文,而你的答案创建了一个没有B和C上下文的A实例。因此,如果每个doThat调用getContext()并且每个类中的getContext实现不同,那么这个答案将无法工作。在你的答案中,它将使用A的getContext(),而调用不可用的super.super将导致使用C的getContext。 - inor
嗯,在某些情况下,也许您可以通过动态代理(http://javahowto.blogspot.co.uk/2011/12/java-dynamic-proxy-example.html)来克服inor的异议,将方法调用重定向到原始对象(在每次调用后同步变量)?看起来代理需要实现接口的所有内容。而且,我想知道是否可能让超超级类调用其中一个超超级方法,并且您不需要重定向那些方法.... - Erhannis
1
我在另一条评论中说,阻止super.super.会引导程序员寻找新的、复杂的和极端错误的方法来绕过它,这是一个完美的例子,因为你的同事们可能会因为你写了这样的代码而非常讨厌你,以至于他们会亲自并真正地朝你的脚开枪。+1 - Braden Best

12

我声望不够高,无法发表评论,所以我会将这个回答添加到其他回答中。

Jon Skeet 给出了一个非常好的例子作为答案。Matt B也有一定道理:并非所有的超类都拥有超类。如果你调用了没有超类的超类的超级方法,你的代码就会出错。

面向对象编程(Java就是如此)是关于对象而不是函数的。如果你想要任务导向的编程,请选择C ++或其他语言。如果你的对象不适合它的超类,那么你需要将其添加到“爷爷类”,创建一个新类,或找到另一个适合它的超类。

个人认为,这种限制是Java最大的优点之一。与我使用过的其他语言相比,代码有些死板,但我总是知道该期望什么。这有助于实现Java的“简单和熟悉”的目标。在我看来,调用super.super并不简单或熟悉。也许开发人员也有同感?


3
你说“并非所有的超类都有超级超类”。嗯,除了java.lang.Object可以返回“null”之外,其他几乎都有超级超类。 - Tim Büthe
每个应用程序员编写的类都有一个“super”(java.lang.Object除外,但应用程序员不编写它)。 - finnw
2
如果有太多的"super",可以通过将超级父类的数量限制为编译时错误来轻松解决。考虑到Java只具有公共继承,如果有人更改了继承层次结构,则会更改接口。因此,我不会担心使用"super^n"会有任何问题。 - Thomas Eding

10
有一些好的理由来这样做。您可能拥有一个子类,其中包含一个实现不正确的方法,但父类方法被实现正确。因为它属于第三方库,您可能无法或不愿更改源代码。在这种情况下,您想创建一个子类,但覆盖一个方法以调用超级.超级方法。
正如其他帖子所示,可以通过反射来实现这一点,但应该可以做到类似于
(SuperSuperClass this).theMethod();
我现在正在处理这个问题-快速修复是复制并粘贴超类方法到子子类方法中 :)

1
在调用方法之前进行强制类型转换并不会改变委托的方法——始终使用子类的实现,而不是父类的。例如,请参见http://stackoverflow.com/questions/1677993/java-inheritance-resolution-in-case-of-instance-methods-and-variables/1678023#1678023 - Joshua Goldberg
2
@Larry 这正是我所遇到的情况,也是我使用的解决方法。干得好! - bcr
如果您有所需方法的代码,您可以打开所有必要的字段并在子类中运行此代码。 - Enyby

6
除了其他人提出的非常好的观点外,我认为还有另一个原因:如果超类没有超类会怎么样?
由于每个类自然地扩展(至少)Object,super.whatever()将始终引用超类中的方法。但是,如果您的类仅扩展Object-那么super.super将指向什么?应该如何处理这种行为-编译器错误、NullPointer等?
我认为不允许这样做的主要原因是违反了封装性,但这也可能是一个小原因。

4
显然,这将是编译器错误——而且这是编译器已经掌握的信息。 - Michael Borgwardt

5
我认为如果您要重写一个方法并且想调用它的所有超类版本(比如说,对于“equals”方法),你通常希望首先调用直接超类版本,因为它会依次调用其超类版本,如果需要的话。
我认为只有极少数情况下才有意义(如果有的话,我无法想到这种情况),去调用某个任意超类的方法版本。我不知道在Java中是否可能实现这一点。在C ++中可以做到:
this->ReallyTheBase::foo();

7
如果有人编写了一个子类,其中一个方法的实现不正确,但是超类的方法完成了90%的工作,那么你需要创建一个子类,并覆盖调用超类方法的方法,然后再加入自己的10%内容。 - Larry Watanabe

3

当你无法更改基类代码时,调用super.super.method()是有意义的。这通常发生在您扩展现有库时。

首先问问自己,为什么要扩展该类?如果答案是“因为我无法更改它”,那么您可以在应用程序中创建完全相同的包和类,并重写naughty方法或创建委托:

package com.company.application;

public class OneYouWantExtend extends OneThatContainsDesiredMethod {

    // one way is to rewrite method() to call super.method() only or 
    // to doStuff() and then call super.method()

    public void method() {
        if (isDoStuff()) {
            // do stuff
        }
        super.method();
    }

    protected abstract boolean isDoStuff();


    // second way is to define methodDelegate() that will call hidden super.method()

    public void methodDelegate() {
        super.method();
    }
    ...
}

public class OneThatContainsDesiredMethod {

    public void method() {...}
    ...
}

例如,您可以在应用程序中创建org.springframework.test.context.junit4.SpringJUnit4ClassRunner类,以便在从jar加载真实类之前加载此类。然后重写方法或构造函数。
注意:这是绝对的黑客技巧,强烈不建议使用,但它是有效的!使用这种方法是危险的,因为可能会出现与类加载器相关的问题。此外,每次更新包含被覆盖类的库时,都可能会出现问题。

3

请看这个 Github 项目,特别是 objectHandle 变量。这个项目展示了如何准确地在孙子对象上调用祖父方法。

以防链接失效,这里是代码:

import lombok.val;
import org.junit.Assert;
import org.junit.Test;

import java.lang.invoke.*;

/*
Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should.
Please don't actually do this... :P
*/
public class ImplLookupTest {
    private MethodHandles.Lookup getImplLookup() throws NoSuchFieldException, IllegalAccessException {
        val field = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
        field.setAccessible(true);
        return (MethodHandles.Lookup) field.get(null);
    }

    @Test
    public void test() throws Throwable {
        val lookup = getImplLookup();
        val baseHandle = lookup.findSpecial(Base.class, "toString",
            MethodType.methodType(String.class),
            Sub.class);
        val objectHandle = lookup.findSpecial(Object.class, "toString",
            MethodType.methodType(String.class),
            // Must use Base.class here for this reference to call Object's toString
            Base.class);
        val sub = new Sub();
        Assert.assertEquals("Sub", sub.toString());
        Assert.assertEquals("Base", baseHandle.invoke(sub));
        Assert.assertEquals(toString(sub), objectHandle.invoke(sub));
    }

    private static String toString(Object o) {
        return o.getClass().getName() + "@" + Integer.toHexString(o.hashCode());
    }

    public class Sub extends Base {
        @Override
        public String toString() {
            return "Sub";
        }
    }

    public class Base {
        @Override
        public String toString() {
            return "Base";
        }
    }
}

愉快的编码!!!


1
你们的科学家们太过于关注他们是否能够做到,而没有停下来思考他们是否应该这样做。请不要真的这样做... :P - Tim Büthe
是的,那些是程序员的话,不是我的。我个人认为,你将来可能真的需要它。 - kyay10

3

@Jon Skeet 很好的解释。 我认为,如果有人想调用super.super方法,则必须要忽略直接父元素的行为,但是想要访问祖先元素的行为。 可以通过 instance Of 来实现。如下所示:

public class A {
    protected void printClass() {
        System.out.println("In A Class");
    }
}

public class B extends A {

    @Override
    protected void printClass() {
        if (!(this instanceof C)) {
            System.out.println("In B Class");
        }
        super.printClass();
    }
}

public class C extends B {
    @Override
    protected void printClass() {
        System.out.println("In C Class");
        super.printClass();
    }
}

这是驱动程序类:

public class Driver {
    public static void main(String[] args) {
        C c = new C();
        c.printClass();
    }
}

这将产生的输出结果是:
In C Class
In A Class

这种情况下,Class B printClass的行为将被忽略。 我不确定这是否是实现super.super的理想或良好实践,但它仍然可以工作。


3
嗯,那很有创意,但并没有真正回答我的问题。C仍然不会调用super.super,B只是表现得不同而已。如果你可以改变A和B,你可以添加另一个方法而不是使用instanceof。在你无法访问A和B且无法更改它们的情况下,Super.super.foo将帮助你。 - Tim Büthe
同意@TimButhe的观点,但如果有人想要调用super.super,那么他/她有意忽略父类的行为,因此您只需要通过Java现有的语法来实现这一点。(您可以选择任何选项,无论是instanceof还是不同的方法) - Sanjay Jain
1
但是这需要超类B知道子类C的存在。因此,如果类B在第三方库中,则无法正常工作。 - Hicham Moustaid

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