CLOVER🍀

That was when it all began.

引数違いのClass#forNameの挙動を確認する

最近、仕事でのトラブルからクラスローダー周りについて調べたことがあり、何気なく使っていたClass#forNameとかClassLoader#loadClassの挙動について1度確認したくなりました。

そういえば、Class#forNameってforName(String)とforName(String, boolean, ClassLoader)がありますよね?

というわけで、簡単な例で確認してみましょう。用意したのは、以下のようなサンプルコード。

ロードされるクラス(C.java)

package test;

public class C {
    static {
        System.out.println("This is C StaticInitializer Block");
    }

    {
        System.out.println("This is C Initializer Block");
    }

    public C() {
        System.out.println("This is C Constructor Block");
    }
}

staticイニシャライザ、初期化ブロック、コンストラクタにそれぞれprintlnを仕込んでいます。

単純なクラスローダー(SimpleClassLoader.java)。

package test;

public class SimpleClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        System.out.println("loadClass[" + name + "]");
        return super.loadClass(name);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        System.out.println("loadClass[" + name + "] resolve[" + resolve + "]");
        return super.loadClass(name, resolve);
    }
}

メインクラス(ClassLoadSample.java)。

package test;

public class ClassLoadSample {
    public static void main(String[] args) {
        try {
            System.out.println("Load Class Start");
            SimpleClassLoader cl = new SimpleClassLoader();

            Class<?> c;
            /** Class Load Code Here... **/

            System.out.println("Loaded Class[" + c + "]");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上記クラスの、以下の部分をいろいろと変えて実験してみましょう。

            Class<?> c;
            /** Class Load Code Here... **/

Class#forName(String)

最も単純な例。

            System.out.println("Load Class Start");
            SimpleClassLoader cl = new SimpleClassLoader();

            Class<?> c;
            c = Class.forName("test.C");

            System.out.println("Loaded Class[" + c + "]");

実行結果。

Load Class Start
This is C StaticInitializer Block
Loaded Class[class test.C]

当然のことながら、Class#forNameに成功していますが、この時にstaticイニシャライザが実行されています。

ちなみに、この時クラスのロードに使用されるクラスローダーですが、SunのAPIによると

Class.forName("Foo")

は

Class.forName("Foo", true, this.getClass().getClassLoader())

と同義だ、と書いてあります。つまり、Class#forNameを呼び出したクラスをロードしたクラスローダーが使われる、と。

これはどういうトリックなのか?ということですが、java/lang/Class.javaのClass#forName(String)のソースを見ると

    public static Class<?> forName(String className) 
                throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());
    }

と書いてあります。ClassLoader#getCallerClassLoaderって?ということで、java/lang/ClassLoader.javaを見てみると

    static ClassLoader getCallerClassLoader() {
        // NOTE use of more generic Reflection.getCallerClass()
        Class caller = Reflection.getCallerClass(3);
        // This can be null if the VM is requesting it
        if (caller == null) {
            return null;
        }
        // Circumvent security check since this is package-private
        return caller.getClassLoader0();
    }

と書いてあります。3つ前の呼び出しスタックにいるクラスを取り出して、クラスローダーを引っ張ってくるってことですね、きっと。

Class#forName(String, boolean, ClassLoader)

今度は、第2引数に初期化有無、第3引数にクラスローダーを指定する版です。では、サンプル。まずは初期化有無をtrueに設定。

            System.out.println("Load Class Start");
            SimpleClassLoader cl = new SimpleClassLoader();

            Class<?> c;
            c = Class.forName("test.C", true, cl);

            System.out.println("Loaded Class[" + c + "]");

実行結果。

Load Class Start
loadClass[test.C]
loadClass[test.C] resolve[false]
This is C StaticInitializer Block
Loaded Class[class test.C]

引数に渡したクラスローダーの、loadClassメソッドを使用していることが確認できます。また、ロードされたクラスのstaticイニシャライザも実行されていますね。

続いて、初期化有無をfalseにした場合。

            System.out.println("Load Class Start");
            SimpleClassLoader cl = new SimpleClassLoader();

            Class<?> c;
            c = Class.forName("test.C", false, cl);


            System.out.println("Loaded Class[" + c + "]");

実行結果。

Load Class Start
loadClass[test.C]
loadClass[test.C] resolve[false]
Loaded Class[class test.C]

引数に渡したクラスローダーが使用されていることは変わりませんが、staticイニシャライザが実行されていませんね。つまり、初期化とは主にstaticに宣言された部分の初期化、となるんでしょうね。

ちなみに、以下のように変更すると

            System.out.println("Load Class Start");
            SimpleClassLoader cl = new SimpleClassLoader();

            Class<?> c;
            c = Class.forName("test.C", false, cl);

            System.out.println("Loaded Class[" + c + "]");

            c.newInstance();  // インスタンスの生成を付ける

実行結果としては、こうなります。

Load Class Start
loadClass[test.C]
loadClass[test.C] resolve[false]
Loaded Class[class test.C]
This is C StaticInitializer Block
This is C Initializer Block
This is C Constructor Block

初期化がコンストラクタを使用した呼び出しまで、遅延していますね。

ClassLoader#loadClass(String)

おまけです。こちらも一応確認してみましょう。

            System.out.println("Load Class Start");
            SimpleClassLoader cl = new SimpleClassLoader();

            Class<?> c;
            c = cl.loadClass("test.C");

            System.out.println("Loaded Class[" + c + "]");

結果は、こちら。

Load Class Start
loadClass[test.C]
loadClass[test.C] resolve[false]
Loaded Class[class test.C]

ClassLoader#loadClass(String)では、クラスを読み込んでもstatic領域の初期化は行われないようですね。もちろん、このあとClass#newInstanceとか実行すると、staticイニシャライザの実行が行われます。