目次
- この記事の目的
- 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++のソースコードを読むことになります。