最近、仕事でのトラブルからクラスローダー周りについて調べたことがあり、何気なく使っていた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イニシャライザの実行が行われます。