Порівняння рядків з ==, які оголошені остаточними в 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. Якщо ви використовуєте рівняння для двох об'єктів, ви повинні знати, чи може бути нульовим або ви ризикуєте перекинути 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 не інтернувати остаточну строкову версію?
Елвін

13
@Alvin JLS вимагає, щоб постійні вирази строків компіляції були інтерновані. Будь-яка відповідна реалізація повинна робити те саме.
Тавіан Барнс

І навпаки, чи відповідає JLS, що компілятор не повинен оптимізувати конкатенацію в першій, не остаточній версії? Чи забороняється компілятору створювати код, який би оцінював порівняння true?
phant0m

1
@ Phant0m приймає поточну формулювання специфікації , « новостворювані об'єкти (§12.5) , якщо вираз не є постійним виразом (§15.28). ”Буквально, застосування оптимізації в не остаточній версії не дозволено, оскільки“ новостворена ”рядок повинна мати іншу ідентичність об’єкта. Я не знаю, чи це навмисно. Зрештою, поточна стратегія компіляції полягає в делегуванні механізму виконання, який не документує таких обмежень. String
Холгер

31

Згідно з моїми дослідженнями, всі final Stringінтерновані на Яві. З однієї з публікацій щоденника:

Отже, якщо вам дійсно потрібно порівняти два рядки, використовуючи == або! =, Перед тим, як порівняти, переконайтесь, що ви викликаєте метод String.intern (). В іншому випадку завжди віддайте перевагу String.equals (String) для порівняння String.

Так що це означає, що якщо ви телефонуєте, String.intern()ви можете порівняти два рядки за допомогою ==оператора. Але String.intern()це не потрібно, тому що у Java final Stringвнутрішньо інтерновані.

Додаткову інформацію для порівняння рядків можна знайти за допомогою оператора == та методу Javadoc for String.intern () .

Також зверніться до цієї публікації Stackoverflow для отримання додаткової інформації.


3
рядки intern () не збираються сміттям, і вони зберігаються в пермгенському просторі, який є низьким, тому ви можете потрапити в проблеми, як помилка пам'яті, якщо не використовувати її належним чином.
Ajeesh

@Ajeesh - Інтерновані струни можна збирати сміттям. Навіть інтерновані рядки в результаті постійних виразів можуть бути сміттям, зібраним за певних обставин.
Stephen C

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буде створено під час виконання, тому не надходитиме з пулу String.


Крім того, якщо Strings є остаточним, тоді компілятор може припустити, що вони ніколи не зміниться, тому замість його використання StringBuilderможна безпечно об'єднати його значення

String concat = str1 + str2;

може бути змінено на

String concat = "str" + "ing";

і об'єдналися в

String concat = "string";

що означає, що concateвін стане sting literal, який буде інтернований у рядок ряду, а потім порівнюється з тим самим рядковим літералом з цього пулу у ifзаяві.


15

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


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

Хоча, коли ви створюєте за допомогою Stral literal позначення Java, він автоматично викликає метод intern (), щоб помістити цей об'єкт у String пул, за умови, що він вже не був у пулі.

Чому фінал має значення?

Компілятор знає, що остаточна змінна ніколи не зміниться, коли ми додамо ці кінцеві змінні, результат виходить до String Pool, оскільки str1 + str2виведення виразів також ніколи не зміниться, тому нарешті компілятор викликає інтер-метод після виведення вищевказаних двох остаточних змінних. У разі незавершення компілятора змінної не викликайте метод інтерну.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.