Java | Java 詳細・モダン文法:JVM・パフォーマンス – String プール

Java Java
スポンサーリンク

String プールを一言でいうと

String プールは、
同じ内容の文字列を、JVM の中でできるだけ 1 個だけにまとめて共有する仕組み」です。

Java の String は不変(immutable)なので、
同じ内容の "hello" が 100 回必要でも、
本当は "hello" というオブジェクトは 1 個あれば足ります。

その「もったいない」を減らすためにあるのが、String プールです。


文字列リテラルと String プールの基本

リテラルは最初からプールに入る

次のコードを見てください。

public class StringPoolBasic {
    public static void main(String[] args) {
        String a = "hello";
        String b = "hello";

        System.out.println(a == b);      // true
        System.out.println(a.equals(b)); // true
    }
}
Java

"hello" というリテラルは、クラスがロードされるときに String プールに登録されます。
ab も、そのプール上の同じ "hello" オブジェクトを参照します。

だから a == btrue になります。
ここが、普通の new と大きく違うポイントです。

new String(…) はプールを使わない

次のコードと比べてみましょう。

public class StringPoolNew {
    public static void main(String[] args) {
        String a = new String("hello");
        String b = new String("hello");

        System.out.println(a == b);      // false
        System.out.println(a.equals(b)); // true
    }
}
Java

new String("hello") は、
毎回「新しい String オブジェクト」をヒープ上に作ります。

中身の文字列は同じでも、オブジェクトとしては別物なので、
a == bfalse になります。

equals は「中身の文字列」を比較するので true です。

ここから分かる大事なことは、
リテラルはプールを使うが、new はプールを使わない」ということです。


intern() メソッドで「プールに合流する」

intern() の役割

String#intern() を呼ぶと、
「この文字列と同じ内容のものを String プールから探し、
あればその参照を返し、なければプールに登録してから返す」
という動きをします。

コードで見てみます。

public class StringIntern {
    public static void main(String[] args) {
        String a = new String("hello");
        String b = "hello";

        System.out.println(a == b); // false

        String c = a.intern();

        System.out.println(b == c); // true
    }
}
Java

a はヒープ上の "hello"(new で作ったもの)。
b はプール上の "hello"(リテラル)。

a.intern() を呼ぶと、
プール上の "hello" の参照が返ってくるので、
b == ctrue になります。

つまり、intern()
「バラバラに存在している同じ内容の String を、プール上の 1 個に合流させる」
ためのメソッドです。


なぜ String プールが存在するのか

メモリ節約のため

Java プログラムでは、同じ文字列が何度も登場します。
クラス名、メソッド名、フィールド名、定数、SQL、メッセージ…

これらを全部バラバラのオブジェクトとして持つと、
メモリがもったいない。

不変な String だからこそ、
「同じ内容なら 1 個だけ持って、みんなで共有しよう」
という発想が成り立ちます。

特に、文字列リテラルはクラスロード時にプールに登録されるので、
同じリテラルが何度出てきても、実体は 1 個で済みます。

比較を高速化できる場合がある

String の比較は通常 equals を使いますが、
プールされた文字列同士なら、== で比較しても意味が通ります。

String a = "hello";
String b = "hello";

if (a == b) {
    // 同じオブジェクト(同じ内容)
}
Java

== は「参照が同じか」を見るだけなので、
equals より高速です。

ただし、
「必ずプールされている」という保証がない文字列に対して
== を使うのは危険なので、
通常のアプリケーションコードでは、
比較には素直に equals を使うのが基本です。

プールを前提に == を使うのは、
かなり意図的な最適化をするときだけに留めるべきです。


String プールとメモリの関係

プールもメモリを食う

String プールは便利ですが、
「プールに入れた文字列は、基本的に長く生きる」
という性質があります。

リテラルはアプリケーションのライフタイム中ずっと残りますし、
intern() で動的に追加した文字列も、
GC されにくい長寿オブジェクトになります。

つまり、
「何でもかんでも intern すればいい」というものではありません。

大量のユニークな文字列(例えばユーザー入力やログの内容など)を
片っ端から intern() すると、
プールがパンパンになり、逆にメモリを圧迫します。

どんなときに intern を検討するか

intern() を検討するのは、例えばこんなケースです。

同じ文字列が何度も何度も登場する。
その文字列を Map のキーなどに大量に使う。
その結果、同じ内容の String オブジェクトがヒープに大量に重複している。

こういうときに、
「このキーは全部 intern 済みにしよう」と決めると、
重複を減らせることがあります。

ただし、これはもう「チューニング」の世界です。
初心者のうちは、
「String プールという仕組みがある」
「リテラルは勝手にプールされる」
new String はプールを使わない」
この 3 点を押さえておけば十分です。


具体例で「プールされる/されない」を整理する

例1:リテラル同士

String a = "abc";
String b = "abc";

System.out.println(a == b); // true
Java

同じクラス内の同じリテラルは、
プール上の同じオブジェクトを指します。

例2:リテラルと new

String a = "abc";
String b = new String("abc");

System.out.println(a == b);      // false
System.out.println(a.equals(b)); // true
Java

b はヒープ上の別オブジェクトです。
中身は同じでも、参照は違います。

例3:new + intern

String a = "abc";
String b = new String("abc").intern();

System.out.println(a == b); // true
Java

intern() によって、
b もプール上の "abc" を指すようになります。


まとめ:String プールを自分の言葉で説明するなら

あなたの言葉で整理すると、こうなります。

「String プールは、同じ内容の文字列を JVM 内で共有するための仕組み。
文字列リテラルは最初からプールに入り、同じリテラルは同じオブジェクトを指す。
new String("...") はプールを使わず毎回新しいオブジェクトを作るが、intern() を呼べばプール上の 1 個に合流できる。

プールはメモリ節約や一部の比較高速化に役立つが、
何でもかんでも intern すると逆にメモリを圧迫する。
だから、まずは『リテラルはプールされる』『new はされない』という基本を押さえ、
本当に必要な場面だけ意識的に intern を使う、というスタンスがちょうどいい。」

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