JPCERT コーディネーションセンター

EXP03-J. ボクシングされたプリミティブ型の値の比較に等値演算子を使わない

EXP03-J. ボクシングされたプリミティブ型の値の比較に等値演算子を使わない

ボクシングされたプリミティブ型の「値」を == 演算子や != 演算子で直接比較することはできない。なぜならば、これらの演算子はオブジェクトの値ではなく参照を比較するからである。プログラマはこの動作に驚くかもしれない。オートボクシングはいくつかのプリミティブの値をメモ化(memoizeあるいはキャッシュ)しており、参照の比較と値の比較は、メモ化された一部の値に関しては同じ結果を示すことがある。

オートボクシングは、プリミティブ型の値を対応するラッパーオブジェクトで自動的にラップする。『Java 言語仕様』§5.1.7「ボクシング変換」には、オートボクシングの際にどのプリミティブ型の値がメモ化されるか記述されている[JLS 2015]。

ボクシング変換の対象となる値 ptrue, false, byte, \u0000 から \u007f までの範囲にある char, あるいは -128 から 127 までの数値となる intshort である場合, 任意の p に対する2つのボクシング変換結果を r1r2 とする。この場合, 常に r1 == r2 が成立する。

プリミティブ型

ボクシングされた結果の型

完全にメモ化される

boolean, byte

Boolean, Byte

はい

char, short, int

Char, Short, Int

いいえ

ボクシングされたプリミティブ型の値のうち、完全にメモ化されるものについては、==!= 演算子による比較を行ってもよい。

ボクシングされたプリミティブ型の値のうち、完全にメモ化されないものを ==!= 演算子を使って比較してもよいのは、表現される値がJava 言語仕様の規定する範囲に収まっていることが保証される場合のみである。

その他の場合には、ボクシングされたプリミティブ型の値の比較に ==!= を使ってはならない。

Java 仮想マシン(JVM)実装では、仕様上必須ではないが、その他の値についてもメモ化することが許されている。[JLS 2015]

たとえば、メモリ制約の少ない実装では、文字と短精度整数のすべて、および、−32K から +32K の範囲にある整数や倍精度整数をキャッシュすることもできる。(§5.1.7

特定のJVM実装の動作に依存したコードは可搬性に欠ける。ターゲットとなるすべての実装がその範囲をサポートしている場合に限り、実装ごとの特性であるメモ化された値の範囲に依存してもよい。

違反コード

以下の違反コード例は、compare() メソッドを持つ Comparator クラスを定義している[Bloch 2009]。compare() メソッドはプリミティブ型をボクシングした値2つを引数にとる。プリミティブ型をボクシングした値の比較には == 演算子を使っている。しかしこれでは、中身の「値」の比較ではなく、ラッパーオブジェクトの「参照」の比較になってしまう。

import java.util.Comparator;

static Comparator<Integer> cmp = new Comparator<Integer>() {
  public int compare(Integer i, Integer j) {
    return i < j ? -1 : (i == j ? 0 : 1);
  }
};

オートボクシングが行われるため、引数としてプリミティブ型の整数を渡すことも可能であることに注意。

適合コード

以下の適合コードでは比較演算子 <><=>= を使っている。これらの演算子では自動的にアンボクシングが行われる。ボクシングされたオブジェクトに対して == 演算子や != 演算子を使ってはいけない。

import java.util.Comparator;

static Comparator<Integer> cmp = new Comparator<Integer>() {
  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() メソッドを使ってオブジェクトの比較を行っている。このコードは、どんなプラットフォームでも期待どおりに、 truefalsetruefalse を出力する。

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> のように宣言し、ラッパークラスとオートボクシングを活用するのがよい。

以下の違反コード例では、配列 list1list2 の要素の値が同じであるインデックスの数を数えようとしている。Integer クラスの値が必ずメモ化されるのは、整数値が −128 から 127 の範囲に収まる場合のみであることを思い出してほしい。この範囲に収まらない値については、ボクシングにより新たなラッパーオブジェクトが生成され、それらのオブジェクトは等値とはみなされない可能性がある。したがって、それらのオブジェクトの比較において == 演算子は false を返し、コードは誤った値0を出力するかもしれない。

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) == 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) {
    // 整数の配列を作る
    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);
  }
}
違反コード (Boolean)

以下の違反コード例では、Boolean クラスのコンストラクタはそれぞれ異なるオブジェクトを新たに生成している。値の比較に参照等価演算子を使っているため、予期せぬ結果となってしまう。

public void exampleEqualOperator(){
  Boolean b1 = new Boolean("true");
  Boolean b2 = new Boolean("true");

  if (b1 == b2) {    // 決してイコールにはならない
    System.out.println("Never printed");
  }
}
適合コード (Boolean)

Boolean.TRUEBoolean.FALSE、オートボクシングされた truefalse リテラルの比較には参照等価演算子を使うことができる。なぜならば、Java 言語では Boolean 型は完全にキャッシュされることを保証しているからだ。つまり、これら(ボクシングで得られるもの)は同一のオブジェクトであることが保証される。

public void exampleEqualOperator(){
  Boolean b1 = true;
  Boolean b2 = true;

  if (b1 == b2) {   // 常にイコールになる
    System.out.println("Always printed");
  }
 
  b1 = Boolean.TRUE;
  if (b1 == b2) {   // 常にイコールになる
    System.out.println("Always printed");
  }
}
リスク評価

プリミティブ型をボクシングした値の比較に等値演算子を使うと、間違った比較につながる。

ルール

深刻度

可能性

自動検出

自動修正

優先度

レベル

EXP03-J

P9

L2

自動検出

プリミティブ型をボクシングした値に対する参照等価演算子の使用を検出するのは簡単である。それらの使用が正しいかどうかを判断するのは一般に不可能である。

ツール バージョン チェッカー 説明
CodeSonar 9.0p0

JAVA.COMPARE.EMPTYSTR
JAVA.COMPARE.EQ
JAVA.COMPARE.EQARRAY

Comparison to empty string 
Should Use equals() instead of == (Java)
equals on Array 

Coverity 7.5

BAD_EQ
FB.EQ_ABSTRACT_SELF
FB.EQ_ALWAYS_FALSE
FB.EQ_ALWAYS_TRUE
FB.EQ_CHECK_FOR_OPERAND_NOT_ COMPATIBLE_WITH_THIS
FB.EQ_COMPARETO_USE_OBJECT_ EQUALS
FB.EQ_COMPARING_CLASS_NAMES
FB.EQ_DOESNT_OVERRIDE_EQUALS
FB.EQ_DONT_DEFINE_EQUALS_ FOR_ENUM
FB.EQ_GETCLASS_AND_CLASS_ CONSTANT
FB.EQ_OTHER_NO_OBJECT
FB.EQ_OTHER_USE_OBJECT
FB.EQ_OVERRIDING_EQUALS_ NOT_SYMMETRIC
FB.EQ_SELF_NO_OBJECT
FB.EQ_SELF_USE_OBJECT
FB.EQ_UNUSUAL
FB.ES_COMPARING_PARAMETER_ STRING_WITH_EQ
FB.ES_COMPARING_STRINGS_ WITH_EQ
FB.ES_COMPARING_PARAMETER_ STRING_WITH_EQ

Implemented
Klocwork

2025.2

CMP.OBJ
Parasoft Jtest 2024.2 CERT.EXP03.UEIC Do not use '==' or '!=' to compare objects
PVS-Studio

7.38

V6013
SonarQube 9.9 S1698 "==" and "!=" should not be used when "equals" is overridden
関連ガイドライン

MITRE CWE

CWE-595, Comparison of Object References Instead of Object Contents
CWE-597, Use of Wrong Operator in String Comparison

参考文献

[Bloch 2009]

Puzzle 4, "Searching for the One"

[JLS 2015]

§5.1.7, "Boxing Conversion"

[Pugh 2009]

Using == to Compare Objects Rather than .equals

[Seacord 2015] Image result for video icon EXP03-J. Do not use the equality operators when comparing values of boxed primitives LiveLesson
翻訳元

これは以下のページを翻訳したものです。

EXP03-J. Do not use the equality operators when comparing values of boxed primitives (revision 143)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter