内部类为何可以访问私有方法?

33

我不理解为什么这段代码可以编译通过。在内部类中,尽管 f() 和 g() 是私有的,但它们仍然是可见的。它们因为是内部类而受到特殊对待吗?

如果 A 和 B 不是静态类,情况仍然相同。

class NotPrivate {
    private static class A {
        private void f() {
            new B().g();
        }
    }

    private static class B {
        private void g() {
            new A().f();
        }
    }
}

1
就像已经说过的那样:根本原因在于私有修饰符是如何在JLS中指定的。“否则,如果成员或构造函数被声明为私有,则仅当访问发生在包含成员或构造函数的声明的顶级类(§7.6)的主体内时,才允许访问[访问]。”因此,“private”总是与封闭的顶级类相关联。您始终可以访问顶级类中的所有内容。 - A Dude
1
这里没有内部类。有静态嵌套类。 - user207421
4个回答

34

(编辑:扩展了答案以回答一些评论)

编译器会将内部类转换为顶级类。由于私有方法仅对内部类可用,因此编译器必须添加新的“合成”方法,其具有包级别访问权限,以便顶级类可以访问它。

大致如此($ 是编译器添加的):

class A 
{
    private void f() 
    {
        final B b;

        b = new B();

        // call changed by the compiler
        b.$g();
    }

    // method generated by the compiler - visible by classes in the same package
    void $f()
    {
        f();
    }
}

class B
{
    private void g() 
    {
        final A a;

        a = new A();

        // call changed by the compiler
        a.$f();
    }

    // method generated by the compiler - visible by classes in the same package
    void $g()
    {
        g();
    }
}

非静态类与普通类相同,但是它们还会包含对外部类的引用,以便可以在其中调用方法。

Java 采取这种方式的原因是他们不想要求 VM 支持内部类的更改,所以所有更改都必须在编译器级别完成。

编译器接收内部类并将其转换为顶层类(因此,在 VM 级别上不存在内部类)。编译器还必须生成新的“转发”方法。它们被制作在包级别(而不是公共级别),以确保只有在同一包中的类才能访问它们。编译器还更新了对私有方法的方法调用,使其指向生成的“转发”方法。

如果将方法声明为“包级别”(即没有 public、private 和 protected),则可以避免编译器生成该方法。但缺点是包中的任何类都可以调用这些方法。

编辑:

是的,你可以调用生成的(合成的)方法,但请不要这样做!

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Main
{
    public static void main(final String[] argv)
        throws Exception
    {
        final Class<?> clazz;

        clazz = Class.forName("NotPrivate$A");        

        for(final Method method : clazz.getDeclaredMethods())
        {
            if(method.isSynthetic())
            {
                final Constructor constructor;
                final Object instance;

                constructor = clazz.getDeclaredConstructor(new Class[0]);
                constructor.setAccessible(true);
                instance = constructor.newInstance();
                method.setAccessible(true);
                method.invoke(null, instance);
            }
        }
    }
}

1
那么,你将一个方法声明为私有的,编译器会将其转换为公共的,而不是生成错误? - cbrulak
好的,它不是公共的,而是包。是的,这就是它的作用。这是因为虚拟机对内部类没有任何了解...所以编译器会代替虚拟机完成所有工作,而不是让虚拟机做特殊的事情。 - TofuBeer
包是包含内部类的类吗? - cbrulak
包访问 -- 可被同一包(命名空间)中的所有类访问。它不会改变你的代码,而是添加了另一个带有 $ 的方法来调用你的代码。无法使用 Java 直接调用该方法,但可以通过生成的字节码或反射来调用,但我对此不确定。 - Lou Franco
这个答案讲述了编译器如何满足jvm-spec的要求,但并没有回答问题所问的为什么。 - A Dude
显示剩余6条评论

15

我认为这句话概括得很好:

......内部类可以访问声明类的所有成员,甚至是私有成员。实际上,内部类本身被视为类的一部分;因此,遵循面向对象工程的规则,它应该可以访问类的所有成员。

基于这个原因,由于两个内部类实际上只是包含类的一部分,它们也应该能够访问彼此的私有成员。


事实上,当编译您的类时,所有内部类实际上都会“折叠”成为包含类的一部分。-- 这是什么意思? - Michael Myers
我非常确定那不是真的。通常会创建类似 Outer$Inner 的类。你无法做到你所说的,因为内部类和外部类的对象数量可能不同。 - Lou Franco
你说得对,那有点误导人。我已经从我的回答中删除了那部分。 - Eric Petroelje
1
@Eric,现在的版本没有解释为什么它们会有访问权限 - 它们是兄弟类,而不是一个包含另一个的类。 - eglasius
@Freddy 他们是兄弟类,但同时在相同的包含类和作用域中。因此,它们可以像类中的一个私有方法调用另一个私有方法一样互相访问。 - Eric Petroelje

4
Java在特殊的访问器中编译了$。因此,您不能编写访问私有方法的Java代码。这里解释了原因:http://www.retrologic.com/innerclasses.doc7.html 编译器生成的成员还有一类。如果一个类C的私有成员m被另一个类D使用,那么如果一个类包含另一个类,或者它们都被一个公共类包含,那么就可以使用。由于虚拟机不知道这种分组方式,编译器在C中创建了一组本地的访问方法协议,以允许D读取、写入或调用成员m。这些方法的名称形式为access$0、access$1等。它们从不是公开的。访问方法的独特之处在于,它们可以添加到封闭类中,而不仅仅是内部类。

0

正如用户'A Dude'在被接受的答案评论中所解释的:

它被编译,是因为语言规范要求它以这种方式工作,即Java Lang Spec说得很清楚:


6.6.1 确定可访问性(至少从JLS6开始)

"否则,如果成员或构造函数声明为private,则只有在嵌套该成员或构造函数的顶级类(§7.6)的主体内部发生时才允许访问。"

也就是说,私有成员的“访问范围”是:在顶级类体的词法边界内部的任何地方。

那意味着:在外层类的类体内定义的所有私有成员都可以从此类体内的其他任何地方访问。

例如,内部类的私有方法可以从外部类的方法或外部类的任何内部类的方法中访问。


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