Nối chuỗi: concat () so với toán tử + +


499

Giả sử Chuỗi a và b:

a += b
a = a.concat(b)

Dưới mui xe, họ là những điều tương tự?

Đây là concat dịch ngược như tài liệu tham khảo. Tôi cũng muốn có thể dịch ngược +toán tử để xem điều đó làm gì.

public String concat(String s) {

    int i = s.length();
    if (i == 0) {
        return this;
    }
    else {
        char ac[] = new char[count + i];
        getChars(0, count, ac, 0);
        s.getChars(0, i, ac, count);
        return new String(0, count + i, ac);
    }
}


3
Tôi không chắc +có thể dịch ngược.
Galen Nare

1
Sử dụng javap để phân tách tệp lớp Java.
Licks nóng

Do "tính không thay đổi", có lẽ bạn nên sử dụng StringBufferhoặc StringBuilder- (luồng không an toàn do đó nhanh hơn, thay vào đó
Ujjwal Singh

Câu trả lời:


560

Không, không hẳn.

Thứ nhất, có một chút khác biệt về ngữ nghĩa. Nếu anull, sau đó a.concat(b)ném một NullPointerExceptionnhưng a+=bsẽ coi giá trị ban đầu anhư thể nó là null. Hơn nữa, concat()phương thức chỉ chấp nhận Stringcác giá trị trong khi +toán tử sẽ âm thầm chuyển đổi đối số thành Chuỗi (sử dụng toString()phương thức cho các đối tượng). Vì vậy, concat()phương pháp nghiêm ngặt hơn trong những gì nó chấp nhận.

Để nhìn dưới mui xe, hãy viết một lớp đơn giản với a += b;

public class Concat {
    String cat(String a, String b) {
        a += b;
        return a;
    }
}

Bây giờ tháo rời với javap -c(bao gồm trong Sun JDK). Bạn sẽ thấy một danh sách bao gồm:

java.lang.String cat(java.lang.String, java.lang.String);
  Code:
   0:   new     #2; //class java/lang/StringBuilder
   3:   dup
   4:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   7:   aload_1
   8:   invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   11:  aload_2
   12:  invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   15:  invokevirtual   #5; //Method java/lang/StringBuilder.toString:()Ljava/lang/    String;
   18:  astore_1
   19:  aload_1
   20:  areturn

Vì vậy, a += blà tương đương với

a = new StringBuilder()
    .append(a)
    .append(b)
    .toString();

Các concatphương pháp cần được nhanh hơn. Tuy nhiên, với nhiều chuỗi hơn, StringBuilderphương thức sẽ thắng, ít nhất là về hiệu suất.

Mã nguồn của StringStringBuilder(và lớp cơ sở riêng của gói) có sẵn trong src.zip của Sun JDK. Bạn có thể thấy rằng bạn đang xây dựng một mảng char (thay đổi kích thước khi cần thiết) và sau đó ném nó đi khi bạn tạo bản cuối cùngString . Trong thực tế phân bổ bộ nhớ là nhanh đáng ngạc nhiên.

Cập nhật: Như chú thích của Pawel Adamski, hiệu suất đã thay đổi trong HotSpot gần đây. javacvẫn tạo ra chính xác cùng một mã, nhưng trình biên dịch mã byte gian lận. Thử nghiệm đơn giản hoàn toàn thất bại vì toàn bộ phần mã bị vứt đi. Tóm tắt System.identityHashCode(không String.hashCode) cho thấy StringBuffermã có một lợi thế nhỏ. Có thể thay đổi khi bản cập nhật tiếp theo được phát hành hoặc nếu bạn sử dụng một JVM khác. Từ @lukasinger , một danh sách các nội tại JVM của HotSpot .


4
@HyperLink Bạn có thể xem mã bằng cách sử dụng javap -cmột lớp được biên dịch sử dụng nó. (. Oh, như trong câu trả lời Bạn chỉ cần để giải thích các bytecode tháo gỡ, mà không nên có khó khăn.)
Tom Hawtin - tackline

1
Bạn có thể tham khảo đặc tả JVM để hiểu các mã byte riêng lẻ. Những thứ bạn muốn tham khảo là trong chương 6. Một chút tối nghĩa, nhưng bạn có thể có được ý chính của nó khá dễ dàng.
Licks nóng

1
Tôi tự hỏi tại sao trình biên dịch Java sử dụng StringBuilderngay cả khi nối hai chuỗi? Nếu Stringbao gồm các phương thức tĩnh để nối tối đa bốn chuỗi hoặc tất cả các chuỗi trong một String[], mã có thể nối thêm bốn chuỗi với hai phân bổ đối tượng (kết quả Stringvà sự hỗ trợ của nó char[], không phải là một dự phòng) và bất kỳ số chuỗi nào có ba phân bổ ( các String[], kết quả String, và sự ủng hộ char[], chỉ với những con người thừa đầu tiên). Như vậy, sử dụng StringBuilderý chí tốt nhất đòi hỏi bốn phân bổ, và sẽ yêu cầu sao chép mỗi ký tự hai lần.
supercat

Biểu thức đó, a + = b. Điều đó không có nghĩa là: a = a + b?
đáng kính nhất thưa ngài

3
Mọi thứ đã thay đổi kể từ khi câu trả lời này được tạo ra. Xin vui lòng đọc câu trả lời của tôi dưới đây.
Paweł Adamski

90

Niyaz là chính xác, nhưng cũng đáng lưu ý rằng toán tử + đặc biệt có thể được chuyển đổi thành thứ gì đó hiệu quả hơn bằng trình biên dịch Java. Java có một lớp StringBuilder đại diện cho một chuỗi có thể thay đổi, không an toàn cho chuỗi. Khi thực hiện một loạt các chuỗi nối, trình biên dịch Java âm thầm chuyển đổi

String a = b + c + d;

vào

String a = new StringBuilder(b).append(c).append(d).toString();

mà cho các chuỗi lớn là hiệu quả hơn đáng kể. Theo tôi biết, điều này không xảy ra khi bạn sử dụng phương pháp concat.

Tuy nhiên, phương thức concat hiệu quả hơn khi nối Chuỗi rỗng vào Chuỗi hiện có. Trong trường hợp này, JVM không cần tạo một đối tượng String mới và chỉ có thể trả về đối tượng hiện có. Xem tài liệu concat để xác nhận điều này.

Vì vậy, nếu bạn cực kỳ quan tâm đến hiệu quả thì bạn nên sử dụng phương pháp concat khi nối các chuỗi có thể trống và sử dụng + nếu không. Tuy nhiên, sự khác biệt hiệu suất nên không đáng kể và có lẽ bạn không nên lo lắng về điều này.


concat infact không làm điều đó. Tôi đã chỉnh sửa bài đăng của mình bằng cách dịch ngược phương thức concat
shsteimer

10
nguyên vẹn nó làm. Nhìn vào dòng đầu tiên của mã concat của bạn. Vấn đề với concat là nó luôn tạo ra một Chuỗi mới ()
Marcio Aguiar

2
@MarcioAguiar: có thể bạn muốn nói rằng + luôn tạo ra một cái mới String- như bạn nói, concatcó một ngoại lệ khi bạn ghép một khoảng trống String.
Blaisorblade

45

Tôi đã chạy thử nghiệm tương tự như @marcio nhưng với vòng lặp sau:

String c = a;
for (long i = 0; i < 100000L; i++) {
    c = c.concat(b); // make sure javac cannot skip the loop
    // using c += b for the alternative
}

Chỉ cần cho các biện pháp tốt, tôi cũng ném vào StringBuilder.append(). Mỗi bài kiểm tra đã được chạy 10 lần, với 100k đại diện cho mỗi lần chạy. Đây là kết quả:

  • StringBuilderthắng tay xuống. Kết quả thời gian đồng hồ là 0 cho hầu hết các lần chạy và thời gian dài nhất mất 16ms.
  • a += b mất khoảng 40000ms (40 giây) cho mỗi lần chạy.
  • concat chỉ cần 10000ms (10 giây) mỗi lần chạy.

Tôi chưa dịch ngược lớp để xem các phần bên trong hoặc chạy nó thông qua trình lược tả, nhưng tôi nghi ngờ a += bdành phần lớn thời gian để tạo các đối tượng mới StringBuildervà sau đó chuyển đổi chúng trở lại String.


4
Thời gian tạo đối tượng thực sự quan trọng. Đó là lý do tại sao trong nhiều tình huống, chúng tôi sử dụng StringBuilder trực tiếp thay vì tận dụng StringBuilder phía sau +.
coolcfan

1
@coolcfan: Khi +được sử dụng cho hai chuỗi, có trường hợp nào sử dụng StringBuildertốt hơn String.valueOf(s1).concat(s2)không? Bất kỳ ý tưởng tại sao trình biên dịch sẽ không sử dụng cái sau [hoặc nếu không bỏ qua valueOfcuộc gọi trong trường hợp s1được biết là không null]?
supercat

1
@supercat xin lỗi tôi không biết. Có lẽ những người đứng sau loại đường này là những người tốt nhất để trả lời điều này.
coolcfan

25

Hầu hết các câu trả lời ở đây là từ năm 2008. Có vẻ như mọi thứ đã thay đổi theo thời gian. Điểm chuẩn mới nhất của tôi được thực hiện với JMH cho thấy trên Java 8 +nhanh hơn khoảng hai lần so vớiconcat .

Điểm chuẩn của tôi:

@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
public class StringConcatenation {

    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State2 {
        public String a = "abc";
        public String b = "xyz";
    }

    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State3 {
        public String a = "abc";
        public String b = "xyz";
        public String c = "123";
    }


    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State4 {
        public String a = "abc";
        public String b = "xyz";
        public String c = "123";
        public String d = "!@#";
    }

    @Benchmark
    public void plus_2(State2 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b);
    }

    @Benchmark
    public void plus_3(State3 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b+state.c);
    }

    @Benchmark
    public void plus_4(State4 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b+state.c+state.d);
    }

    @Benchmark
    public void stringbuilder_2(State2 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).toString());
    }

    @Benchmark
    public void stringbuilder_3(State3 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).toString());
    }

    @Benchmark
    public void stringbuilder_4(State4 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).append(state.d).toString());
    }

    @Benchmark
    public void concat_2(State2 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b));
    }

    @Benchmark
    public void concat_3(State3 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b.concat(state.c)));
    }


    @Benchmark
    public void concat_4(State4 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b.concat(state.c.concat(state.d))));
    }
}

Các kết quả:

Benchmark                             Mode  Cnt         Score         Error  Units
StringConcatenation.concat_2         thrpt   50  24908871.258 ± 1011269.986  ops/s
StringConcatenation.concat_3         thrpt   50  14228193.918 ±  466892.616  ops/s
StringConcatenation.concat_4         thrpt   50   9845069.776 ±  350532.591  ops/s
StringConcatenation.plus_2           thrpt   50  38999662.292 ± 8107397.316  ops/s
StringConcatenation.plus_3           thrpt   50  34985722.222 ± 5442660.250  ops/s
StringConcatenation.plus_4           thrpt   50  31910376.337 ± 2861001.162  ops/s
StringConcatenation.stringbuilder_2  thrpt   50  40472888.230 ± 9011210.632  ops/s
StringConcatenation.stringbuilder_3  thrpt   50  33902151.616 ± 5449026.680  ops/s
StringConcatenation.stringbuilder_4  thrpt   50  29220479.267 ± 3435315.681  ops/s

Tôi tự hỏi tại sao Java Stringkhông bao giờ bao gồm một hàm tĩnh để tạo thành một chuỗi bằng cách nối các phần tử của a String[]. Sử dụng +để nối 8 chuỗi bằng cách sử dụng một hàm như vậy sẽ yêu cầu xây dựng và sau đó từ bỏ String[8], nhưng đó sẽ là đối tượng duy nhất cần được xây dựng bị bỏ rơi, trong khi sử dụng một StringBuilderyêu cầu xây dựng và từ bỏ StringBuildercá thể và ít nhất một char[]cửa hàng sao lưu.
supercat

@supercat Một số String.join()phương thức tĩnh đã được thêm vào trong Java 8, như các hàm bao cú pháp nhanh xung quanh java.util.StringJoinerlớp.
Ti Strga

@TiStrga: Việc xử lý +đã thay đổi để sử dụng các chức năng đó chưa?
supercat

@supercat Điều đó sẽ phá vỡ khả năng tương thích ngược nhị phân, vì vậy không. Đó là chỉ in reply to "tại sao Chuỗi không bao giờ bao gồm một chức năng tĩnh" bình luận của bạn: bây giờ có một chức năng như vậy. Phần còn lại của đề xuất của bạn (tái cấu trúc +để sử dụng nó) sẽ đòi hỏi nhiều hơn những gì các nhà phát triển Java sẵn sàng thay đổi, thật đáng buồn.
Ti Strga

@TiStrga: Có cách nào để một tệp mã byte Java có thể chỉ ra "Nếu hàm X có sẵn, hãy gọi nó; nếu không thì làm gì khác" theo cách có thể được giải quyết trong quá trình tải một lớp? Việc tạo mã bằng một phương thức tĩnh có thể xâu chuỗi thành phương thức tĩnh của Java hoặc người khác sử dụng một trình tạo chuỗi nếu điều đó không có sẵn có vẻ là giải pháp tối ưu.
supercat

22

Tom là chính xác trong việc mô tả chính xác những gì toán tử + làm. Nó tạo tạm thời StringBuilder, nối các phần và kết thúc bằngtoString() .

Tuy nhiên, tất cả các câu trả lời cho đến nay đều bỏ qua ảnh hưởng của tối ưu hóa thời gian chạy HotSpot. Cụ thể, các hoạt động tạm thời này được công nhận là một mẫu chung và được thay thế bằng mã máy hiệu quả hơn vào thời gian chạy.

@marcio: Bạn đã tạo một điểm chuẩn vi mô ; với JVM hiện đại, đây không phải là một cách hợp lệ để mã hồ sơ.

Lý do tối ưu hóa thời gian chạy là nhiều trong số những khác biệt về mã này - thậm chí bao gồm cả việc tạo đối tượng - hoàn toàn khác nhau một khi HotSpot bắt đầu. Cách duy nhất để biết chắc chắn là lược tả mã của bạn tại chỗ .

Cuối cùng, tất cả các phương pháp này trong thực tế là cực kỳ nhanh chóng. Đây có thể là một trường hợp tối ưu hóa sớm. Nếu bạn có mã nối chuỗi nhiều, cách để có tốc độ tối đa có thể không liên quan gì đến toán tử bạn chọn và thay vào đó là thuật toán bạn đang sử dụng!


Tôi đoán bởi "các hoạt động tạm thời" này có nghĩa là bạn sử dụng phân tích thoát để phân bổ các đối tượng "heap" trên ngăn xếp trong trường hợp có thể chứng minh chính xác. Mặc dù phân tích thoát có mặt trong HotSpot (hữu ích để xóa một số đồng bộ hóa), tôi không tin điều đó, tại thời điểm viết, u
Tom Hawtin - tackline

21

Làm thế nào về một số thử nghiệm đơn giản? Đã sử dụng mã dưới đây:

long start = System.currentTimeMillis();

String a = "a";

String b = "b";

for (int i = 0; i < 10000000; i++) { //ten million times
     String c = a.concat(b);
}

long end = System.currentTimeMillis();

System.out.println(end - start);
  • Các "a + b"phiên bản thực hiện trong 2500ms .
  • Việc a.concat(b)thực hiện trong 1200ms .

Đã thử nghiệm nhiều lần. Việc concat()thực hiện phiên bản trung bình mất một nửa thời gian.

Kết quả này làm tôi ngạc nhiên vì concat()phương thức này luôn tạo ra một chuỗi mới (nó trả về một " new String(result)". Nó được biết rằng:

String a = new String("a") // more than 20 times slower than String a = "a"

Tại sao trình biên dịch không có khả năng tối ưu hóa việc tạo chuỗi trong mã "a + b", biết rằng nó luôn dẫn đến cùng một chuỗi? Nó có thể tránh việc tạo chuỗi mới. Nếu bạn không tin vào tuyên bố trên, hãy tự kiểm tra.


Tôi đã thử nghiệm trên java jdk1.8.0_241 mã của bạn, Đối với tôi mã "a + b" đang cho kết quả tối ưu. Với concat (): 203ms và với "+": 113ms . Tôi đoán trong phiên bản trước nó không được tối ưu hóa.
Akki

6

Về cơ bản, có hai sự khác biệt quan trọng giữa + và concatphương thức.

  1. Nếu bạn đang sử dụng phương thức concat thì bạn chỉ có thể nối chuỗi trong khi trong trường hợp toán tử + , bạn cũng có thể nối chuỗi với bất kỳ loại dữ liệu nào.

    Ví dụ:

    String s = 10 + "Hello";

    Trong trường hợp này, đầu ra phải là 10Hello .

    String s = "I";
    String s1 = s.concat("am").concat("good").concat("boy");
    System.out.println(s1);

    Trong trường hợp trên, bạn phải cung cấp hai chuỗi bắt buộc.

  2. Sự khác biệt thứ hai và chính giữa +concat là:

    Trường hợp 1: Giả sử tôi nối các chuỗi tương tự với toán tử concat theo cách này

    String s="I";
    String s1=s.concat("am").concat("good").concat("boy");
    System.out.println(s1);

    Trong trường hợp này, tổng số đối tượng được tạo trong nhóm là 7 như thế này:

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    Trường hợp 2:

    Bây giờ tôi sẽ kết hợp các chuỗi tương tự thông qua toán tử +

    String s="I"+"am"+"good"+"boy";
    System.out.println(s);

    Trong trường hợp trên, tổng số đối tượng được tạo chỉ là 5.

    Thực tế khi chúng ta kết hợp các chuỗi thông qua toán tử + thì nó sẽ duy trì một lớp StringBuffer để thực hiện nhiệm vụ tương tự như sau: -

    StringBuffer sb = new StringBuffer("I");
    sb.append("am");
    sb.append("good");
    sb.append("boy");
    System.out.println(sb);

    Theo cách này, nó sẽ chỉ tạo ra năm đối tượng.

Vì vậy, đây là những khác biệt cơ bản giữa + và phương thức concat . Thưởng thức :)


Bạn thân mến, Bạn biết rất rõ rằng bất kỳ chuỗi ký tự nào được coi là một đối tượng Chuỗi tự lưu trữ trong chuỗi pool. Vì vậy, trong trường hợp này chúng ta có 4 chuỗi ký tự. Rõ ràng ít nhất phải tạo ra 4 đối tượng trong nhóm.
Deepak Sharma

1
Tôi không nghĩ vậy: String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());các bản in true, có nghĩa "good"là không có trong chuỗi chuỗi trước khi gọiintern()
fabian

Tôi chỉ nói về dòng này Chuỗi s = "I" + "am" + "tốt" + "boy"; Trong trường hợp này, cả 4 đều là chuỗi ký tự được giữ trong một nhóm. Nên tạo 4 đối tượng trong nhóm.
Deepak Sharma

4

Để hoàn thiện, tôi muốn thêm rằng định nghĩa của toán tử '+' có thể được tìm thấy trong JLS SE8 15.18.1 :

Nếu chỉ có một biểu thức toán hạng có kiểu Chuỗi, thì chuyển đổi chuỗi (§5.1.11) được thực hiện trên toán hạng khác để tạo ra một chuỗi trong thời gian chạy.

Kết quả của nối chuỗi là một tham chiếu đến một đối tượng Chuỗi là nối của hai chuỗi toán hạng. Các ký tự của toán hạng bên trái đứng trước các ký tự của toán hạng bên phải trong chuỗi vừa tạo.

Đối tượng String mới được tạo (§12.5) trừ khi biểu thức là biểu thức không đổi (§15.28).

Về việc triển khai, JLS cho biết như sau:

Việc triển khai có thể chọn thực hiện chuyển đổi và nối trong một bước để tránh tạo và sau đó loại bỏ một đối tượng Chuỗi trung gian. Để tăng hiệu năng nối chuỗi lặp lại, trình biên dịch Java có thể sử dụng lớp StringBuffer hoặc một kỹ thuật tương tự để giảm số lượng các đối tượng Chuỗi trung gian được tạo bằng cách đánh giá biểu thức.

Đối với các kiểu nguyên thủy, việc triển khai cũng có thể tối ưu hóa việc tạo đối tượng trình bao bọc bằng cách chuyển đổi trực tiếp từ kiểu nguyên thủy sang chuỗi.

Vì vậy, đánh giá từ 'trình biên dịch Java có thể sử dụng lớp StringBuffer hoặc một kỹ thuật tương tự để giảm', các trình biên dịch khác nhau có thể tạo ra mã byte khác nhau.


2

Các nhà điều hành + có thể làm việc giữa một chuỗi và một chuỗi, char, integer, double hoặc float giá trị kiểu dữ liệu. Nó chỉ chuyển đổi giá trị thành biểu diễn chuỗi của nó trước khi nối.

Các nhà điều hành concat chỉ có thể được thực hiện trên và với chuỗi. Nó kiểm tra tính tương thích của kiểu dữ liệu và đưa ra lỗi, nếu chúng không khớp.

Ngoại trừ điều này, mã bạn cung cấp thực hiện cùng một công cụ.


2

Tôi không nghĩ vậy.

a.concat(b)được triển khai trong String và tôi nghĩ rằng việc triển khai không thay đổi nhiều kể từ các máy java đầu tiên. Việc +thực hiện hoạt động phụ thuộc vào phiên bản Java và trình biên dịch. Hiện +đang được thực hiện bằng cách sử dụng StringBufferđể làm cho hoạt động nhanh nhất có thể. Có thể trong tương lai, điều này sẽ thay đổi. Trong các phiên bản trước của +hoạt động java trên String chậm hơn nhiều vì nó tạo ra kết quả trung gian.

Tôi đoán điều đó +=được thực hiện bằng cách sử dụng +và tối ưu hóa tương tự.


7
"Hiện tại + được triển khai bằng StringBuffer" Sai Đó là StringBuilder. StringBuffer là chủ đề an toàn của StringBuilder.
Frederic Morin

1
Nó từng là StringBuffer trước java 1.5, vì đó là phiên bản khi StringBuilder được giới thiệu lần đầu tiên.
ccpizza

0

Khi sử dụng +, tốc độ giảm khi độ dài của chuỗi tăng, nhưng khi sử dụng concat, tốc độ ổn định hơn và tùy chọn tốt nhất là sử dụng lớp StringBuilder có tốc độ ổn định để thực hiện điều đó.

Tôi đoán bạn có thể hiểu tại sao. Nhưng cách hoàn toàn tốt nhất để tạo các chuỗi dài là sử dụng StringBuilder () và append (), tốc độ sẽ không được chấp nhận.


1
sử dụng toán tử + tương đương với sử dụng StringBuilder ( docs.oracle.com/javase/specs/jls/se8/html/ trộm )
ihebiheb
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.