2012年2月27日

ありえないキャスト

こんな感じのクラスがあって、
public class HogeValue {

    private List fugaList = null;

    public void setFugaList(List fugaList) {
        this.fugaList = fugaList;
    }

    public List getFugaList() {
        return fugaList;
    }
}
こんな感じで使っていました。
        HogeValue hoge = createHoge();
        List fuga = hoge.getFugaList();

        for (Object o : fuga) {
            Map map = (Map) o;
            System.out.println("key1:" + map.get("key1"));
            System.out.println("key2:" + map.get("key2"));
        }
思わずなんじゃこりゃ、と言ってしまいました。 List型FugaValueを無視して、Objectでfor-eachのループを回し、中でMapにキャストしています。 「ClassCastException発生しないの?落ちないの?」 と聞いたところ、「落ちません、Listの中はなんかMapになるのです」 なんて言われました。 腑に落ちない。。。。

でも、実際、動いちゃうんです、このソース。

動くのを見て、ようやく気付きました。 総称型ってコンパイルしたら消えるじゃーん。 なので、まず、コンパイルエラーにはならない。

じゃあ、ClassCastExceptionは?

このHogeValueに値を設定しているのは、StrutsのActionFormでした。 詳しく調べていないけど、BeanUtilsとかでリフレクション使って設定してるんでしょうな。 だから、コンパイルエラーにもなりません。 で、BeanUtilsでMapがセットされるようになってた、と。
こんなこともできるんだな、とちょっと関心(?)しました。

2012年2月4日

コンパイル時のtargetオプション

世の中には不思議な事も多々ありまして。

先日、
「検証環境に乗っけるときに、Javaのバージョンが1.6だと動かなかったので、1.5でコンパイルしてライブラリちょうだい。」
なんて言われてしまいました。(検証環境に乗せるまで、誰も使用するJavaのバージョンを聞いても教えてくれないプロジェクトがこの世に存在するのです。)

1.5準拠に変更してコンパイルエラーとなるのは、interfaceのメソッドに対して「@Override」アノテーションをつけているものだけだったので、コンパイル時に「-targetオプション」だけ1.5にして、とりあえずお茶を濁しました。

そもそもこの「-targetオプション」って何なんでしょう?
ということで調べてみました。

まず、javacのドキュメントにはこうあります。
-target version
指定されたバージョンの VM をターゲットにしたクラスファイルを生成します。このクラスファイルは、指定されたターゲット以降のバージョンでは動作しますが、それより前のバージョンの VM では動作しません。有効なターゲットは、1.1、1.2、1.3、1.4、1.5 (5 も可)、および 1.6 (6 も可) です
つまりは、指定したバージョン以降で動作します、ってことですね。

以下の何の面白みもないソースで試してみます。

public class Test {
    public static void main(String[] args) {
        System.out.println("もじれつ");
    }
}
コンパイルして、javap.exeを使ってみます。

まずは1.6。
C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\javac.exe" Test.java

C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\javap.exe" -v Test
Compiled from "Test.java"
public class Test extends java.lang.Object
  SourceFile: "Test.java"
  minor version: 0
  major version: 50
(略)
1.6と1.5で実行してみます。
C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\java.exe" Test
もじれつ

C:\>"C:\Program Files\Java\jdk1.5.0_21\bin\java.exe" Test
Exception in thread "main" java.lang.UnsupportedClassVersionError: Bad version number in .class file
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:620)
        at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)
        at java.net.URLClassLoader.defineClass(URLClassLoader.java:260)
        at java.net.URLClassLoader.access$100(URLClassLoader.java:56)
        at java.net.URLClassLoader$1.run(URLClassLoader.java:195)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:268)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
        at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)

1.5ではUnsupportedClassVersionErrorが投げられてしまいました。

次にソースを1.5に指定してみます。
C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\javac.exe" -target 1.5 Test.java

C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\javap.exe" -v Test
Compiled from "Test.java"
public class Test extends java.lang.Object
  SourceFile: "Test.java"
  minor version: 0
  major version: 49
(略)
major versionが49になりました。

同じように実行。
C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\java.exe" Test
もじれつ

C:\>"C:\Program Files\Java\jdk1.5.0_21\bin\java.exe" Test
もじれつ

どちらも実行できました。
スバラシイ!

今回問題になった、@Overrideアノテーションをつけていた場合はどうなるのでしょう?
実装メソッドに@Overrideがついているのがポイントです。
public interface TestInterface {
    void test();
}
public class TestInterfaceImpl implements TestInterface {
    @Override
    public void test() {
        System.out.println("Test!");
    }
}
public class Test {
    public static void main(String[] args) {
        TestInterface test = new TestInterfaceImpl();
        test.test();
    }
}
これをtarget指定でコンパイルして、1.5のJVMで実行してみます。
C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\javac.exe" -target 1.5 *.java

C:\>"C:\Program Files\Java\jdk1.5.0_21\bin\java.exe" Test
Test!

エラーとならず実行できました。

と、ここまで書いて気付いたのですが、@Overrideはコンパイル時だけ有効なアノテーションなので、コンパイル後は消えてしまうんでしたっけ。あまり意味の無い検証でしたね。。。

じゃあ、1.5に無いメソッドを使ったらどうなる?ということで、試してみます。

またもや面白みの無いソース。1.6から導入されたString#isEmpty()で試してみます。
public class Test {
    public static void main(String[] args) {
        System.out.println("string".isEmpty());
    }
}
当然、1.5でコンパイルすると、コンパイルエラーになります。
C:\>"C:\Program Files\Java\jdk1.5.0_21\bin\javac.exe" Test.java
Test.java:5: シンボルを見つけられません。
シンボル: メソッド isEmpty()
場所    : java.lang.String の クラス
        System.out.println("string".isEmpty());
                           ^
エラー 1 個

じゃあこれを同じ様にコンパイル。
C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\javac.exe"-target 1.5 Test.java

C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\javap.exe" -v Test
Compiled from "Test.java"
public class Test extends java.lang.Object
  SourceFile: "Test.java"
  minor version: 0
  major version: 49
(略)

major versionが49なので、1.5用にコンパイルされています。

そして1.5で実行。
C:>"C:\Program Files\Java\jdk1.5.0_21\bin\java.exe"Test
Exception in thread "main" java.lang.NoSuchMethodError: java.lang.String.isEmpty()Z
        atTest.main(Test.java:5)
あらら。。。NoSuchMethodErrorが発生してしまいました。String#isEmpty()は1.6から導入されたため、当然といえば当然の結果ですが。。。

と、思ったら、Javacのドキュメントにちゃんと書いてありました。
Java プラットフォーム JDK の javac は、デフォルトでは、Java 2 SDK のブートストラップクラスに対してコンパイルを行うので、Java 2 SDK ではなく JDK 1.5 のブートストラップクラスに対してコンパイルを行うように指定する必要があります。これは、-bootclasspath および -extdirs を使って指定します。この指定を行わないと、1.5 VM には存在しない Java 2 プラットフォーム API に対応したコンパイルが行われるため、プログラムの実行時に障害が発生することがあります。
では、サンプルにあるとおり、-bootclasspath、-extdirs を指定してみます。
C:\>"C:\Program Files\Java\jdk1.6.0_30\bin\javac.exe" -target 1.5 -bootclasspath "C:\Program Files\Java\jdk1.5.0_21\jre\lib\rt.jar" -extdirs "" Test.java
Test.java:5: シンボルを見つけられません。
シンボル: メソッド isEmpty()
場所    : java.lang.String の クラス
        System.out.println("string".isEmpty());
                                   ^
エラー 1 個
ちゃんと(?)コンパイル時に怒られるようになりました。
要するに、ちゃんと目的のバージョンのrt.jar等を使ってコンパイルしろ、ってことですね。