Java ClassLoader是否会加载内部类?

27

如果我有一个内部类声明,例如:

Class A {
    public static class B {
    }
}

接着:

Class<?> implClass = getClass().getClassLoader().loadClass("A");

A$B内部类是否也会被加载? 如果B内部类没有声明为“static”,会发生什么?


任何静态初始化块中的代码都会在类加载时运行,因此这应该很容易测试。 - Alex
http://ideone.com/dalGQ6 - jmj
此外,不要将加载类与初始化(解析)类混淆。 - Sotirios Delimanolis
3
当需要一个类时,或者你请求它时,该类就会被加载。即使类是嵌套的,这个原则也不会改变。 - Peter Lawrey
5个回答

34

代码编译后,内部类就不存在了。如果查看javac的结果,你会看到两个文件:

A.class
A$B.class

所以当加载 A 时,并不会加载类 B,只是恰好在 A 中定义了 B


编辑

例如,给定以下两个文件:

package kuporific;

public class A {
    private static class B {}
    private class C {}
}

还有一个 build.gradle 文件(为了方便起见):

apply plugin: 'java'

首先,通过运行gradle build来构建。然后,解压缩生成的JAR文件(位于build/libs目录中):

├── META-INF
│   └── MANIFEST.MF
└── kuporific
    ├── A$B.class
    ├── A$C.class
    └── A.class

打开每个文件(例如在IntelliJ中),可以看到编译器所做的工作:

  • A.class

package kuporific;

public class A {
    public A() {
    }

    private class C {
        public C() {
        }
    }

    private static class B {
        public B() {
        }
    }
}
  • A$B.class:

    package kuporific;
    
    class A$B {
        private A$B() {
        }
    }
    
  • A$C.class:

    package kuporific;
    
    import kuporific.A;
    
    class A$C {
        private A$C(A this$0) {
            this.this$0 = this$0;
        }
    }
    
  • 注意:

    1. A$B 没有指向其父类 A 的引用,而 A$C 有指向,这是因为前者是静态内部类,后者不是;
    2. A$BA$C 现在都是包私有类。

    这就是非静态内部类能够直接引用其父实例的字段和方法,反之亦然。(在内部类中引用的任何父类私有字段也将变为包私有字段。)

    接下来,让我们看看加载类 AA$BA$C 有什么影响。

    首先,添加以下 Java 类:

    package kuporific;
    
    public class Main {
        public static void main(String[] args) throws ClassNotFoundException {
            Main.class.getClassLoader().loadClass("kuporific.A");
        }
    }
    

    现在在build.gradle文件中添加以下内容:

    apply plugin: 'application'
    mainClassName = 'kuporific.Main'
    applicationDefaultJvmArgs = ["-verbose:class"]
    

    -verbose:class选项会输出JVM加载的所有类(请参见Java - Get a list of all Classes loaded in the JVM)。

    在命令行中运行gradle run(这将运行Mainmain方法),输出结果(带有我的附加注释)如下:

    :compileJava
    :processResources UP-TO-DATE
    :classes
    :run
    [Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk/Contents/Home/jre/lib/rt.jar]
    # Lots of omitted output...
    [Loaded kuporific.Main from file:/tmp/build/classes/main/]
            ^ here!
    [Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded kuporific.A from file:/tmp/build/classes/main/]
            ^ here!
    [Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk/Contents/Home/jre/lib/rt.jar]
    [Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk1.8.0_20.jdk/Contents/Home/jre/lib/rt.jar]
    
    BUILD SUCCESSFUL
    
    Total time: 6.502 secs
    

    我们可以看到 kuporific.Mainkuporific.A 被加载了,但没有看到 kuporific.A$B 或者 kuporific.A$C 被加载。


    不完全准确。 A.class 仍然保留 A$BA 的内部类的信息。您可以通过反射访问此信息。 - ghik
    @ghik 确实如此,但加载类 A 不会加载内部类 B,无论是静态还是非静态。我已经编辑了我的答案以更好地解释这一点。 - kuporific
    @ghik 编译器执行一些微妙的魔法来使内部类的语义工作(这是您所说的“不完全相同”)。 - kuporific
    但是调用getEnclosingClass() A$B仍然会加载A。因此,这些信息必须存在某个地方。它是否在名称中? - user502187
    请问您能否提供相关的官方文档呢? - Arun

    3

    内部类,即B类不能存在于父类之外。您需要首先构造父类A类

    如果您从内部类中删除static,即对于非静态内部类,您需要在内部类构建期间传递父类。

    Object a = Class.forName("A").newInstance();    //object of outer class
    
    //object of inner class
    Object b = implClass.getDeclaredConstructor(new Class[] { a.getClass() })
            .newInstance(new Object[] { a });
    

    1
    我认为这并不适用于静态内部类。 - dkatzel
    @dkatzel 这个解决方案主要针对非静态内部类,而问题涉及到静态内部类非静态内部类两者。 - rachana
    好的,我现在明白了。如果您能更清楚地表明代码仅适用于非静态情况,我将取消踩下去的操作。(也许可以添加反射调用以说明如何调用静态内部类?) - dkatzel

    2
    下面的代码可以运行,可以说明其他答案中的一些内容:
    public class Outer
    {
    
       private static final String TEST01 = "I'm TEST01";
    
       static
       {
            System.out.println("1 - Initializing class Outer, where TEST01 = " + TEST01);
       }
    
       public static void main(String[] args)
       {
           System.out.println("2 - TEST01       --> " + TEST01 );
           System.out.println("3 - Inner.class  --> " + Inner.class);
           System.out.println("5 - Inner.info() --> " + Inner.info() );
       }
    
       private static class Inner
       {
    
           static
           {
              System.out.println("4 - Initializing class Inner");
           }
    
           public static String info()
           {
               return "I'm a method in Inner";
           }
        }
    }
    

    请注意序列号,特别是在这一行中:
    System.out.println("5 - Inner.info() --> " + Inner.info() );
    

    运行程序后,您将在控制台上看到以下结果:
    1 - Initializing class Outer, where TEST01 = I'm TEST01
    2 - TEST01       --> I'm TEST01
    3 - Inner.class  --> class com.javasd.designpatterns.tests.Outer$Inner
    4 - Initializing class Inner
    5 - Inner.info() --> I'm a method in Inner
    

    每个步骤的详细说明如下:
    1 - 在运行程序时初始化“Outer”。静态变量TEST01在静态块之前初始化。Inner没有被初始化。
    2 - 调用“main”方法并显示“TEST01”的值;然后,
    3 - System.out显示对“Inner”的引用。Inner没有被初始化,但它已经加载了(这就是为什么它在内存模型中有引用)。
    4 - 这是最有趣的部分。因为System.out需要访问“Inner”中的“info()”方法(Inner.info()),所以在返回“info()”方法的结果之前,“Inner”类应该被初始化这就是为什么这是第4步的原因。
    5 - 最后,System.out拥有显示所有数据所需的数据,然后在控制台上显示最后一行。
    因此,正如@sotirios-delimanolis(Java ClassLoader是否会加载内部类?)所指出的那样,加载类与初始化类不同。

    2
    一个 ClassLoader 只有在被请求时(例如使用 loadClass)才会加载类。在加载类时,ClassLoader 将请求引用的类。
    因为你的类 A 没有引用 A.B,无论它是静态的还是非静态的,A.B 都不会被加载。(实际上,A 确实引用了 A.B,但并没有以使 ClassLoader 加载 A.B 的方式引用它。)
    如果你添加一个类型为 A.B 的字段或以其他方式使用类型 A.B (例如作为方法返回类型),它将在 A.class 中被引用,因此会被加载。

    1
    不要混淆加载类与初始化(解析)类。ClassLoader#loadClass(String)不会初始化一个类。 - Sotirios Delimanolis

    0

    不会,无论哪种情况下嵌套类都不会被加载。


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