【Java】なぜ文字列リテラルは演算子で比較できる時があるのか

f:id:shogonir:20180129023052p:plain

 

目次

  1. この記事の目的
  2. String Constant Pool
  3. String#intern
  4. String#intern の実装を追う
  5. まとめ

 

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という仕組みがあります。
新たに文字列オブジェクトを生成する際に、メモリの節約を行うための仕組みです。
すでにヒープに同じ内容の文字列オブジェクトが乗っていた際に、参照を使い回します。
これは、文字列オブジェクトが変更不能だから可能なことです。

イメージは下記の画像の通りです。

f:id:shogonir:20180129011150p:plain

 

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#internjvm.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++ソースコードを読むことになります。