ボクシングされたプリミティブ型の値を == 演算子や != 演算子で直接比較することはできない。なぜならば、これらの演算子はオブジェクトの値ではなく参照を比較するからである。プログラマはこの動作に驚くかもしれない。オートボクシングはいくつかのプリミティブの値をメモ化(memoizeあるいはキャッシュ)しており、参照の比較と値の比較は、メモ化された一部の値に関しては同じ結果を示すことがある。
オートボクシングは、プリミティブ型の値を対応するラッパーオブジェクトで自動的にラップする。Java 言語仕様 の §5.1.7, 「ボクシング変換」には、オートボクシングの際にどのプリミティブ型の値がメモ化されるか記述されている[JLS 2005]。
ボクシング変換の対象となる値 p が true, false, byte, \u0000 から \u007f までの範囲にある char, あるいは -128 から 127 までの数値となる int や short である場合, 任意の p に対する2つのボクシング変換結果を r1 と r2 とする。この場合, 常に r1 == r2 が成立する。
プリミティブ型 | ボクシングされた結果の型 | 完全にメモ化される |
---|---|---|
boolean, byte | Boolean, Byte | はい |
char, short, int | Char, Short, Int | いいえ |
ボクシングされたプリミティブ型の値のうち、完全にメモ化されるものについては、== や != 演算子による比較を行ってもよい。
ボクシングされたプリミティブ型の値のうち、完全にメモ化されないものを == や != 演算子を使って比較してもよいのは、表現される値がJava 言語仕様の規定する範囲に収まっていることが保証される場合のみである。
その他の場合には、ボクシングされたプリミティブ型の値の比較に == や != を使ってはならない。
JVM 実装では、仕様上必須ではないが、その他の値についてもメモ化することが許されている。
たとえば、メモリ制約の少ない実装では、文字と短精度整数のすべて、および、-32K から 32K の範囲にある整数や倍精度整数をキャッシュすることもできる。
特定のJVM実装の動作に依存したコードは可搬性に欠ける。
違反コード
以下の違反コード例は、compare() メソッドを持つ Comparator クラスを定義している[Bloch 2009]。compare() メソッドはプリミティブ型をボクシングした値2つを引数にとる。プリミティブ型をボクシングした値の比較には == 演算子を使っている。しかしこれでは、中身の値の比較ではなく、ラッパーオブジェクトの参照の比較になってしまう。
static Comparator<Integer> cmp = new Comparator<Integer>() { public int compare(Integer i, Integer j) { return i < j ? -1 : (i == j ? 0 : 1); } };
オートボクシングが行われるため、引数としてプリミティブ型の整数を渡すことも可能であることに注意。
適合コード
以下の適合コードでは比較演算子 <、>、<=、>= を使っている。これらの演算子では自動的にアンボクシングが行われる。ボクシングされたオブジェクトに対して == 演算子や != 演算子を使ってはいけない。
public int compare(Integer i, Integer j) { return i < j ? -1 : (i > j ? 1 : 0) ; }
違反コード
以下の違反コード例は、2つの Integer オブジェクトの値の比較に == 演算子を使っている。しかし、== 演算子はオブジェクトの値ではなく参照の比較を行う。
public class Wrapper { public static void main(String[] args) { Integer i1 = 100; Integer i2 = 100; Integer i3 = 1000; Integer i4 = 1000; System.out.println(i1 == i2); System.out.println(i1 != i2); System.out.println(i3 == i4); System.out.println(i3 != i4); } }
Integer クラスでは、-128 から 127 の範囲の整数についてだけ、値がキャッシュされることが保証されている。2つのオブジェクトが同じ値を持っていたとしても、この範囲に収まらない値であるなら、等値演算子による比較で等値でない結果が得られる可能性がある。たとえば、上記の範囲外の値をまったくキャッシュしない JVM 実装では、このコードの出力は以下のようになるであろう。
true false false true
適合コード
以下の適合コードは、== 演算子ではなく equals() メソッドを使ってオブジェクトの比較を行っている。このコードは、どんなプラットフォームでも期待どおりに、 true、false、true、false を出力する。
public class Wrapper { public static void main(String[] args) { Integer i1 = 100; Integer i2 = 100; Integer i3 = 1000; Integer i4 = 1000; System.out.println(i1.equals(i2)); System.out.println(!i1.equals(i2)); System.out.println(i3.equals(i4)); System.out.println(!i3.equals(i4)); } }
違反コード
Java のコレクションにはプリミティブ型のデータを入れることはできず、オブジェクトしか入れることができない。また、Javaのすべてのジェネリック型における型パラメータは、プリミティブ型ではなくオブジェクト型でなければならない。そのため、int型を要素とする ArrayList<int> という宣言はコンパイルエラーになる。なぜならば、int 型はオブジェクト型ではないからである。正しく宣言するには、ArrayList<Integer> のように宣言し、ラッパークラスとオートボクシングを活用するのがよい。
以下の違反コード例では、配列 list1 と list2 の要素の値が同じであるインデックスの数を数えようとしている。Integer クラスの値が必ずメモ化されるのは、整数値が -128 から 127 の範囲に収まる場合のみであることを思い出してほしい。この範囲に収まらない値については、ボクシングにより新たなラッパーオブジェクトが生成され、それらのオブジェクトは等値とはみなされない可能性がある。したがって、それらのオブジェクトの比較において == 演算子は false を返し、コードは誤った値0を出力するかもしれない。
public class Wrapper { public static void main(String[] args) { // 整数の配列を作成する ArrayList<Integer> list1 = new ArrayList<Integer>(); for (int i = 0; i < 10; i++) { list1.add(i + 1000); } // 各要素の値が list1 と同じになるような整数配列をもう1つ作る ArrayList<Integer> list2 = new ArrayList<Integer>(); for (int i = 0; i < 10; i++) { list2.add(i + 1000); } // 同じ値となる要素を数える int counter = 0; for (int i = 0; i < 10; i++) { if (list1.get(i) == list2.get(i)) { // '==' を使っている counter++; } } // 変数 counter の値を出力; この例では 0 になる System.out.println(counter); } }
しかし、このコードを実行する JVM で -32768 から 32767 の範囲の整数値がメモ化されたとすると、このコードで使われているすべての int 型の値がそれぞれ1つのInteger オブジェクトにオートボクシングされ、このコードはプログラマの期待どおりに動作していただろう。オブジェクト等価演算子の代わりに参照等価演算子を使ってこのような動作をさせるには、すべての値が JVM によってメモ化される範囲に収まっている必要がある。Java 言語仕様には、この範囲に関する規定がなく、メモ化が行われるべき最小限の範囲だけが規定されている。結局、このコードの動作を予測するには JVM の実装の詳細を知る必要がある。
適合コード
以下の適合コードでは、equals() メソッドを使ってラッパーオブジェクトが持つ値の比較を行っており、想定どおり 10 が出力される。
public class Wrapper { public static void main(String[] args) { // 各要素が 127 より大きい値を持つ整数の配列を作る ArrayList<Integer> list1 = new ArrayList<Integer>(); for (int i = 0; i < 10; i++) { list1.add(i + 1000); } // 各要素の値が list1 と同じになるような整数配列をもう1つ作る ArrayList<Integer> list2 = new ArrayList<Integer>(); for (int i = 0; i < 10; i++) { list2.add(i + 1000); } // 同じ値となる要素を数える int counter = 0; for(int i = 0; i < 10; i++) { if (list1.get(i).equals(list2.get(i))) { // 'equals()' を使う counter++; } } // 変数 counter の値を出力。この例では 10 になる System.out.println(counter); } }
違反コード (new Boolean)
以下の違反コード例では、Boolean クラスのコンストラクタはそれぞれ異なるオブジェクトを新たに生成している。値の比較に参照等価演算子を使っているため、予期せぬ結果となってしまう。
public void exampleEqualOperator(){ Boolean b1 = new Boolean("true"); Boolean b2 = new Boolean("true"); if(b1 == b2) { // 決してイコールにはならない System.out.println("Never printed"); } }
適合コード (new Boolean)
以下の適合コードにおいて、オートボクシングされ Boolean 型変数に代入された値の比較には参照等価演算子を使うことができる。なぜならば、Java 言語では Boolean 型は完全にキャッシュされることを保証しているからだ。つまり、これら(ボクシングで得られるもの)は同一のオブジェクトであることが保証される。
public void exampleEqualOperator(){ Boolean b1 = true; // Or Boolean.True Boolean b2 = true; // Or Boolean.True if(b1 == b2) { // 常にイコールになる System.out.println("Always print"); } }
例外
EXP03-EX0 プログラムが一種類の JVM 実装でしか実行されないことが保証されるのであれば、実装依存の特性であるキャッシュされる値の範囲に依存したコーディングを行ってもよい。
リスク評価
プリミティブ型をボクシングした値の比較に参照等価演算子を使うと、間違った比較につながる。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
EXP03-J | 低 | 高 | 中 | P6 | L2 |
自動検出
プリミティブ型をボクシングした値に対する参照等価演算子の使用を検出するのは簡単である。それらの使用が正しいかどうかを判断するのは一般に不可能である。
関連ガイドライン
MITRE CWE | CWE-595. Comparison of Object References Instead of Object Contents |
CWE-597. Use of Wrong Operator in String Comparison |
参考文献
[Bloch 2009] | 4. Searching for the One |
[JLS 2005] | §5.1.7, Boxing Conversion |
[Pugh 2009] | Using == to Compare Objects Rather than .equals |
翻訳元
これは以下のページを翻訳したものです。
EXP03-J. Do not use the equality operators when comparing values of boxed primitives (revision 88)