Java オブジェクトの比較に関するひっかけ問題

検証環境

]$ 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メソッド を使いましょう。言いたいことはそれだけです。