数字の比較ってどうやってやりますか?数字が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
になるときは下記の条件で起こります。
Integer
やLong
のオブジェクト同士の比較
(primitiveのint
やlong
の場合起こらない)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
だけじゃなく、 Long
や Short
でも同じことが起きます。
なんでこんな紛らわしいことになってるんだって思いますよね。 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なのでこの問題はあまりなかったです。