Vergleichen von Zeichenfolgen mit ==, die in Java als endgültig deklariert sind

220

Ich habe eine einfache Frage zu Zeichenfolgen in Java. Das folgende Segment einfachen Codes verkettet nur zwei Zeichenfolgen und vergleicht sie dann mit ==.

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

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

Der Vergleichsausdruck concat=="string"kehrt falseals offensichtlich (ich verstehe den Unterschied zwischen equals()und ==).


Wenn diese beiden Zeichenfolgen so deklariert finalsind,

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

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

Der Vergleichsausdruck concat=="string"gibt in diesem Fall zurück true. Warum macht finaldas einen Unterschied? Hat es etwas mit dem Praktikantenpool zu tun oder werde ich nur irregeführt?

Sehr klein
quelle
22
Ich fand es immer albern, dass gleich die Standardmethode ist, um nach gleichen Inhalten zu suchen, anstatt == dies zu tun und einfach referenceEquals oder ähnliches zu verwenden, um zu überprüfen, ob die Zeiger gleich sind.
Davio
25
Dies ist kein Duplikat von "Wie vergleiche ich Zeichenfolgen in Java?" in irgendeiner Weise. Das OP versteht den Unterschied zwischen equals()und ==im Kontext von Zeichenfolgen und stellt eine aussagekräftigere Frage.
Arshajii
@ David Aber wie würde das funktionieren, wenn die Klasse nicht ist String? Ich denke, es ist sehr logisch, nicht albern, den Inhaltsvergleich durchführen zu lassen equals, eine Methode, die wir überschreiben können, um festzustellen, wann zwei Objekte gleich sind, und um den Identitätsvergleich durchführen zu lassen ==. Wenn der Inhaltsvergleich von durchgeführt würde ==, könnten wir dies nicht überschreiben, um zu definieren, was wir unter "gleichem Inhalt" verstehen, und die Bedeutung von equalsund ==nur für Strings umgekehrt zu haben, wäre dumm. Unabhängig davon sehe ich ohnehin keinen Vorteil darin, ==stattdessen den Inhaltsvergleich durchzuführen equals.
SantiBailors
@SantiBailors Sie haben Recht, dass dies in Java genau so funktioniert. Ich habe auch C # verwendet, wobei == aus Gründen der Inhaltsgleichheit überladen ist. Ein zusätzlicher Bonus bei der Verwendung von == ist, dass es null-sicher ist: (null == "etwas") gibt false zurück. Wenn Sie für 2 Objekte gleich verwenden, müssen Sie sich darüber im Klaren sein, ob entweder null sein kann oder ob Sie das Risiko eingehen, dass eine NullPointerException ausgelöst wird.
Davio

Antworten:

232

Wenn Sie eine String( unveränderliche ) Variable als deklarieren finalund sie mit einem Konstantenausdruck zur Kompilierungszeit initialisieren, wird sie auch zu einem Konstantenausdruck zur Kompilierungszeit, und ihr Wert wird vom Compiler, in dem sie verwendet wird, eingefügt. In Ihrem zweiten Codebeispiel wird die Zeichenfolgenverkettung nach dem Inlinieren der Werte vom Compiler in Folgendes übersetzt:

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

was im Vergleich zu "string"gibt true, weil String-Literale interniert sind .

Aus JLS §4.12.4 - finalVariablen :

Eine Variable vom primitiven Typ oder Typ String, die finalmit einem konstanten Ausdruck zur Kompilierungszeit (§15.28) initialisiert wird, wird als konstante Variable bezeichnet .

Auch aus JLS §15.28 - Konstanter Ausdruck:

Konstante Ausdrücke des Typs Stringzur Kompilierungszeit werden immer "interniert" , um mithilfe der Methode eindeutige Instanzen gemeinsam zu nutzen String#intern().


Dies ist in Ihrem ersten Codebeispiel nicht der Fall, in dem die StringVariablen nicht vorhanden sind final. Sie sind also keine konstanten Ausdrücke zur Kompilierungszeit. Der dortige Verkettungsvorgang wird bis zur Laufzeit verzögert, wodurch ein neues StringObjekt erstellt wird. Sie können dies überprüfen, indem Sie den Bytecode beider Codes vergleichen.

Das erste Codebeispiel (Nichtversion final) wird mit dem folgenden Bytecode kompiliert:

  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

Es ist klar, dass es in zwei separaten Variablen gespeichert strund ingverwendet wird StringBuilder, um die Verkettungsoperation auszuführen.

Ihr zweites Codebeispiel ( finalVersion) sieht folgendermaßen aus:

  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

Daher wird die endgültige Variable direkt eingefügt, um stringzur Kompilierungszeit einen String zu erstellen, der durch die ldcschrittweise Operation geladen wird 0. Dann wird das zweite String-Literal durch ldcschrittweise Operation geladen 7. Zur StringLaufzeit wird kein neues Objekt erstellt. Der String ist bereits zur Kompilierungszeit bekannt und wird interniert.

Rohit Jain
quelle
2
Nichts hindert andere Java-Compiler-Implementierungen daran, einen endgültigen String nicht zu internieren, oder?
Alvin
13
@Alvin the JLS erfordert, dass konstante Zeichenfolgenausdrücke zur Kompilierungszeit interniert werden. Jede konforme Implementierung muss hier dasselbe tun.
Tavian Barnes
Fordert das JLS umgekehrt, dass ein Compiler die Verkettung in der ersten, nicht endgültigen Version nicht optimieren darf ? Ist es dem Compiler verboten, Code zu erstellen, mit dem der Vergleich ausgewertet werden soll ? true
Phant0m
1
@ phant0m übernimmt den aktuellen Wortlaut der Spezifikation : „ Das StringObjekt wird neu erstellt (§12.5), es sei denn, der Ausdruck ist ein konstanter Ausdruck (§15.28). Das Anwenden einer Optimierung in der nicht endgültigen Version ist buchstäblich nicht zulässig, da eine neu erstellte Zeichenfolge eine andere Objektidentität haben muss. Ich weiß nicht, ob das beabsichtigt ist. Schließlich besteht die aktuelle Kompilierungsstrategie darin, an eine Laufzeiteinrichtung zu delegieren, die solche Einschränkungen nicht dokumentiert.
Holger
31

Nach meinen Recherchen sind alle final Stringin Java interniert. Aus einem der Blog-Beiträge:

Wenn Sie also wirklich zwei Zeichenfolgen mit == oder! = Vergleichen müssen, rufen Sie die Methode String.intern () auf, bevor Sie einen Vergleich durchführen. Andernfalls bevorzugen Sie immer String.equals (String) für den String-Vergleich.

Wenn Sie also aufrufen String.intern(), können Sie zwei Zeichenfolgen mit dem ==Operator vergleichen. Dies ist hier String.intern()jedoch nicht erforderlich, da in Java final Stringintern interniert sind.

Weitere Informationen finden Sie unter String-Vergleich mit dem Operator == und der Methode Javadoc für String.intern () .

Weitere Informationen finden Sie auch in diesem Stackoverflow- Beitrag.

Pradeep Simha
quelle
3
intern () -Strings werden nicht durch Müll gesammelt und in einem geringen Permgen-Speicherplatz gespeichert, sodass Sie bei nicht ordnungsgemäßer Verwendung in Schwierigkeiten geraten, z. B. aufgrund eines Speichermangels .
Ajeesh
@Ajeesh - Internierte Zeichenfolgen können durch Müll gesammelt werden. Sogar internierte Zeichenfolgen, die aus konstanten Ausdrücken resultieren, können unter bestimmten Umständen als Müll gesammelt werden.
Stephen C
21

Wenn Sie sich diese Methoden ansehen

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

und javap -c ClassWithTheseMethods es wird mit Versionen dekompiliert, die Sie sehen werden

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

und

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

Wenn Strings also nicht endgültig sind, muss der Compiler StringBuilderzum Verketten verwenden str1und str2so weiter

String concat=str1+str2;

wird zu kompiliert

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

Dies bedeutet, dass concates zur Laufzeit erstellt wird und nicht aus dem String-Pool stammt.


Auch wenn Strings endgültig sind, kann der Compiler davon ausgehen, dass sie sich niemals ändern, sodass er anstelle seiner Verwendung StringBuilderseine Werte sicher verketten kann

String concat = str1 + str2;

kann geändert werden zu

String concat = "str" + "ing";

und verkettet in

String concat = "string";

Dies bedeutet, dass dies concatezu einem Sting-Literal wird, das im String-Pool interniert und dann mit demselben String-Literal aus diesem Pool in der ifAnweisung verglichen wird .

Pshemo
quelle
15

Pool-Konzept für Stapel- und String-Conts Geben Sie hier die Bildbeschreibung ein

pnathan
quelle
6
Was? Ich verstehe nicht, wie dies positive Stimmen hat. Können Sie Ihre Antwort klarstellen?
22.
Ich denke, die beabsichtigte Antwort lautet: Da str1 + str2 nicht für einen internierten String optimiert ist, führt der Vergleich mit einem String aus dem String-Pool zu einer falschen Bedingung.
viki.omega9
3

Sehen wir uns einen Bytecode für das finalBeispiel an

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
}

Bei 0:und 2:wird das String "string"auf den Stapel (aus dem konstanten Pool) geschoben und concatdirekt in der lokalen Variablen gespeichert . Sie können daraus schließen, dass der Compiler zur Kompilierungszeit das Selbst erstellt (verkettet) String "string".

Der Nicht- finalByte-Code

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
}

Hier haben Sie zwei StringKonstanten, "str"und "ing"die müssen zur Laufzeit mit einer verketteten werden StringBuilder.

Sotirios Delimanolis
quelle
0

Wenn Sie mit der String-Literal-Notation von Java erstellen, wird automatisch die intern () -Methode aufgerufen, um dieses Objekt in den String-Pool einzufügen, sofern es nicht bereits im Pool vorhanden war.

Warum macht final einen Unterschied?

Der Compiler weiß, dass sich die endgültige Variable niemals ändern wird. Wenn wir diese endgültigen Variablen hinzufügen, wird die Ausgabe in den String-Pool verschoben, da sich auch die str1 + str2Ausdrucksausgabe niemals ändert. Schließlich ruft der Compiler nach der Ausgabe der beiden oben genannten endgültigen Variablen die Methode inter auf. Rufen Sie im Falle eines nicht endgültigen Variablen-Compilers die interne Methode nicht auf.

Premraj
quelle