簡體   English   中英

字符串連接:concat() 與“+”運算符

[英]String concatenation: concat() vs "+" operator

假設字符串 a 和 b:

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

在引擎蓋下,它們是一樣的嗎?

這里是 concat 反編譯作為參考。 我也希望能夠反編譯+運算符,看看它做了什么。

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

不,不完全是。

首先,語義上略有不同。 如果anull ,則a.concat(b)拋出NullPointerExceptiona+=b會將a的原始值視為null 此外, concat()方法只接受String值,而+運算符會默默地將參數轉換為 String(對對象使用toString()方法)。 所以concat()方法接受的內容更加嚴格。

要深入了解,請使用a += b;

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

現在用javap -c反匯編(包含在 Sun JDK 中)。 您應該會看到一個列表,其中包括:

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

所以, a += b等價於

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

concat方法應該更快。 但是,如果字符串越多, StringBuilder方法就會勝出,至少在性能方面如此。

Sun JDK 的 src.zip 中提供了StringStringBuilder (及其包私有基類)的源代碼。 您可以看到您正在構建一個 char 數組(根據需要調整大小),然后在創建最終String時將其丟棄。 在實踐中,內存分配速度驚人。

更新:正如 Pawel Adamski 所說,最近的 HotSpot 的性能發生了變化。 javac仍然產生完全相同的代碼,但字節碼編譯器作弊。 簡單的測試完全失敗,因為整個代碼體都被丟棄了。 System.identityHashCode (不是String.hashCode )求和顯示StringBuffer代碼有一點優勢。 下一次更新發布或您使用不同的 JVM 時可能會發生變化。 來自@lukasederHotSpot JVM 內在函數列表

Niyaz是正確的,但也值得注意的是,特殊的 + 運算符可以通過 Java 編譯器轉換為更有效的東西。 Java 有一個 StringBuilder 類,它代表一個非線程安全的可變字符串。 當執行一堆字符串連接時,Java 編譯器會默默地轉換

String a = b + c + d;

進入

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

對於大字符串,這明顯更有效。 據我所知,使用 concat 方法時不會發生這種情況。

但是,concat 方法在將空字符串連接到現有字符串時更有效。 在這種情況下,JVM 不需要創建新的 String 對象,只需返回現有的對象即可。 請參閱concat 文檔以確認這一點。

因此,如果您非常關心效率,那么在連接可能為空的字符串時應該使用 concat 方法,否則使用 + 。 但是,性能差異應該可以忽略不計,您可能永遠不必擔心這一點。

我運行了與@marcio 類似的測試,但使用了以下循環:

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
}

為了更好地衡量,我也加入了StringBuilder.append() 每個測試運行 10 次,每次運行 100k 次。 結果如下:

  • StringBuilder贏得了勝利。 大多數運行的時鍾時間結果為 0,最長為 16 毫秒。
  • a += b每次運行大約需要 40000 毫秒(40 秒)。
  • concat每次運行只需要 10000 毫秒(10 秒)。

我還沒有反編譯該類以查看內部結構或通過分析器運行它,但我懷疑a += b花費大量時間創建StringBuilder的新對象,然后將它們轉換回String

這里的大多數答案都是從 2008 年開始的。看起來事情隨着時間的推移發生了變化。 我使用 JMH 進行的最新基准測試表明,在 Java 8 +上比concat快兩倍左右。

我的基准:

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

結果:

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

Tom 准確地描述了 + 運算符的作用是正確的。 它創建一個臨時StringBuilder ,附加部分,並以toString()結束。

但是,到目前為止,所有答案都忽略了 HotSpot 運行時優化的影響。 具體來說,這些臨時操作被認為是一種常見模式,並在運行時被更高效的機器代碼所取代。

@marcio:您已經創建了一個 微基准 對於現代 JVM,這不是分析代碼的有效方法。

運行時優化很重要的原因是,一旦 HotSpot 開始運行,代碼中的許多差異——甚至包括對象創建——都完全不同。 .唯一可以確定的方法是分析您的代碼。

最后,所有這些方法實際上都非常快。 這可能是過早優化的情況。 如果您有很多連接字符串的代碼,那么獲得最大速度的方法可能與您選擇的運算符無關,而是您使用的算法!

做一些簡單的測試怎么樣? 使用了下面的代碼:

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);
  • "a + b"版本在2500ms 內執行。
  • a.concat(b)1200ms內執行。

測試了幾次。 concat()版本的執行平均花費了一半的時間。

這個結果讓我感到驚訝,因為concat()方法總是創建一個新字符串(它返回一個“ new String(result) ”。眾所周知:

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

為什么編譯器不能優化“a + b”代碼中的字符串創建,知道它總是產生相同的字符串? 它可以避免創建新的字符串。 如果您不相信上面的陳述,請自行測試。

基本上,+ 和concat方法之間有兩個重要的區別。

  1. 如果您使用的是concat方法,那么您將只能連接字符串,而在使用+運算符的情況下,您還可以將字符串與任何數據類型連接起來。

    例如:

     String s = 10 + "Hello";

    在這種情況下,輸出應該是10Hello

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

    在上述情況下,您必須提供兩個強制性字符串。

  2. +concat之間的第二個主要區別是:

    案例 1:假設我以這種方式使用concat運算符連接相同的字符串

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

    在這種情況下,池中創建的對象總數為 7,如下所示:

     I am good boy Iam Iamgood Iamgoodboy

    案例二:

    現在我將通過+運算符連接相同的字符串

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

    在上述情況下,創建的對象總數只有 5 個。

    實際上,當我們通過+運算符連接字符串時,它會維護一個 StringBuffer 類來執行相同的任務,如下所示:-

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

    這樣,它將只創建五個對象。

所以伙計們,這些是+concat方法之間的基本區別。 享受 :)

為了完整起見,我想補充一點,“+”運算符的定義可以在JLS SE8 15.18.1中找到:

如果只有一個操作數表達式是字符串類型,則對另一個操作數執行字符串轉換(第 5.1.11 節)以在運行時生成字符串。

字符串連接的結果是對 String 對象的引用,該對象是兩個操作數字符串的連接。 在新創建的字符串中,左側操作數的字符在右側操作數的字符之前。

String 對象是新創建的(第 12.5 節),除非表達式是常量表達式(第 15.28 節)

關於實施,JLS 說如下:

實現可以選擇在一個步驟中執行轉換和連接,以避免創建然后丟棄中間 String 對象。 為了提高重復字符串連接的性能,Java 編譯器可以使用 StringBuffer 類或類似技術來減少通過計算表達式創建的中間 String 對象的數量。

對於原始類型,實現還可以通過直接從原始類型轉換為字符串來優化包裝對象的創建。

因此從“Java 編譯器可能使用 StringBuffer 類或類似技術來減少”來看,不同的編譯器可能會產生不同的字節碼。

我不這么認為。

a.concat(b)是在 String 中實現的,我認為自早期的 java 機器以來實現並沒有太大變化。 +操作的實現取決於 Java 版本和編譯器。 目前+是使用StringBuffer實現的,以使操作盡可能快。 也許在未來,這種情況會改變。 在早期版本的 java +中,對字符串的操作要慢得多,因為它會產生中間結果。

我猜+=是使用+實現的,並進行了類似的優化。

+ 運算符可以在字符串和字符串、字符、整數、雙精度或浮點數據類型值之間工作。 它只是在連接之前將值轉換為其字符串表示形式。

concat 運算符只能在字符串上完成。 它檢查數據類型的兼容性,如果不匹配則拋出錯誤。

除此之外,您提供的代碼執行相同的操作。

使用 + 時,速度隨着字符串長度的增加而降低,但使用 concat 時,速度更穩定,最好的選擇是使用速度穩定的 StringBuilder 類來做到這一點。

我想你可以理解為什么。 但是創建長字符串的最佳方法是使用 StringBuilder() 和 append(),這兩種速度都將是不可接受的。

注意s.concat("hello"); 當 s 為 null 時會導致NullPointereException 在 Java 中,+ 運算符的行為通常由左操作數決定:

System.out.println(3 + 'a'); //100

但是,字符串是一個例外。 如果任一操作數是字符串,則結果應為字符串。 這就是 null 被轉換為“null”的原因,即使您可能期望出現RuntimeException

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM