Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

続続・クラスローダー

ついでにClassCastExceptionも出しておきました。ドッペルゲンガーと紹介されてるやつです。

内部でキャストのテストを行うcastMe()メソッドを持つインタフェースと、その実装クラスを定義します。

/* TestInterface.java */
public interface TestInterface{
	public void castMe(TestInterface object);
}

/* TestClass.java */
public class TestClass implements TestInterface{
	public void castMe(TestInterface object){
		System.out.println(object + "を");
		System.out.println(this + 
                    "のcastMe()メソッドでTestClassにキャストします。");
		TestClass tc = (TestClass) object;
		System.out.println("成功。");
	}
}

で、これを利用するクラスでは、通常のCLASSPATHベースのローダではなく、URLClassLoaderによってクラスをロードさせます。検索URLとして、カレントディレクトリ下のhoge1ディレクトリとhoge2ディレクトリを検索するローダーを、それぞれ一つずつ、計2つのローダを作ります。これらのローダは、CLASSPATHベースのローダの子となります。

import java.net.URLClassLoader;
import java.io.File;
import java.net.URL;
import java.net.MalformedURLException;

class Test{
	static public void main(String[] args){
		URL url1 = null;
		URL url2 = null;
		try{
                        // クラス検索URLの作成
			url1 = new File("hoge1").toURI().toURL();
			url2 = new File("hoge2").toURI().toURL();
		}catch(MalformedURLException mue){
			mue.printStackTrace();
			return;
		}

                // ローダーの作成
		ClassLoader cl1 = new URLClassLoader(
			new URL[] {url1}, 
			ClassLoader.getSystemClassLoader()
		);

		ClassLoader cl2 = new URLClassLoader(
			new URL[] {url2}, 
			ClassLoader.getSystemClassLoader()
		);

		TestInterface hoge1 = null;
		TestInterface hoge2 = null;
		try{
                        // それぞれのローダーで実装クラスをロード
			Class clas1 = cl1.loadClass("TestClass");
			hoge1 = (TestInterface) clas1.newInstance();
			Class clas2 = cl2.loadClass("TestClass");
			hoge2 = (TestInterface) clas2.newInstance();
		}catch(ClassNotFoundException e){
			e.printStackTrace();
			return;
		}catch(InstantiationException e){
			e.printStackTrace();
			return;
		}catch(IllegalAccessException e){
			e.printStackTrace();
			return;
		}

                // それぞれのローダーで作ったクラス同士で、キャストのテスト
		hoge1.castMe(hoge1);
		hoge2.castMe(hoge2);
		hoge1.castMe(hoge2);

	}
}

まずは、全部のクラスをカレントに置いて実行してみます。hoge1とhoge2を検索するローダーですが、親にCLASSPATHベースのローダーが居るので、hoge1とhoge2ディレクトリに目的のクラスがなくても無事に検索出来ます。

$ java Test
TestClass@50e1fを
TestClass@50e1fのcastMe()メソッドでTestClassにキャストします。
成功。
TestClass@be2358を
TestClass@be2358のcastMe()メソッドでTestClassにキャストします。
成功。
TestClass@be2358を
TestClass@50e1fのcastMe()メソッドでTestClassにキャストします。
成功。

次に、TestClassをhoge1とhoge2の両ディレクトリに移動します。CLASSPATHから見える位置に置くとそちらが優先されるので、見えないようにしておいて実行します。

$ java Test
TestClass@a2961bを
TestClass@a2961bのcastMe()メソッドでTestClassにキャストします。
成功。
TestClass@a3bcc1を
TestClass@a3bcc1のcastMe()メソッドでTestClassにキャストします。
成功。
TestClass@a3bcc1を
TestClass@a2961bのcastMe()メソッドでTestClassにキャストします。
Exception in thread "main" java.lang.ClassCastException: TestClass
        at TestClass.castMe(TestClass.java:5)
        at Test.main(test.java:48)

無事に(?)ドッペルゲンガーが出ました。TestClass→TestClassのキャストですが、ローダーが違うために別物*1扱いされています。


余談。通常のローダーは親を優先する性質があるのでここまで面倒なことをしないとClassCastExceptionを再現できないですが、tomcat等のように子を優先するローダーを使うと、この現象はもっと簡単に起きると思います。上の例で具体的に言うと、仮に子が優先のローダーでこの実験をすると、CLASSPATHからTestClassを除かなくてもhoge1やhoge2に別のTestClass実装を置いただけでこの例外が起こるようになります。

*1:この場合は継承関係がないって意味の別物