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

220

У меня есть простой вопрос о строках в 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значение? Это как-то связано со стажировкой или меня просто вводят в заблуждение?

крошечный
источник
22
Я всегда считал глупым, что равенство было способом проверки на равное содержимое по умолчанию вместо того, чтобы делать это == и просто использовать referenceEquals или что-то подобное, чтобы проверить, совпадают ли указатели.
Давио
25
Это не дубликат «Как сравнить строки в Java?» в любом случае. ОП понимает разницу между equals()и ==в контексте строк и задает более значимый вопрос.
Аршаджи
@Davio Но как это будет работать, если урок не String? Я думаю, что вполне логично, а не глупо проводить сравнение содержимого equals, метод, который мы можем переопределить, чтобы сообщить, когда мы считаем два объекта равными, и выполнить сравнение идентификаторов ==. Если бы сравнение содержимого было выполнено, ==мы не могли бы переопределить это, чтобы определить, что мы подразумеваем под «равным содержанием», и иметь значение equalsи ==обращенное только для Strings было бы глупо. Кроме того, независимо от этого, я не вижу никакого преимущества в том ==, чтобы сравнивать содержимое вместо equals.
SantiBailors
@SantiBailors вы правы, что именно так это работает в Java, я также использовал C #, где == перегружен для равенства содержимого. Дополнительным бонусом использования == является то, что он безопасен на нуль: (null == "что-то") возвращает false. Если вы используете равно для 2 объектов, вы должны знать, может ли оно быть нулевым, или вы рискуете вызвать исключение NullPointerException.
Давио

Ответы:

232

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

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

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

Из JLS §4.12.4 - finalПеременные :

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

Также из JLS §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версия) выглядит так:

  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
Ничто не мешает другим реализациям Java-компилятора не интернировать окончательный вариант String?
Элвин
13
@ Alvin JLS требует, чтобы постоянные строковые выражения во время компиляции были интернированы. Любая соответствующая реализация должна делать то же самое здесь.
Тавиан Барнс
И наоборот, предписывает ли JLS, чтобы компилятор не оптимизировал конкатенацию в первой, не финальной версии? Запрещено ли компилятору создавать код, который будет сравнивать true?
phant0m
1
@ phant0m принимает текущую формулировку спецификации , « вновь создаваемые объекты (§12.5) , если выражение не является постоянным выражением (§15.28). «Буквально применение оптимизации в не финальной версии недопустимо, так как« вновь созданная »строка должна иметь другую идентичность объекта. Я не знаю, намеренно ли это. В конце концов, текущая стратегия компиляции заключается в делегировании средству выполнения, которое не документирует такие ограничения. String
Хольгер
31

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

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

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

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

Также обратитесь к этому сообщению Stackoverflow для получения дополнительной информации.

Прадип Симха
источник
3
Строки intern () не являются сборщиком мусора, и они хранятся в пространстве permgen, которое является небольшим, поэтому вы будете сталкиваться с проблемами, такими как нехватка памяти, если не используются должным образом.
Ajeesh
@Ajeesh - Стажированные строки можно собирать мусором. Даже интернированные строки, являющиеся результатом константных выражений, могут в некоторых случаях собираться мусором.
Стивен С
21

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

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выражении.

Pshemo
источник
15

Концепция пула стековых и строковых контентов введите описание изображения здесь

pnathan
источник
6
Какой? Я не понимаю, как это имеет положительные отзывы. Вы можете уточнить свой ответ?
Cᴏʀʏ
Я думаю, что предполагаемый ответ таков: поскольку str1 + str2 не оптимизирован для интернированной строки, сравнение со строкой из пула строк приведет к ложному условию.
viki.omega9
3

Давайте посмотрим некоторый байт-код для 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.

Сотириос Делиманолис
источник
0

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

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

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

Premraj
источник