为什么嵌套的子类可以访问父类的私有成员,但孙子类却不能?

52
可能与这个问题类似:为什么外部Java类可以访问内部类的私有成员? 或者 使用子类中的super关键字访问超类私有字段

但是有一些区别:子类可以访问其父类(仅限于最近的父类)的私有成员。

给定下面的示例代码:

public class T {

    private int t;

    class T1 {
        private int t1;

        public void test() {
            System.out.println(t);
        }
    }

    class T2 extends T1 {

        private int t2;

        public void test() {
            System.out.println(t);
            System.out.println(super.t1);
            System.out.println(this.t2);
        }
    }

    class T3 extends T2 {

        public void test() {
            System.out.println(t);
            System.out.println(super.t1); // NG: t1 Compile error! Why?
            System.out.println(super.t2); // OK: t2 OK
        }
    }
}

4
你混淆了两个不同的概念:类嵌套(内部类)和子类化。但这是一个非常有趣的问题。T3不能访问super.t1是有道理的,因为T3的super没有t1。我必须承认我不理解为什么T3可以访问t2。内部类很奇怪。 :-) - T.J. Crowder
1
@T.J.Crowder 是的,但为什么 T2 可以访问 t1,而只有 T3 无法访问 t1 - andyf
10
在大多数国家,儿童触及父母私处都是违法行为。 - DevNewb
5
@DevNewb那个是不必要的... - DividedByZero
4
但仍然很有趣。 - Jared Smith
显示剩余2条评论
3个回答

52

很棒的例子!但实际上这是一个有些无聊的解释 - 没有可见性问题,你只是没有办法直接从T3引用t1,因为super.super是不被允许的

T2不能直接访问它自己的t1字段,因为它是私有的(子类不继承其父类的私有字段),但是super实际上是T1的一个实例,因此在同一类中,T2可以引用super的私有字段。只是没有机制让T3直接访问其祖父类T1的私有字段。

这两个在T3中都可以编译通过,这证明了T3能够访问其祖父类的private字段:

System.out.println(((T1)this).t1);
System.out.println(new T1().t1);

相反,在T2T3中都无法编译:

System.out.println(t1);
如果允许使用 super.super,那么您就可以从 T3 中进行此操作。
System.out.println(super.super.t1);
如果我定义了三个类A、B、C,其中A具有一个受保护的字段t1,并且B继承自A,C继承自B,那么C可以通过调用super.t1来引用A的字段t1,因为它在这里可见。逻辑上说,即使字段是私有的,内部类继承应该也是相同的,因为这些私有成员应该由于在同一类中而可见。但如果t1是private,则存在问题,因为类没有意识到其父类的私有字段,因此不能直接引用它们,尽管在实践中它们是可见的,这就是为什么您必须从T2中使用super.t1,以便甚至能够引用相关字段。虽然对于T3来说似乎没有t1字段,但通过处于同一外部类中,它可以访问T1的私有字段。因此,您只需要将this转换为T1,就可以引用私有字段了。在T2中的super.t1调用(本质上)是将this转换为T1,让我们能够引用其字段。

9
所有这些类都可以访问彼此的“private”字段,因为它们都在同一个外部类中。 - dimo414
@dimo414 但是对于普通的继承,我可以通过调用 super.t1 在每个继承点引用 t1。为什么这里表现不同呢? - SomeJavaGuy
1
什么是“正常继承”?如果这些都在单独的文件中,即使通过super也无法访问父类的私有字段。 - dimo414
如果我定义了3个类,ABC,其中A有一个protected字段t1B继承自AC继承自B,那么C可以通过调用super.t1来引用At1,因为它在这里是可见的。逻辑上讲,即使字段是私有的,内部类继承也应该适用于相同的情况,因为这些私有成员应该由于在同一个类中而可见。或者我在这里有逻辑错误吗? - SomeJavaGuy
2
@KevinEsche 的区别在于:声明为 protected 的成员是可以被继承的,而声明为 private 的成员则不行。这意味着一个字段 protected int t1 也是 B(或者像例子中的 T2)的成员,因此在 C(或者 T3)中使用 super.t2 是允许的。 - Seelenvirtuose
显示剩余2条评论

15

合成访问器方法

从技术上讲,在JVM级别,您无法访问另一个类的任何private成员 - 无论是封闭类(T.t)的成员还是父类(T2.t2)的成员。在您的代码中,这只是看起来像可以,因为编译器在被访问的类中为您生成synthetic访问器方法。当您在T3类中使用正确的形式((T1) this).t1修复无效引用super.t1时,也会发生相同的情况。

借助这样一个由编译器生成的synthetic访问器方法,您通常可以访问外层(顶级)T类中嵌套的任何类的private成员,例如从T1可以使用new T2().t2。请注意,这适用于private static成员。

JDK 1.1版本引入了synthetic属性来支持嵌套类,这是当时Java的一种新语言特性。从那时起,JLS明确允许在顶级类中相互访问所有成员,包括private成员。

但出于向后兼容性考虑,编译器会展开嵌套类(例如为T$T1T$T2T$T3),并将private成员访问转换为对生成的synthetic访问器方法的调用(因此这些方法需要具有包私有默认可见性):

class T {
    private int t;

    T() { // generated
        super(); // new Object()
    }

    static synthetic int access$t(T t) { // generated
        return t.t;
    }
}

class T$T1 {
    private int t1;

    final synthetic T t; // generated

    T$T1(T t) { // generated
        this.t = t;
        super(); // new Object()
    }

    static synthetic int access$t1(T$T1 t$t1) { // generated
            return t$t1.t1;
    }
}

class T$T2 extends T$T1 {
    private int t2;

    {
        System.out.println(T.access$t((T) this.t)); // t
        System.out.println(T$T1.access$t1((T$T1) this)); // super.t1
        System.out.println(this.t2);
    }

    final synthetic T t; // generated

    T$T2(T t) { // generated
        this.t = t;
        super(this.t); // new T1(t)
    }

    static synthetic int access$t2(T$T2 t$t2) { // generated
        return t$t2.t2;
    }
}

class T$T3 extends T$T2 {
    {
        System.out.println(T.access$t((T) this.t)); // t
        System.out.println(T$T1.access$t1((T$T1) this)); // ((T1) this).t1
        System.out.println(T$T2.access$t2((T$T2) this)); // super.t2 
    }

    final synthetic T t; // generated

    T$T3(T t) { // generated
        this.t = t;
        super(this.t); // new T2(t)
    }
}

注意:您不允许直接引用synthetic成员,因此在源代码中,您不能自己使用例如int i = T.access$t(new T());


谢谢,直到看到你的回答我才知道synthetic这个概念。 - andyf
合成方法是编译器的实现细节,不是语言规范的一部分。它们是必要的,因为嵌套类(以及它们具有的不寻常的可见性特权)直到Java 1.2才被引入,并且需要与先前的字节码一起工作。合成的access$t1()方法使t1T2T3都可访问,因为它是包私有的。如果编译器想要,它可以在T3中添加对access$t1()的调用。 - dimo414
1
我的观点是,synthetic 方法的存在并不能解释 OP 的问题。 - dimo414
1
@dimo414:没错,它本身并没有,但是你的回答已经解决了问题。我的回答是补充说明。 - charlie
1
@dimo414 你的帖子真正回答了这个问题。但我真的很高兴能够获得可视化编译器内部的链接,因为“合成”的内容可以通过反射看到。 - A.H.
显示剩余3条评论

7
非常好的发现!我认为,我们都假设你的代码示例应该编译。
不幸的是,事实并非如此......而JLS§15.11.2. "使用super访问超类成员"中给出了答案(重点是我的):

假设在类C中出现一个字段访问表达式super.f,并且C的直接超类是S类。如果从类C可以访问S中的f(§6.6),则super.f被视为在类S的主体中使用this.f表达式。否则,会发生编译时错误。

可访问性是由于所有字段都在同一封闭类中。它们可以是私有的,但仍然是可访问的。
问题在于,T3直接 父类 T2 中将 super.t1 视为 this.t1 是非法的 - 在 T2 中没有字段 t1。因此编译器会报错。

谢谢你的回答,它解释了super的工作原理。但我接受另一个答案,因为代码System.out.println(((T1)this).t1);更容易理解。 - andyf

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