検証環境
]$ uname -a Linux localhost.localdomain 3.10.0-957.1.3.el7.x86_64 #1 SMP Thu Nov 29 14:49:43 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux ]$ java -version openjdk version "11.0.1" 2018-10-16 LTS OpenJDK Runtime Environment 18.9 (build 11.0.1+13-LTS) OpenJDK 64-Bit Server VM 18.9 (build 11.0.1+13-LTS, mixed mode, sharing)
突然ですが、問題です。
問題その1
以下のコードの実行結果は、何でしょうか?
public class Main { public static void main(String[] args) { String str1 = new String("hello world"); String str2 = new String("hello world"); System.out.println(str1 == str2); } }
正解は、 false です。
]$ javac Main.java ]$ java Main false
問題その2
では、以下のコードの実行結果は、何でしょうか?
public class Main { public static void main(String[] args) { String str1 = "hello world"; String str2 = "hello world"; System.out.println(str1 == str2); } }
正解は、 true です。
]$ javac Main.java ]$ java Main true
解説
参照型の比較をするときに==
を利用すると、同一インスタンスを指しているか、の比較になってしまうため、 equals
メソッド を使う必要がある、と習ったと思います。そのため、この結果は少し不思議ではないでしょうか。(不思議じゃない人はお疲れ様です)
なぜ 参照型の比較を==
で実施した結果が true になるのでしょうか?
それは、String型は頻繁に使われるクラスなので、言語仕様レベルで特別な扱いを受けているためです。例えば、構文中で "hello world"
と宣言できることからも明らかな通り、文字列リテラルです。
そして大抵のリテラルは、コンスタントプールで管理されます。
参考 [Chapter 3. Compiling for the Java Virtual Machine] (https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-3.html#jvms-3.4)
参考 Javaメモリ勉強会
コンスタントプール
Javaでインスタンスを生成する(new
演算子を使う)と、ヒープ領域にインスタンスが確保されます。
参考 Javaメモリ勉強会
しかし、コンパイル時に静的に決まる情報(定数やメソッドに関する情報)もあります。これらの情報が、コンスタントプールで管理されます。
String型は immutable なクラスのため、インスタンスの状態が生成時から変更されません。そのため文字列リテラルは、コンスタントプールを利用して、同一インスタンスが利用されるようになっています。
参考 [Chapter 3. Compiling for the Java Virtual Machine] (https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-3.html#jvms-3.4)
問題その2 のクラスファイルを javap
にかけてみると、
... Constant pool: #1 = Methodref #6.#19 // java/lang/Object."<init>":()V #2 = String #20 // hello world #3 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/Pri ... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: ldc #2 // String hello world 2: astore_1 3: ldc #2 // String hello world 5: astore_2 6: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 9: aload_1 10: aload_2 ...
コンスタントプールに hello world
が格納されており、その値をmain
メソッドで利用している様子がわかります。
参考 ldc命令
また、コンスタントプールに追加する(プールにすでに存在する場合は参照を取得)APIも用意されています。
public class Main { public static void main(String[] args) { String str1 = new String("hello world"); String str2 = new String("hello world"); System.out.println(str1.intern() == str2.intern()); // true } }
問題その3
では、以下のコードの実行結果は、何でしょうか?
public class Main { public static void main(String[] args) { Long l1 = 1L; Long l2 = 1L; System.out.println(l1 == l2); } }
正解は、 true です。
]$ javac Main.java ]$ java Main true
問題その4
では、以下のコードの実行結果は、何でしょうか?
public class Main { public static void main(String[] args) { Long l1 = -999L; Long l2 = -999L; System.out.println(l1 == l2); } }
正解は、 false です。
]$ javac Main.java ]$ java Main false
解説
またコンスタントプールなのでしょうか?いいえ、今回は違います。
問題その3 のクラスファイルを javap
にかけてみると、
... public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: lconst_1 1: invokestatic #2 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 4: astore_1 5: lconst_1 6: invokestatic #2 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long; 9: astore_2 10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; ...
autoboxing 時に Long.valueOf
が呼ばれてることがわかります。
この API を見ると、以下のような記載がされています。
public static Long valueOf?(long l) 指定されたlong値を表すLongインスタンスを返します。 新規Longインスタンスが不要な場合、通常このメソッドがコンストラクタLong(long)に優先して使用されます。その理由は、このメソッドが頻繁に要求される値をキャッシュするので、操作に必要な領域や時間がはるかに少なくて済む場合が多いためです。 このメソッドは、-128から127の範囲(両端含む)の値を常にキャッシュしますが、この範囲に含まれないその他の値をキャッシュすることもあります。
参考 JavaSE11 API Reference Long#valueOf(long))
ある範囲まではプールされたインスタンスが利用されるようです。 しかしこれは、JVM実装レベルではなくクラス実装レベルの工夫です。
最後に
勉強がてら、ネタとして問題を作成しました。
参照型で同一値かどうかを比較したければ、上記のことはすべて忘れて equals
メソッド を使いましょう。言いたいことはそれだけです。