Javaパズル解答:"test".length()の呼び出しで挙動が変わるのはナゼ?

土曜日に紹介したパズル
http://d.hatena.ne.jp/nowokay/20121020#1350703958


このコードで、Java7の場合に"test".length()の呼び出しがない場合とある場合で最後のprintlnでの出力が変わるのはなぜか、という話。

String te = "te", st = "st";
// "test".length();
String username = te + st;
username.intern();
System.out.println(
  "String object the same is:" + (username == "test"));


まあ、intern()が思くそ怪しいので、このあたりがらみであることはすぐにわかると思います。
intern()は、JavaDocで次のように書かれています。

intern メソッドが呼び出されたときに、equals(Object) メソッドによってこの String オブジェクトに等しいと判定される文字列がプールにすでにあった場合は、プール内の該当する文字列が返されます。そうでない場合は、この String オブジェクトがプールに追加され、この String オブジェクトへの参照が返されます。
String (Java Platform SE 6)

この訳はJava6のものですが、Java7でも英語版ドキュメントの該当部分に変更はありません。


要するに、equalsではなく==で同一性を判定できるオブジェクトを返すというメソッドなのですが、ここで文字列プール内に該当する文字列がなかった場合にこのオブジェクトがプールに追加される、つまりintern()に副作用があるというのがポイントです。


ところで、問題を単純にするために、次のようなコードを動かしてみます。

String te = "te", st = "st";
String username = te + st;
System.out.println(username.intern() == username);

このコード、上記のドキュメントの「String オブジェクトに等しいと判定される文字列がプールにすでにあった場合」でない場合になるので、「String オブジェクトがプールに追加され、この String オブジェクトへの参照が返され」て、==での比較がtrueになるはずです。
Java SE 7 Update 9の場合は、これはtrueを表示します。ところが、Java SE 6 Update 37で実行するとこれはfalseを表示します。ということで、Java6の場合は「String オブジェクトがプールに追加され、この String オブジェクトへの参照が返」すという動きになっていないと言えます。


ここで、問題に戻りますが、「"test".length()」がない場合は、usernameオブジェクトが文字列プールに登録されるので、あとで"test"リテラルが出てきた場合にもこのusernameオブジェクトが文字列プールからひっぱられることになり、「username == "test"」がtrueを返すことになります。
「"test".length()」などとして文字列リテラルを使うと、"test"が文字列プールに追加されるので、intern()時点で「String オブジェクトに等しいと判定される文字列がプールにすでにあった場合」となるので、usernameオブジェクトが文字列プールに登録されることがなくなります。そのため「username == "test"」はfalseになります。


このことから、実は文字列定数を使う処理ってintern()相当のことをやってるはずだから結構おそいのかも、ということがわかります。ただ文字列定数はそう多く使うものでもないし、頻繁に使われる場合は最適化されるでしょうから、現実的には影響は少ないと思われます。