2011年9月18日日曜日

依存 jar を変更したらどうなるか

あるアプリケーションがビルド時に参照していた ライブラリの jar を変更して、そのアプリケーション再コンパイルせずに実行したらどうなるか。

====
以下のようなライブラリ・コードを書き、コンパイルして foo.jar に丸めておく。
package p1;

public class Foo {
  public String bar() {
    return "hello";
  }
}
で、このライブラリを使う以下のようなクライアントコードを書いて、-cp に foo.jar を指定してコンパイルする。
package p2;

import p1.Foo;

public class Client {
  public static void main(String[] args) {
    System.out.println(new Foo().bar());
  }
}

コンパイルされたクラスを java コマンドで -cp に foo.jar を指定して実行すると、標準出力に hello と出力される。


さて、ここで クラス Foo を変更して、つまり foo.jar の中身を変えて、再コンパイルせずに再度 Client#main() を実行したらどうなるか。

(1) メソッドの中身を変えてみる

  public String bar() {
    return "good-bye";
  }
問題無し。標準出力に good-bye と表示される。

(2) メソッドの名前を変えてみる

  public String Bar() {
    return "hello";
  }
メソッド p1.Foo.bar()Ljava/lang/String が見当たらないって事で、NoSuchMethodError が送出される。リフレクションのコーディングでよく catch したりするNoSuchMethodExceptionではなく、NoSuchMethodError が投げられてきた。

(3) メソッドの引数を変えてみる

  public String Bar(String s) {
    return "hello";
  }
これは (2) と同じ

(4) メソッドの戻り値を変えてみる

  public Object Bar() {
    return "hello";
  }
これも (2) と同じ。戻り値も識別される。

(5) throws を追加してみる

  public String bar() throws IOException {
    throw new IOException("test");
  }
これは意外にも普通に実行されて、IOExceptionが送出されてスタックトレースされる。意外というのは、これを再コンパイルしようとすると、コンパイルエラーが出るからで、Client#main() に throws を追加するか、bar() を try-catch で囲むかしないと、コンパイルが通らない。でも、コンパイルは通らなくても実行時のメソッド呼び出しは成功する。

(6) メソッドの可視性を変えてみる

  private String bar() {
    return "hello";
  }
これは IllegalAccessError が送出される。つまり、一応 p1.Foo.bar()Ljava/lang/String の存在は識別された上で、アクセスに失敗して例外が発生した模様。

(7) クラスの可視性を変えてみる

class Foo {
…
(6) と同様だが、クラス p1.Foo の存在を識別したあとに、アクセスするところで失敗している。

(8) 定数を変えてみる

メソッドは以上のような感じで、フィールドもだいたい想像がつく。で、ここでちょっと定数を試してみる事にする。

まずライブラリコードを以下のように変える。

package p1;

public class Foo {
  public static final int C = 100;
}
次に、クライアントコードを以下の様に変える
package p2;

import p1.Foo;

public class Client {
  public static void main(String[] args) {
    System.out.println(Foo.C);
  }
}

両方コンパイルして実行すると、標準出力に100が実行される。

ここでライブラリコードを以下の様に修正し、jar を作り直す。

  public static final int C = 200; 
で、クライアントコードを再実行すると、標準出力に 200 が出力されると思いきや、実際には変更前と同じ100が出力される。
javap で見てみると、バイトコードに定数 100 が埋め込まれているのがわかる。なるほど定数はこういう扱いらしい。
public static void main(java.lang.String[]);
  Code:
   0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3: bipush 100
   5: invokevirtual #3; //Method java/io/PrintStream.println:(I)V
   8: return 

(9) null値 を試してみる

ライブラリ・コード を以下のように変更して、両方とも再コンパイルして実行してみる。
package p1;

public class Foo {
  public static final String C = null;
}
標準出力には、null が出力される。

ここでライブラリ・コードを、public static final String C = "a"; に変更して、クライアントを実行してみる。

(8) の結果から、null が出力される結果を類推してしまうが、実際には "a" が出力される。理由は、Java 言語仕様として null 定数として扱われないためコンパイル時には byte コードに直接書き込まれず、実行時に初めて Foo.Cの値を読むことになるかららしい。javap は以下のようになる。
public static void main(java.lang.String[]);
  Code:
   0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3: getstatic #3; //Field p1/Foo.C:Ljava/lang/String;
   6: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   9: return

0 件のコメント:

コメントを投稿