Java | Java 標準ライブラリ:Integer キャッシュ

Java Java
スポンサーリンク

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);
Java

Integer.valueOf(int) の中で、
「ある範囲の値ならキャッシュから返す」処理が入っています。

その「ある範囲」が、デフォルトでは

-128 から 127 まで

です。

つまり、

100 のとき
→ キャッシュ済みインスタンスを返す
ab は同じインスタンスを参照
a == b は true

200 のとき
→ キャッシュ範囲外なので、毎回新しい Integer を作る
xy は別インスタンス
x == y は false

という仕組みになっています。


なぜこんな仕組みがあるのか(目的)

よく使われる小さい整数を「使い回した方が得」だから

Java の設計者たちは、

-128127 あたりの小さい整数は、
ものすごく頻繁に使われる

と考えました。

例えば、ループカウンタ、フラグ、ステータスコードなど。
これを毎回 new Integer(… ) していると、
無駄なオブジェクトが大量に作られてしまいます。

そこで、

一度だけ -128127Integer を配列に作っておく
valueOf されたときに、その配列から既存インスタンスを返す

というキャッシュ方式を採用しました。

メリットは、

同じ整数値のためにオブジェクトを何度も作らなくて済む(メモリ節約)
同じインスタンスを再利用できるので、GC の負担も減る

といったパフォーマンス面です。


重要:Integer キャッシュは「== を使え」というサインではない

「100 なら == で true だし、これで比較すればいいのでは?」は大間違い

ここで一番伝えたいのはこれです。

キャッシュのおかげで a == b が true になるからといって、
Integer 同士の比較に == を使ってはいけない ということです。

理由はシンプルで、

キャッシュは「インスタンスの再利用」の仕組みであって、
「値比較を保証するもの」ではない

からです。

Integer はオブジェクトなので、
== は「同じインスタンスを指しているか?」しか見ません。

値としての等しさを見たいなら、必ず

a.equals(b)
Java

を使うべきです。

なぜ特に危険か:環境や実装に依存してしまうから

Integer キャッシュの範囲は、JVM の実装やオプションで変わる可能性があります。

デフォルトは多くの実装で -128127 ですが、
実行オプションで範囲を変えられる 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

ここでは、

aIntegerbint
== の比較時に a が auto-unboxing されて int になる

ので、「int 同士の比較」になります。
つまりこれは「値としての比較」として成立します。

一方、ab が両方 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 は -128127Integer をキャッシュして再利用している
その範囲の値は、Integer.valueOfInteger a = 100; で同じインスタンスになることがある
その結果、Integer 同士の == が、値によって true になったり false になったりする
でも、それは「パフォーマンス最適化」であって、「値比較を保証する仕様」ではない
だから、Integer の値比較に == を使ってはいけない。equals か、アンボクシングしてから ==

要するに、

キャッシュの存在は「挙動に驚かないため」に知っておく
コードを書くときはキャッシュを当てにしない

このスタンスで付き合うのが一番健全です。

タイトルとURLをコピーしました