Javaで数字を==で比較は危ないぞ

2022-08-01 Mon 00:00

数字の比較ってどうやってやりますか?数字がPositiveかどうかのチェックとかは n > 0 でやりますよね。でも数字同士が同じかどうかチェックするときに n1 == n2 とかやってないですか?これは実はJavaだと危ないんです。

何が危ないの?

例を見たら一発で == の怖さがわかるので例を出してみます。
下記のコードはどんな結果になると思いますか?

Integer a = 126;
Integer b = 126;
return a == b;

これは true が返ってきます。「なんだ想定通りじゃないか」って思ってますか?まぁ待ってください。次のコードはどんな結果になると思いますか?

Integer a = 127;
Integer b = 127;
return a == b;

これは false が返ってきます。完全に罠ですよね。これを見つけた僕はもう何を信じればいいのかわからなくなりました。なのでこれ解明するために単体テストを書きました。単体テストは信用できる。

どういうときにこの現象が起こるの?

同じ数字なのに == の結果が false になるときは下記の条件で起こります。

  • IntegerLong のオブジェクト同士の比較
    (primitiveの intlong の場合起こらない)
  • Integer n の値が n < -128 && 127 < n の場合

例としてテストケースをいくつか書いておきます。完結な記述にするためにinlineで cast してます。

Integer オブジェクト同士の比較:

Test case Expected Actual
(Integer) -129 == (Integer) -129 true false
(Integer) -128 == (Integer) -128 true true
(Integer) 127 == (Integer) 127 true true
(Integer) 128 == (Integer) 128 true false

Integer オブジェクト同士の場合は false になる値をprimitiveの int で比較:

Test case Expected Actual
128 == 128 true true
(Integer) 128 == 128 true true

ちなみにこれは Integer だけじゃなく、 LongShort でも同じことが起きます。

なんでこんな紛らわしいことになってるんだって思いますよね。 Integer クラスのコードを 127 で検索して探ってみたら IntegerCache というNestedクラスがありました。このクラスは -128 から 127 をcacheしてパフォーマンスを上げるクラスです。これが原因で比較の結果に違いが出てます。Cacheを使ってパフォーマンスを改善した影響で、Cacheで同じオブジェクトを使っているため -129 から 127 の範囲だけ == が数字の値の比較と同じ結果になってしまっています。紛らわしいですね。

問題はなに?

ちゃんと問題提起をしてみます。これするの大事ですよね。

起きている現象:

  • 数字を == で比較するときにオブジェクト同士で数字の値が n < -128 && 127 < n の場合、数字の値の比較ではなく、オブジェクト比較になる

起きている現象のよる問題:

  • 数字を == で比較している場合、数字の値の比較になる条件を気にする必要がある
    • 比較してる数字のタイプ
      (オブジェクトなのかprimitiveなのか)
    • 比較してる数字の値
      (-128 から 127 以外の数字でちゃんと想定通り動くのか)

解決方法

僕が考えた解決方法は「数字の比較に == は使わないで .equals() を使う」です。

会社ではこの案を導入しました。 .equals() は引数が同じ型であれば、数字がオブジェクトでもprimitiveでも数字の値の比較できます。
このやり方だと:

  • Integer でも int でもいいのでタイプを気にする必要がない
  • n < -128 && 127 < n のルールがなく、 null じゃなかったらどの値でも数字の比較をしてくれる

問題が解決できてますね。ですが、 .equals() はオブジェクトのメソッドなので、 NullPointerException が起きる可能性があるので Null を許容するシステムの場合ハンドルする必要があります。こんな感じなメソッドを作ればいちいちハンドルしないで良くなりますね。

public boolean isEqual(Integer a, Integer b) {
    return Optional.ofNullable(a)
        .map(aValue -> aValue.equals(b))
        .orElse(b == null);
}

会社のシステムはNullを許容しないJava Applicationなのでこの問題はあまりなかったです。