Java オートボクシングに気をつけろ

java を学んだ人にとって、「Stringの比較は、== ではなく、equalsメソッドを利用する」というのは、常識でしょう。

「==」はオブジェクトのインスタンスが同じかどうかを判定するんであって、インスタンスが同値かは「equals」を使って判定します。

ちょっとややこしいのは、文字列プールという仕組みがあるために、リテラルの文字列については、インスタンスを共有するので、次のように同じ”abc”をあらわす文字列でも、「==」で比較したときに、結果がtrueになったりfalseになったりする点でしょう。

    String a = “abc”;
    String b = “abc”;
    String c = new String(“abc”);
    System.out.println(a == b);   // これは、true
    System.out.println(a == c);   // これは、false

でも、equalsメソッドなら、文字列が表す値が同じならtrueを返してくれます。
なので、文字列の比較では「常に equalsメソッドを使う」という単純な戦略でオーケーです。

で、待望(?)のオートボクシング/アンボクシング機能を見てみますと、参照型で「==」はオブジェクトのインスタンスが同じかどうかの比較だったんですが、アンボクシングが行われるため、以下のようなプリミティブ型との比較の場合、同一インスタンスであるかどうかの比較ではなく、同じ値であるかという比較が行われます。

    int i1       = 77; // プリミティブ型
    Integer i2 = 77;
    System.out.println(i1 == i2); // true 同値

また、以下の例のように、参照型どうしを比較しても、trueが返ります。

    // インスタンスの比較1
    Integer i3 = 99;
    Integer i4 = 99;
    System.out.println(i3 == i4); // true

    Boolean b1 = true;
    Boolean b2 = true;
    System.out.println(b1 == b2); // true

すばらしい!今までプリミティブ型を使ってたところをクラスに置き換えちゃえ!富豪的プログラマ(?)を目指すなら、次のようなコードもためらわずにかけちゃうかもしれません。オブジェクト指向的にもベター(プリミティブ型の存在が欺瞞だったのだ!)のかも?

    Integer max   = 999;
    Integer case1 = 100;
    Integer case2 = 300;

    for (Integer i=0; i < max; i++) {
        if (i == case1) {
            :
        }
        if (i == case2) {
            :
        }
    }

しかし、ちょちょっとまってください。一見便利になったような「==」による比較ですが、簡単に信用するのは甘すぎました。上の例では、 case2 のブロックが実行されない可能性があるんです。

    // インスタンスの比較2
    Integer j1 = new Integer(99);
    Integer j2 = new Integer(99);
    System.out.println(j1 == j2); // false

    // -128 ~ 127 以外は false となる!!
    Integer j3 = 200;
    Integer j4 = 200;
    System.out.println(j3 == j4); // false

    // Boolean も同様!
    Boolean b3 = new Boolean(true);
    Boolean b4 = new Boolean(true);
    System.out.println(b3 == b4); // false

確かに、プリミティブ型と参照型との比較では、参照型がアンボクシングされてプリミティブ型どうしの比較と同じことになっていました。要するに、

   i1 == i2

というのは、

   i1 == i2.intValue(); // i2 がアンボクシングされる。

のシンタックスシュガーだったわけです。

けれども、実は、参照型どうしの比較の場合、アンボクシングは行われません。要するに今までどおりの同一インスタンスかを比較する「==」なのです。

ん?

    Integer i3 = 99;  // ボクシング
    Integer i4 = 99;  // ボクシング
    System.out.println(i3 == i4); // true

    Boolean b1 = true; // ボクシング
    Boolean b2 = true; // ボクシング
    System.out.println(b1 == b2); // true

さっき、このコードでは true になったやんけ~

なんと、実は、true, false, byte, char(\u0000 から \u007f), int もしくは short (-128 and 127) をボクシングにより作成した場合は、「==」を使った結果も等しくなる。。。という仕様なのです。

実際ソースコードを覗いてみると、例えば、Integer.IntegerCache というスタティックなインナークラスに、-128 ~ 127 のインスタンスがキャッシュされていて、その範囲の値は、そのキャッシュされたインスタンスを利用するようになっているようです。

要するに、ボクシングを利用して生成されたインスタンスは、上記の範囲の値に限って、Stringの文字列プールと同様のことがおこるわけです。

しかも、「プール」されるのは、-128 ~ 127 の範囲に限られうえ、文字列のようにリテラルに限られるわけでもありません。

public static void main(String[] args) {
    int i0 = Integer.parseInt(args[0]);
    Integer i1 = i0;
    Integer i2 = i0;
    System.out.println(i1 == i2);
}

のようなプログラムでは、arg[0] の値が、-128 ~ 127 に含まれるか否かによって、結果が変わってきてしまいます。

なかなか、複雑な仕様ではありませんか!?
文字列の比較では「常に equalsメソッドを使う」という戦略でOKでしたが、ボクシング、アンボクシングを考慮したときにはどういう戦略をとるのがベターなんでしょう?

まぁ、こういう仕様になったのにはおそらく深遠な理由があるのでしょうが。
わかりにくーーいバグの温床になっちゃう予感が激しくするのは僕だけでしょうか。

C#とかどういう仕様になってるんだろ。

Follow me!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です