Сравнение строк с ==, которые объявлены окончательными в Java

У меня есть простой вопрос о строках в Java. Следующий сегмент простого кода просто конкатенирует две строки и затем сравнивает их с ==.

String str1="str";
String str2="ing";
String concat=str1+str2;

System.out.println(concat=="string");

Выражение сравнения concat=="string" возвращает false как очевидное (я понимаю разницу между equals() и ==).


Если эти две строки объявлены как final так,

final String str1="str";
final String str2="ing";
String concat=str1+str2;

System.out.println(concat=="string");

Выражение сравнения concat=="string", в этом случае возвращает true. Почему final имеет значение? Нужно ли что-то делать с помощью пула или просто вводить в заблуждение?

Ответ 1

Когда вы объявляете переменную String (которая является неизменной) как final и инициализируйте ее с помощью выражения константы времени компиляции, оно также становится выражением постоянной времени компиляции, и его значение встроено компилятором где он используется. Итак, во втором примере кода, после вставки значений, конкатенация строк транслируется компилятором на:

String concat = "str" + "ing";  // which then becomes `String concat = "string";`

который по сравнению с "string" даст вам true, потому что строковые литералы интернированы.

Из JLS & sect; 4.12.4 - final Переменные:

Переменная примитивного типа или типа String, то есть final и инициализированная выражением константы времени компиляции (§15.28), называется постоянной переменной.

Также из JLS & sect; 15.28 - Константное выражение:

Выражения константы времени компиляции типа String всегда "интернированы", чтобы обмениваться уникальными экземплярами, используя метод String#intern().


Это не так в первом примере кода, где переменные String не являются final. Таким образом, они не являются постоянными выражениями времени компиляции. Операция конкатенации будет отложена до времени выполнения, что приведет к созданию нового объекта String. Вы можете проверить это, сравнив байтовый код обоих кодов.

Первый пример кода (не final версия) скомпилирован для следующего байтового кода:

  Code:
   0:   ldc     #2; //String str
   2:   astore_1
   3:   ldc     #3; //String ing
   5:   astore_2
   6:   new     #4; //class java/lang/StringBuilder
   9:   dup
   10:  invokespecial   #5; //Method java/lang/StringBuilder."<init>":()V
   13:  aload_1
   14:  invokevirtual   #6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   17:  aload_2
   18:  invokevirtual   #6; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   21:  invokevirtual   #7; //Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   24:  astore_3
   25:  getstatic       #8; //Field java/lang/System.out:Ljava/io/PrintStream;
   28:  aload_3
   29:  ldc     #9; //String string
   31:  if_acmpne       38
   34:  iconst_1
   35:  goto    39
   38:  iconst_0
   39:  invokevirtual   #10; //Method java/io/PrintStream.println:(Z)V
   42:  return

Очевидно, что он хранит str и ing в двух отдельных переменных и использует StringBuilder для выполнения операции конкатенации.

В то время как ваш второй пример кода (final version) выглядит следующим образом:

  Code:
   0:   ldc     #2; //String string
   2:   astore_3
   3:   getstatic       #3; //Field java/lang/System.out:Ljava/io/PrintStream;
   6:   aload_3
   7:   ldc     #2; //String string
   9:   if_acmpne       16
   12:  iconst_1
   13:  goto    17
   16:  iconst_0
   17:  invokevirtual   #4; //Method java/io/PrintStream.println:(Z)V
   20:  return

Поэтому он непосредственно вставляет конечную переменную для создания String String во время компиляции, которая загружается с помощью операции ldc на шаге 0. Затем второй строковый литерал загружается с помощью операции ldc на шаге 7. Это не связано с созданием какого-либо нового объекта String во время выполнения. Строка уже известна во время компиляции, и они интернированы.

Ответ 2

Согласно моему исследованию, все final String интернированы в Java. Из одного из сообщений в блоге:

Итак, если вам действительно нужно сравнить две строки, используя == или! =, перед вызовом сравнения вы вызываете метод String.intern(). В противном случае всегда предпочитайте String.equals(String) для сравнения строк.

Значит, если вы вызываете String.intern(), вы можете сравнить две строки с помощью оператора ==. Но здесь String.intern() не требуется, потому что в Java final String внутренне интернированы.

Вы можете найти дополнительную информацию Сравнение строк с использованием оператора == и Javadoc для String.intern().

Также обратитесь к этому сообщению fooobar.com/questions/18552/... за дополнительной информацией.

Ответ 3

Если вы посмотрите на эти методы

public void noFinal() {
    String str1 = "str";
    String str2 = "ing";
    String concat = str1 + str2;

    System.out.println(concat == "string");
}

public void withFinal() {
    final String str1 = "str";
    final String str2 = "ing";
    String concat = str1 + str2;

    System.out.println(concat == "string");
}

и его декомпилировать с помощью javap -c ClassWithTheseMethods версии вы увидите

  public void noFinal();
    Code:
       0: ldc           #15                 // String str
       2: astore_1      
       3: ldc           #17                 // String ing
       5: astore_2      
       6: new           #19                 // class java/lang/StringBuilder
       9: dup           
      10: aload_1       
      11: invokestatic  #21                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
      14: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
      17: aload_2       
      18: invokevirtual #30                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: invokevirtual #34                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      ...

и

  public void withFinal();
    Code:
       0: ldc           #15                 // String str
       2: astore_1      
       3: ldc           #17                 // String ing
       5: astore_2      
       6: ldc           #44                 // String string
       8: astore_3      
       ...

Итак, если строки не являются окончательными, компилятор должен будет использовать StringBuilder для объединения str1 и str2, поэтому

String concat=str1+str2;

будет скомпилирован в

String concat = new StringBuilder(str1).append(str2).toString();

что означает, что concat будет создан во время выполнения, поэтому он не будет получен из пула строк.


Кроме того, если строки являются окончательными, то компилятор может предположить, что они никогда не изменятся, поэтому вместо использования StringBuilder он может смежно объединить свои значения, чтобы

String concat = str1 + str2;

можно изменить на

String concat = "str" + "ing";

и конкатенируется в

String concat = "string";

что означает, что concate станет строгим литералом, который будет интернирован в пуле строк, а затем сравним с тем же строковым литералом из этого пула в инструкции if.

Ответ 4

Концепция пула и строки conts pool enter image description here

Ответ 5

Посмотрите некоторый байт-код для примера final

Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: ldc           #2                  // String string
       2: astore_3
       3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: aload_3
       7: ldc           #2                  // String string
       9: if_acmpne     16
      12: iconst_1
      13: goto          17
      16: iconst_0
      17: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      20: return
}

В 0: и 2:, String "string" помещается в стек (из пула констант) и сохраняется непосредственно в локальной переменной concat. Вы можете сделать вывод о том, что компилятор создает (конкатенирует) сам String "string" во время компиляции.

Не final байтовый код

Compiled from "Main2.java"
public class Main2 {
  public Main2();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.Exception;
    Code:
       0: ldc           #2                  // String str
       2: astore_1
       3: ldc           #3                  // String ing
       5: astore_2
       6: new           #4                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
      17: aload_2
      18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/Stri
ngBuilder;
      21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      24: astore_3
      25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
      28: aload_3
      29: ldc           #9                  // String string
      31: if_acmpne     38
      34: iconst_1
      35: goto          39
      38: iconst_0
      39: invokevirtual #10                 // Method java/io/PrintStream.println:(Z)V
      42: return
}

Здесь у вас есть две константы String, "str" и "ing", которые необходимо объединить во время выполнения с помощью StringBuilder.

Ответ 6

Хотя при создании с использованием строковой литературной нотации Java он автоматически вызывает метод intern(), чтобы поместить этот объект в пул строк, если он уже не присутствовал в пуле.

Почему окончательное значение имеет значение?

Компилятор знает, что конечная переменная никогда не изменится, когда мы добавим эти конечные переменные, выход идет в String Pool из-за str1 + str2 вывода выражения также никогда не изменится, поэтому компилятор вызывает inter method after вывод двух последних конечных переменных. В случае не конечного компилятора переменных не вызывайте метод intern.