การเปรียบเทียบสตริงด้วย == ซึ่งประกาศเป็นครั้งสุดท้ายใน 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 หรือสิ่งที่คล้ายกันเพื่อตรวจสอบว่าตัวชี้เหมือนกัน
Davio

25
นี่ไม่ใช่สิ่งที่ซ้ำกันของ "ฉันจะเปรียบเทียบสตริงใน Java ได้อย่างไร" ในทางใดทางหนึ่ง. OP เข้าใจความแตกต่างระหว่างequals()และ==ในบริบทของสตริงและกำลังถามคำถามที่มีความหมายมากกว่า
arshajii

@Davio แต่วิธีการที่จะทำงานเมื่อชั้นไม่ได้String? ฉันคิดว่ามันเป็นตรรกะมากไม่โง่ที่จะมีการเปรียบเทียบเนื้อหากระทำโดยวิธีการที่เราสามารถแทนที่การที่จะบอกเมื่อเราพิจารณาวัตถุสองเท่ากันและจะมีการเปรียบเทียบตัวตนที่ทำโดยequals ==หากการเปรียบเทียบเนื้อหาทำโดย==เราไม่สามารถแทนที่สิ่งนั้นเพื่อกำหนดสิ่งที่เราหมายถึงโดย "เนื้อหาที่เท่าเทียมกัน" และการมีความหมายequalsและ==ย้อนกลับเฉพาะสำหรับStrings จะโง่ นอกจากนี้ยังคำนึงถึงนี้ผมไม่เห็นประโยชน์ใด ๆ อยู่แล้วในการทำเปรียบเทียบเนื้อหาแทน== equals
SantiBailors

@SantiBailors คุณถูกต้องว่านี่เป็นเพียงวิธีการทำงานใน Java ฉันยังใช้ C # โดยที่ == ถูกโหลดมากเกินไปสำหรับความเท่าเทียมกันของเนื้อหา โบนัสเพิ่มจากการใช้ == คือมันปลอดภัยแล้ว: (null == "บางอย่าง") จะคืนค่าเท็จ ถ้าคุณใช้เท่ากับ 2 วัตถุคุณต้องระวังว่าอาจเป็นโมฆะหรือเสี่ยงต่อการโยน NullPointerException
Davio

คำตอบ:


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

ดังนั้นจึงโดยตรง inlines ตัวแปรสุดท้ายที่จะสร้าง String stringที่รวบรวมเวลาซึ่งเป็นที่โหลดโดยการดำเนินการในขั้นตอนที่ldc 0แล้วตัวอักษรสตริงที่สองคือการโหลดโดยการดำเนินการในขั้นตอนที่ldc 7ไม่เกี่ยวข้องกับการสร้างStringวัตถุใหม่ที่รันไทม์ String รู้จักกันแล้วในเวลารวบรวมและพวกเขาจะฝึกงาน


2
ไม่มีอะไรขัดขวางการใช้งานคอมไพเลอร์ Java อื่น ๆ ที่จะไม่ฝึกสตริงสุดท้ายใช่ไหม?
อัลวิน

13
@Alvin JLS ต้องการให้นิพจน์สตริงคงที่คอมไพล์เวลาคอมไพล์ การใช้งานที่สอดคล้องใด ๆ จะต้องทำสิ่งเดียวกันที่นี่
Tavian Barnes

ในทางกลับกัน JLS มีคำสั่งว่าคอมไพเลอร์จะต้องไม่ปรับการต่อข้อมูลให้เหมาะสมในเวอร์ชันแรกไม่ใช่เวอร์ชันสุดท้ายหรือไม่? คอมไพเลอร์ถูกห้ามจากการสร้างรหัสซึ่งจะมีการประเมินเปรียบเทียบtrueหรือไม่
phant0m

1
@ phant0m การใช้ถ้อยคำปัจจุบันของสเปควัตถุที่สร้างขึ้นใหม่ (§12.5) เว้นแต่จะแสดงออกคือการแสดงออกคงที่ (§15.28) "แท้จริงการใช้การเพิ่มประสิทธิภาพในรุ่นที่ไม่ใช่ครั้งสุดท้ายไม่ได้รับอนุญาตเนื่องจากสตริง" สร้างขึ้นใหม่ "จะต้องมีเอกลักษณ์ของวัตถุที่แตกต่างกัน ฉันไม่รู้ว่านี่เป็นความตั้งใจหรือไม่ ท้ายที่สุดแล้วกลยุทธ์การรวบรวมปัจจุบันคือการมอบสิทธิ์ให้กับศูนย์อำนวยความสะดวกรันไทม์ซึ่งไม่มีเอกสารข้อ จำกัด ดังกล่าว String
Holger

31

ตามการวิจัยของฉันทั้งหมดfinal Stringจะฝึกงานใน Java จากหนึ่งในบล็อกโพสต์:

ดังนั้นหากคุณต้องการเปรียบเทียบสองสตริงโดยใช้ == หรือ! = ตรวจสอบให้แน่ใจว่าคุณเรียกใช้เมธอด String.intern () ก่อนทำการเปรียบเทียบ มิฉะนั้นให้เลือกใช้ String.equals (String) เสมอสำหรับการเปรียบเทียบ String

ดังนั้นหมายความว่าถ้าคุณโทรหาString.intern()คุณสามารถเปรียบเทียบสองสายโดยใช้==โอเปอเรเตอร์ แต่ที่นี่String.intern()ไม่จำเป็นเพราะใน Java final Stringมีการฝึกงานภายใน

ท่านสามารถหาข้อมูลเพิ่มเติมได้เปรียบเทียบ String ใช้ประกอบ ==และ Javadoc สำหรับString.intern ()วิธีการ

อ้างถึงโพสต์Stackoverflowนี้สำหรับข้อมูลเพิ่มเติม


3
สตริงฝึกงาน ()ไม่ได้ถูกเก็บรวบรวมขยะและมันถูกเก็บไว้ในพื้นที่ permgen ที่ต่ำดังนั้นคุณจะได้รับปัญหาเช่นหน่วยความจำผิดพลาดหากไม่ได้ใช้อย่างถูกต้อง
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");
}

และ decompiled กับ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เพื่อ concatenate str1และstr2เพื่อให้

String concat=str1+str2;

จะถูกรวบรวมไปยัง

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

ซึ่งหมายความว่าconcatจะถูกสร้างที่รันไทม์ดังนั้นจะไม่มาจาก String pool


และถ้า Strings เป็นตัวสุดท้ายคอมไพเลอร์สามารถสันนิษฐานได้ว่าพวกมันจะไม่เปลี่ยนแปลงดังนั้นแทนที่จะใช้StringBuilderมันสามารถเชื่อมค่าของมันได้อย่างปลอดภัย

String concat = str1 + str2;

สามารถเปลี่ยนเป็น

String concat = "str" + "ing";

และตัดแบ่งเป็น

String concat = "string";

ซึ่งหมายความว่าconcateจะกลายเป็นตัวอักษรต่อยซึ่งจะถูกฝึกงานในสตริงสระแล้วเปรียบเทียบกับตัวอักษรสตริงเดียวกันจากสระว่ายน้ำในifคำสั่ง


15

สแต็คและสตริงต่อแนวคิดของพูล ป้อนคำอธิบายรูปภาพที่นี่


6
อะไร? ฉันไม่เข้าใจว่าสิ่งนี้มี upvotes อย่างไร คุณช่วยชี้แจงคำตอบของคุณได้ไหม?
Cᴏʀʏ

ฉันคิดว่าคำตอบที่ตั้งใจคือเนื่องจาก str1 + str2 ไม่ได้รับการปรับให้เหมาะสมกับสตริง interned การเปรียบเทียบกับสตริงจากกลุ่มสตริงจะนำไปสู่เงื่อนไขที่ผิด
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

แม้ว่าเมื่อคุณสร้างโดยใช้สัญกรณ์ String ของ Java มันจะเรียกวิธีฝึกงาน () โดยอัตโนมัติเพื่อวางวัตถุนั้นลงใน String pool โดยที่ไม่ได้มีอยู่ในพูลแล้ว

ทำไมสุดท้ายสร้างความแตกต่าง?

คอมไพเลอร์รู้ว่าตัวแปรสุดท้ายจะไม่มีการเปลี่ยนแปลงเมื่อเราเพิ่มตัวแปรสุดท้ายเหล่านี้เอาต์พุตไปที่ String Pool เนื่องจากstr1 + str2เอาต์พุตนิพจน์ไม่เคยเปลี่ยนแปลงดังนั้นในที่สุดคอมไพเลอร์จึงเรียกวิธีการอินเตอร์หลังจากผลลัพธ์ของตัวแปรสุดท้ายทั้งสองข้างต้น ในกรณีที่คอมไพเลอร์ตัวแปรสุดท้ายไม่เรียกวิธีการฝึกงาน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.