
目次
- この記事の目的
- String Constant Pool
- String#intern
- String#intern の実装を追う
- まとめ
1. この記事の目的
Javaで文字列同士の比較(等しいか判定)したい時は、 equals メソッドを使いましょう。
とは初心者に最初に教えるべき1つの教訓になっています。
では次のコードを実行するとどのように出力されるでしょうか。
public class StringLiteral { public static void main(String[] args) { String str1 = "abc"; String str2 = "abc"; System.out.println(str1 == str2); } }
== 演算子は参照の比較を行います。
"abc" のオブジェクトが2つ生成されていれば、str1, str2の参照は異なるはずです。
なので、パッと見はfalseと表示されるように見えます。
しかし答えはtrueです。
==で比較すべきではありませんが、「equalsを使いなさい」と教えた時に「なぜ==で比較できる時があるのですか」と聞かれて答えられないわけにもいきません。
なぜtrueと表示されるのか、この記事で説明したいと思います。
2. String Constant Pool
JVMにはString Constant Poolという仕組みがあります。
新たに文字列オブジェクトを生成する際に、メモリの節約を行うための仕組みです。
すでにヒープに同じ内容の文字列オブジェクトが乗っていた際に、参照を使い回します。
これは、文字列オブジェクトが変更不能だから可能なことです。
イメージは下記の画像の通りです。

3. String#intern
どういう時にString Constant Poolが適応されるのかについては、JVMの仕様によるため、正直なところ私はよくわかっていません。
しかし明示的にこのプールのオブジェクトを使う方法であればわかります。
String#internが使うことで可能になります。
ここからは少し余談になります。
例えば次のコードを実行するとfalseと表示されます。
public class NewString { public static void main(String[] args) { String str1 = "abc"; String str2 = new String("abc"); System.out.println(str1 == str2); } }
str2を文字列リテラルではなくコンストラクタで生成したため、String Constant Poolの参照が使いまわされなかったためです。
しかし次のコードを実行するとtrueと表示されます。
public class NewString { public static void main(String[] args) { String str1 = "abc"; String str2 = new String("abc").intern(); System.out.println(str1 == str2); } }
String#internを用いて明示的にString Constant Poolにアクセスしたためです。
4. String#intern の実装を追う
ここからはさらに余談ですが、せっかく調査したのでまとめます。
java.lang.String(.java)
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // ... public native String intern(); }
実装は書かれていません。
native修飾子がついているので、他の言語で実装されているみたいですね。
OpenJDKのソースコードを読んでみます。
src/share/native/java/lang/String.c
#include "jvm.h" #include "java_lang_String.h" JNIEXPORT jobject JNICALL Java_java_lang_String_intern(JNIEnv *env, jobject this) { return JVM_InternString(env, this); }
String#internはjvm.hで定義されているJVM_InternStringを読んでいるだけのようです。
JVMの機能をそのまま呼び出すメソッドがStringクラスに準備されていたとは。
src/share/vm/prims/jvm.cpp
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str)) JVMWrapper("JVM_InternString"); JvmtiVMObjectAllocEventCollector oam; if (str == NULL) return NULL; oop string = JNIHandles::resolve_non_null(str); oop result = StringTable::intern(string, CHECK_NULL); return (jstring) JNIHandles::make_local(env, result); JVM_END
次はStringTable::internですね。
src/share/vm/classfile/stringTable.cpp
oop StringTable::intern(Handle string_or_null, jchar* name, int len, TRAPS) { // shared table always uses java_lang_String::hash_code unsigned int hashValue = java_lang_String::hash_code(name, len); oop found_string = lookup_shared(name, len, hashValue); if (found_string != NULL) { return found_string; } // ...
なるほど、ここで文字列からハッシュ値を計算していますね。
ハッシュ値からテーブルから検索して、あればその参照を返す。
そうでなければ、新たにテーブルに登録しているようです。
この感じだと、テーブルサイズをnとした時にO(log(n))で検索できそうです。
至るところでString#internを使用すると、実際パフォーマンスに影響があるようなので気をつけましょう。
メモリと時間との相談になりそうです。
5. まとめ
結構余談が多くなりましたが、まとめます。
2つの文字列リテラルが==演算子で比較できてしまうのは、String Constant Poolという仕組みのおかげです。
明示的にString Constant Poolを使いたい時はString#internを使いましょう。
String#internの実装を知りたい際は、OpenJDKのC++のソースコードを読むことになります。