Integer キャッシュってそもそも何か
Integer キャッシュ は、Java が
「よく使われる小さい整数については、Integer オブジェクトを使い回す」
という最適化の仕組みです。
もっとざっくり言うと、
Integer のある範囲の値は、毎回 new しているわけじゃなくて、
あらかじめ用意してあるインスタンスを「再利用」している
ということです。
このせいで、
同じ値なのに == が true になったり false になったりする
という、不思議で危険な現象が起こります。
この挙動を知らないと、ラッパークラスの比較で盛大にハマります。
具体例:なぜ 100 は true で 200 は false になるのか
実験コードで体感してみる
まず、こんなコードを考えます。
public class IntegerCacheExample {
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
Integer x = 200;
Integer y = 200;
System.out.println(a == b); // 1
System.out.println(x == y); // 2
}
}
Java実行結果は、多くの環境でこうなります。
true
false
同じ Integer 同士なのに、== の結果が違います。
ここが Integer キャッシュの正体に関わる部分です。
何が起きているのかを解剖する
Integer a = 100; のようなコードを書くと、
コンパイラはだいたいこう変換します。
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
JavaInteger.valueOf(int) の中で、
「ある範囲の値ならキャッシュから返す」処理が入っています。
その「ある範囲」が、デフォルトでは
-128 から 127 まで
です。
つまり、
100 のとき
→ キャッシュ済みインスタンスを返す
→ a と b は同じインスタンスを参照
→ a == b は true
200 のとき
→ キャッシュ範囲外なので、毎回新しい Integer を作る
→ x と y は別インスタンス
→ x == y は false
という仕組みになっています。
なぜこんな仕組みがあるのか(目的)
よく使われる小さい整数を「使い回した方が得」だから
Java の設計者たちは、
-128 〜 127 あたりの小さい整数は、
ものすごく頻繁に使われる
と考えました。
例えば、ループカウンタ、フラグ、ステータスコードなど。
これを毎回 new Integer(… ) していると、
無駄なオブジェクトが大量に作られてしまいます。
そこで、
一度だけ -128〜127 の Integer を配列に作っておくvalueOf されたときに、その配列から既存インスタンスを返す
というキャッシュ方式を採用しました。
メリットは、
同じ整数値のためにオブジェクトを何度も作らなくて済む(メモリ節約)
同じインスタンスを再利用できるので、GC の負担も減る
といったパフォーマンス面です。
重要:Integer キャッシュは「== を使え」というサインではない
「100 なら == で true だし、これで比較すればいいのでは?」は大間違い
ここで一番伝えたいのはこれです。
キャッシュのおかげで a == b が true になるからといって、Integer 同士の比較に == を使ってはいけない ということです。
理由はシンプルで、
キャッシュは「インスタンスの再利用」の仕組みであって、
「値比較を保証するもの」ではない
からです。
Integer はオブジェクトなので、== は「同じインスタンスを指しているか?」しか見ません。
値としての等しさを見たいなら、必ず
a.equals(b)
Javaを使うべきです。
なぜ特に危険か:環境や実装に依存してしまうから
Integer キャッシュの範囲は、JVM の実装やオプションで変わる可能性があります。
デフォルトは多くの実装で -128〜127 ですが、
実行オプションで範囲を変えられる JVM もあります。
つまり、
ある環境だと 200 同士でも == が true
別の環境だと false
ということが起こりうる、ということです。
「キャッシュを知っているから大丈夫」ではなく、
キャッシュがあるからこそ、== 比較に頼るのが危険
と考えてください。
実務でどう考えるか:正しいラッパークラスの比較
ルールを一つ決めてしまうと迷わない
ラッパークラス全般に言える話ですが、Integer に関しても、
値比較に == を使わない
「値として等しいかどうか」は equals を使う
または、アンボクシングして基本型で == 比較する
とルール化してしまうのが一番楽です。
例えば:
Integer a = 100;
Integer b = 100;
boolean same1 = a.equals(b); // 正しい値比較
boolean same2 = (int)a == (int)b; // アンボクシングしてから比較(これも OK)
Javaどちらも「値としての比較」だと読み手にすぐ分かります。
逆に、
boolean same = (a == b); // キャッシュに依存してしまうので避ける
Javaと書いてしまうと、
「これ、意図的にインスタンス比較してる?
それとも値比較したかったのにキャッシュに頼ってるだけ?」
と読み手が迷います。
auto-boxing と組み合わさると、さらに読みにくくなる
こんなコードがあったとします。
Integer a = 100;
int b = 100;
System.out.println(a == b);
Javaここでは、
a は Integer、b は int== の比較時に a が auto-unboxing されて int になる
ので、「int 同士の比較」になります。
つまりこれは「値としての比較」として成立します。
一方、a と b が両方 Integer の場合は、
unboxing が起こらない限り「参照比較」になります。
見た目が似ているのに挙動が違うので、
あまり == に頼った書き方をすると、
自分でも何を意図しているのか見失いやすくなります。
だからこそ、
ラッパークラスの比較は equals
どうしても == を使うなら、先にアンボクシングする
と決めておくのが安全です。
「キャッシュを知っておく」ことの唯一のメリット
「なんで true と false が混ざるの?」と混乱しなくて済む
Integer キャッシュ自体は、普段意識して使うものではありません。
「これを活用すると便利」という類のものではなく、
「これを知らないと挙動にびっくりする」
系の知識です。
特に、
「なんか Integer の == の結果が謎なんだけど?」
とデバッグするときに、
ああ、-128〜127 はキャッシュされてるんだった
だからこのケースだけ同じインスタンスなんだな
と納得できる、という意味で役に立ちます。
「あえて new Integer(…) を使う」ケースはほぼない
昔は、
Integer a = new Integer(100); // 必ず新しいインスタンス
Integer b = new Integer(100);
System.out.println(a == b); // 常に false
Javaのように、「あえて new してキャッシュを使わない」こともできました。
ですが、今は new Integer(...) 自体が非推奨の空気感ですし、
キャッシュを避けたいケース自体もほぼありません。
キャッシュの存在は「頭の片隅に置いておく」程度で十分で、
わざわざそれに依存したコードを書くべきではありません。
まとめ:Integer キャッシュをどう理解しておくか
初心者として、このトピックをどう頭に残しておくかを整理します。
Java は -128〜127 の Integer をキャッシュして再利用している
その範囲の値は、Integer.valueOf や Integer a = 100; で同じインスタンスになることがある
その結果、Integer 同士の == が、値によって true になったり false になったりする
でも、それは「パフォーマンス最適化」であって、「値比較を保証する仕様」ではない
だから、Integer の値比較に == を使ってはいけない。equals か、アンボクシングしてから ==。
要するに、
キャッシュの存在は「挙動に驚かないため」に知っておく
コードを書くときはキャッシュを当てにしない
このスタンスで付き合うのが一番健全です。
