自作クラスローダーによるキャストの制限

[twitter:@pato_taityo]さんが書いてくださったJJUG ナイト・セミナー「Neil Bartlett氏によるOSGi講座」レポートの補足です。

こんな記載がありました。

若干メモが怪しいのですが、制約もあるとの事でした。モジュールごとにクラスローダーが異なるので、依存モジュールが同じであっても別のクラスとしてロードされており、モジュールをまたがって依存モジュールを操作しようとするとおかしくなる、ということを説明されていたように思います。
裏付けとなる資料が見つからず、申し訳ないです。

Javaではクラス名の衝突を防ぐためにパッケージを使用していることがよく知られています。
クラスを作成するときに一番上につけているやつですね。

パッケージとクラス名がすべて等しければ同じクラスとなると入門書には書いてありますが、実は違います。
正確には同じクラスローダー内でパッケージとクラス名がすべて等しければ同じクラスとなります。

クラスローダーとは

Javaのクラスファイルを実ファイルからメモリ上にロードするものですね。
そのままだ。

実際にコードを書いてみる

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class Main {
    public static void main(String... args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader m1 = new MyClassLoader();  //自作クラスローダをインスタンス化
        MyClassLoader m2 = new MyClassLoader();  //自作クラスローダをインスタンス化
        Class o1 = m1.findClass("Main");
        Class o2 = m2.findClass("Main");
        System.out.println("o1.getName() == o2.getName():" + o1.getName().equals(o2.getName())); // same: o1.getName() == o2.getName():true
        System.out.println("o1 == o2:" + (o1 == o2)); //not equals: o1 == o2:false

    }
}

class MyClassLoader extends ClassLoader {

    private static final int BUFFER_SIZE = 1024;

    protected Class findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = read(this.getClass().getClassLoader().getResourceAsStream(name + ".class"));
            return defineClass(name, data, 0, data.length);
        } catch (Throwable t) {
            throw new ClassNotFoundException(name, t);
        }
    }

    private static byte[] read(InputStream in) throws IOException {
        try{
            in = new BufferedInputStream(in, BUFFER_SIZE);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            byte[] buf = new byte[BUFFER_SIZE];
            for (int readBytes = in.read(buf);
                 readBytes != -1;
                 readBytes = in.read(buf)) {
                out.write(buf, 0, readBytes);
            }
            return out.toByteArray();
        } finally {
            if (in != null)
                in.close();
        }
    }
}

さくさくっと説明すると、自作クラスローダーを二つ作ってみてそれぞれでクラスをロードしている。
で、パッケージ名(今回ないけど)とクラス名が一致しているのに、クラスオブジェクトとしては別。

ちなみに、mainメソッドの最後に以下のようなコードを書くと実行時エラーになります。

Main m = (Main) o1.newInstance();

Exception in thread "main" java.lang.ClassCastException: Main cannot be cast to Main

MainがMainにキャストできないというカオスなメッセージとなります。
キャスト先のMainクラスは自作のクラスローダーからロードされているわけではなく、Java本体のシステムクラスローダーからロードされているからですね。

まあ、こんな感じの問題がOSGiでは発生するかもしれないということで。
こうなった場合にどう対応すればいいのかというとSerializableインターフェースを実装するという手がありますが、今回の件からは外れる気がするので気が向けば。

参考:http://blog.livedoor.jp/lalha_java/archives/50741760.html