Porównywanie ciągów z ==, które są zadeklarowane jako końcowe w Javie


220

Mam proste pytanie dotyczące napisów w Javie. Następny segment prostego kodu po prostu łączy dwa ciągi, a następnie porównuje je ==.

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

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

Wyrażenie porównania concat=="string"zwraca falsejako oczywiste (rozumiem różnicę między equals()i ==).


Gdy te dwa ciągi zostaną zadeklarowane w ten finalsposób,

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

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

Wyrażenie porównania concat=="string"w tym przypadku zwraca true. Dlaczego robi finalróżnicę? Czy to musi coś zrobić z pulą stażystów, czy jestem po prostu wprowadzany w błąd?


22
Zawsze uważałem za głupie, że równość była domyślnym sposobem sprawdzania równej zawartości, zamiast == zrobić to i po prostu użyć referenceEquals lub czegoś podobnego, aby sprawdzić, czy wskaźniki są takie same.
Davio

25
To nie jest duplikat „Jak porównać ciągi znaków w Javie?” w jakikolwiek sposób. OP rozumie różnicę pomiędzy łańcuchami equals()i ==w ich kontekście i zadaje bardziej sensowne pytanie.
arshajii

@Davio Ale jak by to działało, gdy klasa nie jest String? Myślę, że bardzo logiczne, a nie głupie jest przeprowadzanie porównania zawartości equals, metoda, którą możemy zastąpić, aby stwierdzić, kiedy uważamy dwa obiekty za równe, i wykonanie porównania tożsamości ==. Gdyby porównanie zawartości zostało wykonane przez, ==nie moglibyśmy pominąć tego, aby zdefiniować, co rozumiemy przez „równe treści”, a posiadanie znaczenia equalsi ==odwrócenie tylko dla Strings byłoby głupie. Niezależnie od tego, nie widzę żadnej przewagi w ==porównaniu zawartości equals.
SantiBailors

@ SantiBailors masz rację, że tak właśnie działa w Javie, użyłem również C # gdzie == jest przeciążony dla równości treści. Dodatkową korzyścią z używania == jest to, że jest zerowo bezpieczny: (null == „coś”) zwraca false. Jeśli użyjesz równości dla 2 obiektów, musisz wiedzieć, czy którykolwiek z nich może mieć wartość NULL lub ryzykujesz zgubieniem wyjątku NullPointerException.
Davio,

Odpowiedzi:


232

Gdy zadeklarujesz zmienną String(która jest niezmienna ) jako finali zainicjujesz ją wyrażeniem stałym czasu kompilacji, stanie się ono również wyrażeniem stałym czasu kompilacji, a jego wartość zostanie podkreślona przez kompilator, w którym jest używana. Zatem w drugim przykładzie kodu, po wstawieniu wartości, kompilacja łańcucha jest tłumaczona przez kompilator na:

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

co w porównaniu do "string"da ci true, ponieważ literały łańcuchowe są internowane .

Z JLS §4.12.4 - finalZmienne :

Zmienna pierwotnego typu lub typu String, która jest finalinicjowana za pomocą wyrażenia stałego w czasie kompilacji (§ 15.28), nazywana jest zmienną stałą .

Również z JLS § 15.28 - Wyrażenie stałe:

Wyrażenia typu stałego w czasie kompilacji Stringsą zawsze „internowane”, tak aby współużytkować unikalne instancje przy użyciu tej metody String#intern().


Nie jest tak w pierwszym przykładzie kodu, w którym Stringzmienne nie są final. Nie są to zatem wyrażenia stałe czasu kompilacji. Operacja konkatenacji zostanie opóźniona do czasu wykonania, co spowoduje utworzenie nowego Stringobiektu. Możesz to sprawdzić, porównując kod bajtowy obu kodów.

Pierwszy przykład kodu (nie w finalwersji) jest kompilowany do następującego kodu bajtowego:

  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

Oczywiste jest przechowywanie stri ingw dwóch różnych zmiennych, a przy użyciu StringBuilderwykonać operację konkatenacji.

Natomiast twój drugi przykład kodu ( finalwersja) wygląda następująco:

  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

Więc bezpośrednio wstawia ostatnią zmienną do utworzenia ciągu stringw czasie kompilacji, który jest ładowany przez ldcoperację w kroku 0. Następnie drugi ldckrok literału jest ładowany przez operację w kroku 7. Nie wymaga tworzenia żadnego nowego Stringobiektu w czasie wykonywania. Łańcuch jest już znany w czasie kompilacji i jest internowany.


2
Nic nie stoi na przeszkodzie, aby inna implementacja kompilatora Java nie internowała końcowego ciągu, prawda?
Alvin

13
@Alvin JLS wymaga, aby wyrażenia ciągłe w czasie kompilacji były internowane. Każda zgodna implementacja musi zrobić to samo.
Tavian Barnes

I odwrotnie, czy JLS nakazuje, aby kompilator nie optymalizował konkatenacji w pierwszej, nie finalnej wersji? Czy kompilatorowi nie wolno wytwarzać kodu, który oceniałby porównanie true?
phant0m

1
@ phant0m przy dotychczasowej treści specyfikacjiobiektu nowo utworzone (§12.5), o ile ekspresja jest stała wyrażenie (§15.28). ”Dosłownie stosowanie optymalizacji w nie finalnej wersji jest niedozwolone, ponieważ„ nowo utworzony ”ciąg musi mieć inną tożsamość obiektu. Nie wiem, czy to celowe. W końcu obecna strategia kompilacji polega na delegowaniu do narzędzia wykonawczego, które nie dokumentuje takich ograniczeń. String
Holger

31

Według moich badań wszystkie final Stringsą internowane w Javie. Z jednego z postów na blogu:

Tak więc, jeśli naprawdę musisz porównać dwa String za pomocą == lub! = Upewnij się, że wywołujesz metodę String.intern () przed dokonaniem porównania. W przeciwnym razie zawsze preferuj String.equals (String) do porównywania String.

Oznacza to, że jeśli zadzwonisz String.intern(), możesz porównać dwa ciągi za pomocą ==operatora. Ale tutaj String.intern()nie jest to konieczne, ponieważ w Javie final Stringsą wewnętrznie internalizowane.

Możesz znaleźć więcej informacji Porównanie ciągów znaków za pomocą operatora == i Javadoc dla metody String.intern () .

Zobacz także ten post Stackoverflow, aby uzyskać więcej informacji.


3
Ciągi intern () nie są odśmiecane i są przechowywane w przestrzeni permgenowej, która jest niska, więc będziesz miał kłopoty, takie jak błąd braku pamięci, jeśli nie zostanie właściwie użyty.
Ajeesh

@Ajeesh - Wewnętrzne ciągi mogą być usuwane. Nawet internowane łańcuchy wynikające ze stałych wyrażeń mogą być w niektórych przypadkach odśmiecane.
Stephen C

21

Jeśli spojrzysz na te metody

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");
}

i jego dekompilacja z javap -c ClassWithTheseMethods wersjami, które zobaczysz

  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;
      ...

i

  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      
       ...

Więc jeśli ciągi nie są ostateczne, kompilator będzie musiał użyć StringBuilderdo połączenia str1i str2tak

String concat=str1+str2;

zostanie skompilowany do

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

co oznacza, że concatzostaną utworzone w czasie wykonywania, więc nie będą pochodzić z puli ciągów.


Również jeśli ciągi są ostateczne, kompilator może założyć, że nigdy się nie zmieni, więc zamiast StringBuildergo używać , może bezpiecznie połączyć swoje wartości, aby

String concat = str1 + str2;

można zmienić na

String concat = "str" + "ing";

i połączone w

String concat = "string";

co oznacza, że concatestanie się literałem żądła, które zostanie internowane w puli łańcuchów, a następnie porównane z tym samym literałem łańcucha z tej puli w ifinstrukcji.


15

Stos i ciąg conts koncepcja puli wprowadź opis zdjęcia tutaj


6
Co? Nie rozumiem, jak to się cieszy. Czy możesz wyjaśnić swoją odpowiedź?
Cᴏʀʏ

Myślę, że zamierzoną odpowiedzią jest to, że ponieważ str1 + str2 nie jest zoptymalizowany do łańcucha wewnętrznego, porównanie z łańcuchem z puli ciągów doprowadzi do stanu fałszywego.
viki.omega9

3

Zobaczmy na finalprzykład kod bajtowy

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
}

At 0:i 2:, String "string"jest wypychany na stos (ze stałej puli) i zapisywany concatbezpośrednio w zmiennej lokalnej . Można wywnioskować, że kompilator sam tworzy (konkatenuje) String "string"w czasie kompilacji.

Non finalkod bajtowy

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
}

Tutaj masz dwie Stringstałe, "str"a "ing"które muszą być łączone przy starcie z StringBuilder.


0

Chociaż podczas tworzenia za pomocą notacji Javy literału ciągowego, automatycznie wywołuje metodę intern () w celu umieszczenia tego obiektu w puli ciągów, pod warunkiem, że nie był już obecny w puli.

Dlaczego finał robi różnicę?

Kompilator wie, że końcowa zmienna nigdy się nie zmieni, kiedy dodamy te zmienne końcowe, dane wyjściowe trafiają do puli ciągów, ponieważ str1 + str2dane wyjściowe również nigdy się nie zmienią, więc w końcu kompilator wywołuje metodę inter po wyjściu powyższych dwóch zmiennych końcowych. W przypadku niekończącego kompilatora zmiennych nie należy wywoływać metody intern.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.