So sánh các chuỗi với == được khai báo là cuối cùng trong Java


220

Tôi có một câu hỏi đơn giản về các chuỗi trong Java. Đoạn mã đơn giản sau đây chỉ nối hai chuỗi và sau đó so sánh chúng với ==.

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

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

Biểu thức so sánh concat=="string"trả về falselà hiển nhiên (tôi hiểu sự khác biệt giữa equals()==).


Khi hai chuỗi này được khai báo finalnhư vậy,

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

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

Các biểu thức so sánh concat=="string", trong trường hợp này trả về true. Tại sao finallàm cho một sự khác biệt? Nó có phải làm một cái gì đó với nhóm thực tập hay tôi chỉ đang bị lừa?


22
Tôi luôn thấy thật ngớ ngẩn khi bằng là cách kiểm tra mặc định cho các nội dung bằng nhau, thay vì có == làm điều đó và chỉ sử dụng ReferenceEquals hoặc một cái gì đó tương tự để kiểm tra xem các con trỏ có giống nhau không.
Davio

25
Đây không phải là bản sao của "Làm cách nào để so sánh các chuỗi trong Java?" bằng mọi cách OP hiểu sự khác biệt giữa equals()==trong ngữ cảnh của các chuỗi và đang đặt ra một câu hỏi có ý nghĩa hơn.
arshajii

@Davio Nhưng làm thế nào mà nó hoạt động khi lớp học không String? Tôi nghĩ rằng nó rất logic, không ngớ ngẩn, để thực hiện so sánh nội dung bằng equalsmột phương pháp mà chúng ta có thể ghi đè để biết khi nào chúng ta xem xét hai đối tượng bằng nhau và để so sánh nhận dạng được thực hiện theo ==. Nếu việc so sánh nội dung được thực hiện bởi ==chúng tôi không thể ghi đè điều đó để xác định ý nghĩa của chúng tôi bằng "nội dung bằng nhau" và việc chỉ có ý nghĩa equals==đảo ngược cho Strings sẽ là ngớ ngẩn. Ngoài ra, bất kể điều này, dù sao tôi cũng không thấy bất kỳ lợi thế nào khi ==thực hiện so sánh nội dung thay vì equals.
SantiBailors

@SantiBailors bạn đúng rằng đây chỉ là cách nó hoạt động trong Java, tôi cũng đã sử dụng C # trong đó == bị quá tải cho sự bình đẳng nội dung. Một phần thưởng bổ sung khi sử dụng == là nó không an toàn: (null == "cái gì đó") trả về sai. Nếu bạn sử dụng bằng cho 2 đối tượng, bạn phải biết nếu có thể là null hoặc bạn có nguy cơ bị ném NullPulumException.
Davio

Câu trả lời:


232

Khi bạn khai báo một biến String( không thay đổi ) là finalvà khởi tạo nó bằng biểu thức hằng số thời gian biên dịch, nó cũng trở thành biểu thức hằng số thời gian biên dịch và giá trị của nó được trình biên dịch sử dụng. Vì vậy, trong ví dụ mã thứ hai của bạn, sau khi nội tuyến các giá trị, phép nối chuỗi được dịch bởi trình biên dịch thành:

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

mà khi so sánh "string"sẽ cung cấp cho bạn true, bởi vì chuỗi ký tự được thực tập .

Từ JLS §4.12.4 - finalBiến :

Một biến của kiểu nguyên thủy hoặc kiểu String , được finalkhởi tạo với biểu thức hằng số thời gian biên dịch (§15.28), được gọi là biến không đổi .

Cũng từ JLS §15.28 - Biểu thức không đổi:

Các biểu thức hằng số thời gian biên dịch kiểu Stringluôn được "tập trung" để chia sẻ các thể hiện duy nhất, sử dụng phương thứcString#intern() .


Đây không phải là trường hợp trong ví dụ mã đầu tiên của bạn, trong đó String biến không final. Vì vậy, chúng không phải là một biểu thức hằng số thời gian biên dịch. Hoạt động nối sẽ có độ trễ cho đến thời gian chạy, do đó dẫn đến việc tạo ra một Stringđối tượng mới . Bạn có thể xác minh điều này bằng cách so sánh mã byte của cả hai mã.

Ví dụ mã đầu tiên (không phải finalphiên bản) được biên dịch thành mã byte sau:

  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

Rõ ràng nó đang lưu trữ stringtrong hai biến riêng biệt, và sử dụng StringBuilderđể thực hiện thao tác nối.

Trong khi đó, ví dụ mã thứ hai ( finalphiên bản) của bạn trông như thế này:

  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

Vì vậy, nó trực tiếp nội tuyến biến cuối cùng để tạo Chuỗi stringtại thời gian biên dịch, được tải bởi ldchoạt động trong bước 0. Sau đó, chuỗi ký tự thứ hai được tải bằng ldcthao tác trong bước 7. Nó không liên quan đến việc tạo ra bất kỳ Stringđối tượng mới nào trong thời gian chạy. Chuỗi đã được biết đến tại thời điểm biên dịch và chúng được thực hiện.


2
Không có gì ngăn cản việc triển khai trình biên dịch Java khác không thực hiện Chuỗi cuối cùng phải không?
Alvin

13
@Alvin JLS yêu cầu các biểu thức chuỗi hằng số thời gian biên dịch được thực hiện. Bất kỳ thực hiện phù hợp phải làm điều tương tự ở đây.
Tavian Barnes

Ngược lại, JLS có bắt buộc một trình biên dịch không được tối ưu hóa việc ghép nối trong phiên bản đầu tiên, không phải là cuối cùng không? Là trình biên dịch bị cấm sản xuất mã, mà sẽ có so sánh đánh giá true?
phant0m

1
@ phant0m dùng từ ngữ hiện tại của các đặc điểm kỹ thuật , “ Các Stringđối tượng mới được tạo ra (§12.5) trừ khi biểu thức là một biểu thức hằng số (§15.28). Theo nghĩa đen, việc áp dụng tối ưu hóa trong phiên bản không phải là cuối cùng là không được phép, vì một chuỗi mới được tạo ra của Wikipedia phải có một danh tính đối tượng khác. Tôi không biết liệu đây là cố ý. Rốt cuộc, chiến lược biên dịch hiện tại là ủy thác cho một cơ sở thời gian chạy không ghi lại các hạn chế đó.
Holger

31

Theo nghiên cứu của tôi, tất cả final Stringđều được thực hiện trong Java. Từ một trong những bài đăng trên blog:

Vì vậy, nếu bạn thực sự cần so sánh hai Chuỗi bằng cách sử dụng == hoặc! = Hãy chắc chắn rằng bạn gọi phương thức String.i INTERN () trước khi so sánh. Mặt khác, luôn thích String.equals (String) để so sánh String.

Vì vậy, nó có nghĩa là nếu bạn gọi, String.intern()bạn có thể so sánh hai chuỗi bằng ==toán tử. Nhưng ở đây String.intern()không cần thiết vì trong Javafinal String được nội bộ.

Bạn có thể tìm thêm thông tin So sánh chuỗi bằng cách sử dụng toán tử == và Javadoc cho String.i INTERN () phương thức .

Cũng tham khảo bài viết Stackoverflow này để biết thêm thông tin.


3
Các chuỗi intern () không phải là rác được thu thập và nó được lưu trữ trong không gian permgen ở mức thấp nên bạn sẽ gặp rắc rối như lỗi bộ nhớ nếu không được sử dụng đúng cách.
Ajeesh

@Ajeesh - Chuỗi có thể được thu gom rác. Ngay cả các chuỗi được thực hiện do các biểu thức không đổi có thể là rác được thu thập trong một số trường hợp.
Stephen C

21

Nếu bạn xem phương pháp này

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

và nó được dịch ngược với javap -c ClassWithTheseMethods các phiên bản bạn sẽ thấy

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

Vì vậy, nếu Chuỗi không phải là trình biên dịch cuối cùng sẽ phải sử dụng StringBuilderđể nối str1str2vì vậy

String concat=str1+str2;

sẽ được biên dịch thành

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

điều đó có nghĩa là nó concatsẽ được tạo trong thời gian chạy vì vậy sẽ không đến từ chuỗi String.


Ngoài ra, nếu Chuỗi là cuối cùng thì trình biên dịch có thể cho rằng chúng sẽ không bao giờ thay đổi, vì vậy thay vì sử dụng StringBuildernó có thể nối các giá trị của nó một cách an toàn như vậy

String concat = str1 + str2;

có thể thay đổi thành

String concat = "str" + "ing";

và nối vào

String concat = "string";

điều đó có nghĩa là điều đó concatesẽ trở thành sting nghĩa đen sẽ được thực hiện trong nhóm chuỗi và sau đó được so sánh với cùng một chuỗi ký tự từ nhóm đó trong ifcâu lệnh.


15

Khái niệm pool và chuỗi conts pool nhập mô tả hình ảnh ở đây


6
Gì? Tôi không hiểu làm thế nào điều này có upvotes. Bạn có thể làm rõ câu trả lời của bạn?
Cᴏʀʏ

Tôi nghĩ rằng câu trả lời dự định là bởi vì str1 + str2 không được tối ưu hóa thành chuỗi được liên kết, nên việc so sánh với một chuỗi từ nhóm chuỗi sẽ dẫn đến một điều kiện sai.
viki.omega9

3

Hãy xem một số mã byte cho finalví dụ

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
}

Tại 0:2:, String "string"được đẩy lên ngăn xếp (từ nhóm không đổi) và được lưu trữ concattrực tiếp vào biến cục bộ . Bạn có thể suy luận rằng trình biên dịch đang tạo (ghép) String "string"chính nó tại thời gian biên dịch.

Mã không finalbyte

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
}

Ở đây bạn có hai Stringhằng số, "str""ing"chúng cần được nối vào lúc chạy với a StringBuilder.


0

Mặc dù, khi bạn tạo bằng cách sử dụng ký hiệu chuỗi ký tự của Java, nó sẽ tự động gọi phương thức intern () để đưa đối tượng đó vào nhóm Chuỗi, miễn là nó chưa có trong nhóm.

Tại sao cuối cùng làm cho một sự khác biệt?

Trình biên dịch biết biến cuối cùng sẽ không bao giờ thay đổi, khi chúng ta thêm các biến cuối cùng này, đầu ra sẽ vào String Pool vì str1 + str2đầu ra biểu thức cũng sẽ không bao giờ thay đổi, vì vậy cuối cùng trình biên dịch gọi phương thức inter sau đầu ra của hai biến cuối cùng ở trên. Trong trường hợp trình biên dịch biến không cuối cùng không gọi phương thức intern.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.