なぜJavaの文字列(String)はイミュータブルなのか

Java における String は不変(イミュータブル)であるというのはよく知られていることだと思います.が,なぜそうなっているのかということについては考えたことがありませんでした.今回はその理由についてメモ.

理由① String Pool のため

Java では String は少々特別扱いされていて,Java ヒープには String Pool と呼ばれる文字列を格納しておく領域があります.プログラム中で使われる文字列をここで重複無く管理しておくことで,いくつも同じ値の文字列オブジェクトが生成されることを無くすための仕組みです.所謂 Flyweight パターンです.

例えば以下のコードがあった時,String Pool には "Cat" と "Dog" が格納されます.

String str1 = "Cat";
String str2 = "Dog";
String str3 = "Cat";
str1 == str3; // true

まず,1行目で String Pool に "Cat" が格納されます.そして,3行目が実行されるタイミングでは既に String Pool に "Cat" が存在します.この場合,新たなオブジェクトは作成されず,String Pool にある既存のオブジェクトへの参照を str3 に代入します.すなわち,str1 と str3 が同じオブジェクトへの参照を持つことになります.これにより,プログラムを省リソース化できます.

さて,この時 str1 と str3 を == で比較すると当然 true になるわけです.ここで,String が可変だったとすると,str1 の値を変更すると str3 の値まで変わってしまいます(例えば破壊的な str1.toLowerCase() とか.String が可変の世界ではあり得る).一方で,String が不変だとこのような問題が起きないのは自明です.

可変であったとしても,str1 の値の変更を検出した時にその値が str3 からも参照されていることを判定できれば,str3 には新しく生成した "Cat" への参照を代入するというやり方も無くはないかもしれません.もしくは逆に,2箇所以上から参照されている文字列を変更する時だけは新たにオブジェクトを生成するとか(参照カウンタを使ったりして).ただ,こんなことをするよりかは文字列を不変オブジェクトにしてしまう方が容易でしょう.

ちなみに,String Pool が使われるのは上記のコードのようにダブルクォートで文字列を生成した場合です(所謂リテラル).で,new String() を使うと String Pool が考慮されずに,String Pool ではないヒープ上に文字列オブジェクトが新規生成されます.ということで,省リソースのためにも文字列はダブルクォートで作った方が良さそうです.

String str1 = "Cat";
String str2 = "Cat";
String str3 = new String("Cat");
str1 == str2; // true
str1 == str3; // false
str1 == str3.intern(); // true intern()はStringPoolの同一文字列を検索してヒットしたらその参照を返す

余談ですが最新の IntelliJ では new String(); したらちゃんと注意されるようになってました.

f:id:norikone:20180515165258p:plain

また,処理時間を測ってみてもだいぶ差があることがわかります.なお計測方法が正しいかどうかは不明.

String s1 = "Cat";
long startTime;
long endTime;

startTime = System.nanoTime();
String s2 = "Cat";
endTime = System.nanoTime();
System.out.println("処理時間1:" + (endTime - startTime) + " ns");

startTime = System.nanoTime();
String s3 = "Dog";
endTime = System.nanoTime();
System.out.println("処理時間2:" + (endTime - startTime) + " ns");

startTime = System.nanoTime();
String s4 = new String("Cat");
endTime = System.nanoTime();
 System.out.println("処理時間3:" + (endTime - startTime) + " ns");

結果

処理時間1:319 ns    // String Pool ヒット
処理時間2:3314 ns   // String Pool ノーヒット
処理時間3:10007 ns  // new で生成

処理時間1は endTime の計算が処理時間の90%以上を占めている感じなので,実際の処理時間はもっと小さそうです.

理由② キャッシングのため

不変であるということは,一度オブジェクトが生成されるとその状態が将来的に変更されないということです.これは,ハッシュコードが変更されないことを保証します.なぜなら,ハッシュコードはそのオブジェクトの状態を基に生成されるからです.なので,オブジェクトが不変なおかげで,一度ハッシュコードを計算したらそれをキャッシュして使い回しできるのです.

String クラスには以下のように hash という int 型の private フィールドがあり,hashCode() 実行時には hash に値があればそれを返します.なければハッシュコードの計算をして,hash に入れます.

...
private int hash; // Default to 0
...
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
...

このキャッシングにより,パフォーマンスの向上が期待できます.例えば,String は HashMap のキーなどでよく利用されますが,そのようなハッシュコードを頻繁に使うケースでより効率的に処理できるようになります.Hash コレクション系では値を追加する時や値を取得する時にハッシュコードが使われるので,それを高速化できる感じですね.

理由③ セキュリティのため

String は引数として使われることが非常に多いです.それも,データベース接続用のユーザ名であったり,ファイルを開くためのパスであったりと,外部リソースを扱う時によく使われたりします.このような状況下で String が可変であると,セキュリティ的によろしくありません.

例えば,引数として渡された文字列に対して何かしらのセキュリティ処理をした後に,文字列を変更されてしまうケースが想定できます.

public void secureOpen(String path){
    if(!isSecure(path)){ // 権限等のチェック
        throw new SecurityException();
    }

    // Stringが可変だと,ここで他の参照を使ってpathの値を書き換えることが可能
    open(path);   
}

isSecure() でのセキュリティチェックを通過した後で,他の参照から文字列が書き換えられてしまうと,予期せぬ問題が発生する可能性があります.他の参照というのは,path が指しているのと同じオブジェクトへの参照を持つ他の変数のことです.String を不変にしておくことで,呼び出し先に渡した文字列が途中で外部から書き換えられることを防げます.このように,String が不変な理由としてはセキュリティ的な観点もあります.

理由④ スレッドセーフティ

String が不変であるおかげで,マルチスレッドプログラミングで文字列を扱う際の面倒事を減らせます.String が可変である場合には,スレッド間で文字列を共有する際の同期制御について考える必要があります.

例えば String が可変であると,理由③に書いたコードのように文字列を参照するフェーズと使用するフェーズに分かれている場合,参照時と使用時で,スレッド間で共有している文字列の値が変わっている可能性があります.あるスレッドで isSecure() を実行した際にこのチェックをパスできる値になっていたとしても,次の open(path) が呼び出されるまでの間に他のスレッドが path の値を書き換えてしまうかもしれません.これを防ぐためにはロックなどの仕組みを使わなければならず,実装がやや複雑化します.

String が不変だとスレッド間での共有が簡単で,考え事やコードが少なくなります.つまり,正しく作成された不変オブジェクトは暗黙にスレッドセーフなので,文字列みたいな頻繁に使われるクラスは不変化したほうが捗るよね,ということでしょうか.String が可変だと,常に外部から変更される危険を孕むので,例えスレッド間で文字列を意図的に共有しない場合だったとしても面倒事が増えそうです.

おわり

これらは十分に String を不変とする理由になるかと思います.大きく分けると,「パフォーマンス」と「安全性」のため,になるでしょうか.最近のコンピューティング環境の発展に鑑みれば,パフォーマンスについてはあまり深刻に考える必要もないかもしれんが.といっても String は非常に使用頻度の高いオブジェクトなので,不変化による恩恵はそれなり受けられそうです.同じような理由で,Integer や Long などのクラスも不変クラスになっている模様です.
上記から分かる不変の強みは以下.

  • 不変オブジェクトには flyweight パターンが適用しやすい
  • 不変オブジェクトはキャッシュによるパフォーマンス向上がしやすい
  • 不変オブジェクトはセキュリティを向上できる
  • 不変オブジェクトはスレッドセーフ

参考

http://www.java67.com/2014/01/why-string-class-has-made-immutable-or-final-java.html