String拼接字符串效率低,你知道緣由嗎?

面試官Q1:請問爲何String用"+"拼接字符串效率低下,最好能從JVM角度談談嗎?java

對於這個問題,咱們先來看看以下代碼:面試

public class StringTest {
     public static void main(String[] args) {
        String a = "abc";
        String b = "def";
        String c = a + b;
        String d = "abc" + "def";
        System.out.Println(c);
        System.out.Println(d);
    }
}

打印結果:app

abcdef
abcdef

從上面代碼示例中,咱們看到兩種方式拼接的字符串打印的結果是同樣的。但這只是表面上的,實際內部運行不同。dom

二者究竟有什麼不同?性能

爲了看到二者的不一樣,對代碼作以下調整:ui

public class StringTest {
     public static void main(String[] args) {
         String a = "abc";
         String b = "def";
         String c = a + b;
         System.out.Println(c);
    }
}

咱們看看編譯完成後它是什麼樣子:spa

C:\Users\GRACE\Documents>javac StringTest.java
 2C:\Users\GRACE\Documents>javap -verbose StringTest
 3Classfile /C:/Users/GRACE/Documents/StringTest.class
 4  Last modified 2018-7-21; size 607 bytes
 5  MD5 checksum a2729f11e22d7e1153a209e5ac968b98
 6  Compiled from "StringTest.java"
 7public class StringTest
 8  minor version: 0
 9  major version: 52
10  flags: ACC_PUBLIC, ACC_SUPER
11Constant pool:
12   #1 = Methodref          #11.#20        // java/lang/Object."<init>":()V
13   #2 = String             #21            // abc
14   #3 = String             #22            // def
15   #4 = Class              #23            // java/lang/StringBuilder
16   #5 = Methodref          #4.#20         // java/lang/StringBuilder."<init>":()V
17   #6 = Methodref          #4.#24         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18   #7 = Methodref          #4.#25         // java/lang/StringBuilder.toString:()Ljava/lang/String;
19   #8 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
20   #9 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
21  #10 = Class              #30            // StringTest
22  #11 = Class              #31            // java/lang/Object
23  #12 = Utf8               <init>
24  #13 = Utf8               ()V
25  #14 = Utf8               Code
26  #15 = Utf8               LineNumberTable
27  #16 = Utf8               main
28  #17 = Utf8               ([Ljava/lang/String;)V
29  #18 = Utf8               SourceFile
30  #19 = Utf8               StringTest.java
31  #20 = NameAndType        #12:#13        // "<init>":()V
32  #21 = Utf8               abc
33  #22 = Utf8               def
34  #23 = Utf8               java/lang/StringBuilder
35  #24 = NameAndType        #32:#33        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36  #25 = NameAndType        #34:#35        // toString:()Ljava/lang/String;
37  #26 = Class              #36            // java/lang/System
38  #27 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
39  #28 = Class              #39            // java/io/PrintStream
40  #29 = NameAndType        #40:#41        // println:(Ljava/lang/String;)V
41  #30 = Utf8               StringTest
42  #31 = Utf8               java/lang/Object
43  #32 = Utf8               append
44  #33 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
45  #34 = Utf8               toString
46  #35 = Utf8               ()Ljava/lang/String;
47  #36 = Utf8               java/lang/System
48  #37 = Utf8               out
49  #38 = Utf8               Ljava/io/PrintStream;
50  #39 = Utf8               java/io/PrintStream
51  #40 = Utf8               println
52  #41 = Utf8               (Ljava/lang/String;)V
53{
54  public StringTest();
55    descriptor: ()V
56    flags: ACC_PUBLIC
57    Code:
58      stack=1, locals=1, args_size=1
59         0: aload_0
60         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
61         4: return
62      LineNumberTable:
63        line 1: 0
64
65  public static void main(java.lang.String[]);
66    descriptor: ([Ljava/lang/String;)V
67    flags: ACC_PUBLIC, ACC_STATIC
68    Code:
69      stack=2, locals=4, args_size=1
70         0: ldc           #2                  // String abc
71         2: astore_1
72         3: ldc           #3                  // String def
73         5: astore_2
74         6: new           #4                  // class java/lang/StringBuilder
75         9: dup
76        10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
77        13: aload_1
78        14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
79        17: aload_2
80        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
81        21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
82        24: astore_3
83        25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
84        28: aload_3
85        29: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
86        32: return
87      LineNumberTable:
88        line 3: 0
89        line 4: 3
90        line 5: 6
91        line 6: 25
92        line 7: 32
93}
94SourceFile: "StringTest.java"

首先看到使用了一個指針指向一個常量池中的對象內容爲「abc」,而另外一個指針指向「def」,此時經過new申請了一個StringBuilder,而後調用這個StringBuilder的初始化方法;而後分別作了兩次append操做,而後最後作一個toString()操做;可見String的+在編譯後會被編譯爲StringBuilder來運行,咱們知道這裏作了一個new StringBuilder的操做,而且作了一個toString的操做,若是你對JVM有所瞭解,凡是new出來的對象絕對不會放在常量池中,toString會發生一次內容拷貝,可是也不會在常量池中,因此在這裏常量池String+常量池String放在了堆中。指針

 

咱們再來看看另一種狀況,用一樣的方式來看看結果是什麼:code

代碼以下:對象

public class StringTest {
     public static void main(String[] args) {
         String c = "abc" + "def";
         System.out.println(c);
    }
}

咱們也來看看它編譯完成後是什麼樣子:

C:\Users\GRACE\Documents>javac StringTest.java
 2
 3C:\Users\GRACE\Documents>javap -verbose StringTest
 4Classfile /C:/Users/GRACE/Documents/StringTest.class
 5  Last modified 2018-7-21; size 426 bytes
 6  MD5 checksum c659d48ff8aeb45a3338dea5d129f593
 7  Compiled from "StringTest.java"
 8public class StringTest
 9  minor version: 0
10  major version: 52
11  flags: ACC_PUBLIC, ACC_SUPER
12Constant pool:
13   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
14   #2 = String             #16            // abcdef
15   #3 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
16   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
17   #5 = Class              #21            // StringTest
18   #6 = Class              #22            // java/lang/Object
19   #7 = Utf8               <init>
20   #8 = Utf8               ()V
21   #9 = Utf8               Code
22  #10 = Utf8               LineNumberTable
23  #11 = Utf8               main
24  #12 = Utf8               ([Ljava/lang/String;)V
25  #13 = Utf8               SourceFile
26  #14 = Utf8               StringTest.java
27  #15 = NameAndType        #7:#8          // "<init>":()V
28  #16 = Utf8               abcdef
29  #17 = Class              #23            // java/lang/System
30  #18 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
31  #19 = Class              #26            // java/io/PrintStream
32  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
33  #21 = Utf8               StringTest
34  #22 = Utf8               java/lang/Object
35  #23 = Utf8               java/lang/System
36  #24 = Utf8               out
37  #25 = Utf8               Ljava/io/PrintStream;
38  #26 = Utf8               java/io/PrintStream
39  #27 = Utf8               println
40  #28 = Utf8               (Ljava/lang/String;)V
41{
42  public StringTest();
43    descriptor: ()V
44    flags: ACC_PUBLIC
45    Code:
46      stack=1, locals=1, args_size=1
47         0: aload_0
48         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
49         4: return
50      LineNumberTable:
51        line 1: 0
52
53  public static void main(java.lang.String[]);
54    descriptor: ([Ljava/lang/String;)V
55    flags: ACC_PUBLIC, ACC_STATIC
56    Code:
57      stack=2, locals=2, args_size=1
58         0: ldc           #2                  // String abcdef
59         2: astore_1
60         3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
61         6: aload_1
62         7: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
63        10: return
64      LineNumberTable:
65        line 3: 0
66        line 4: 3
67        line 5: 10
68}
69SourceFile: "StringTest.java"

這一次編譯完後的代碼比前面少了不少,並且,仔細看,你會發現14行處,編譯的過程當中直接變成了"abcdef",這是爲何呢?由於當發生「abc」 + 「def」在同一行發生時,JVM在編譯時就認爲這個加號是沒有用處的,編譯的時候就直接變成

String d = "abcdef";

同理若是出現:String a =「a」 + 1,編譯時候就會變成:String a = 「a1″;

 

再補充一個例子:

final String a = "a";
final String b = "ab";
String c = a + b;

在編譯時候,c部分會被編譯爲:String c = 「aab」;可是若是a或b有任意一個不是final的,都會new一個新的對象出來;其次再補充下,若是a和b,是某個方法返回回來的,不論方法中是final類型的仍是常量什麼的,都不會被在編譯時將數據編譯到常量池,由於編譯器並不會跟蹤到方法體裏面去看你作了什麼,其次只要是變量就是可變的,即便你認爲你看到的代碼是不可變的,可是運行時是能夠被切入的。

那麼效率問題從何提及?

那說了這麼多,也沒看到有說效率方面的問題呀?

 

其實上面兩個例子,鏈接字符串行表達式很簡單,那麼"+"和StringBuilder基本是同樣的,但若是結構比較複雜,如使用循環來鏈接字符串,那麼產生的Java Byte Code就會有很大的區別。咱們再來看看下面一段代碼:

import java.util.*;
public class StringTest {
     public static void main(String[] args){
          String s = "";
          Random rand = new Random();
          for (int i = 0; i < 10; i++){
              s = s + rand.nextInt(1000) + " ";
          }
          System.out.println(s);
      }
}

上面代碼反編譯後的結果以下:

C:\Java\jdk1.8.0_171\bin>javap -c E:\StringTest.class
Picked up _JAVA_OPTIONS: -Xmx512M
Compiled from "StringTest.java"
public class StringTest {
  public StringTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       //String s = "";
       0: ldc           #16                 // String
       2: astore_1
       //Random rand = new Random();
       3: new           #18                 // class java/util/Random
       6: dup
       7: invokespecial #20                 // Method java/util/Random."<init>":()V
      10: astore_2
      //StringBuilder result = new StringBuilder();
      11: iconst_0
      12: istore_3
      13: goto          49
      //s = (new StringBuilder(String.valueOf(s))).append(rand.nextInt(1000)).append(" ").toString();
      16: new           #21                 // class java/lang/StringBuilder
      19: dup
      20: aload_1
      21: invokestatic  #23                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
      24: invokespecial #29                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
      27: aload_2
      28: sipush        1000
      31: invokevirtual #32                 // Method java/util/Random.nextInt:(I)I
      34: invokevirtual #36                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      37: ldc           #40                 // String
      39: invokevirtual #42                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      42: invokevirtual #45                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      45: astore_1
      46: iinc          3, 1
      49: iload_3
      50: bipush        10
      52: if_icmplt     16
      //System.out.println(s);
      55: getstatic     #49                 // Field java/lang/System.out:Ljava/io/PrintStream;
      58: aload_1
      59: invokevirtual #55                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      62: return
}

咱們能夠看到,雖然編譯器將"+"轉換成了StringBuilder,但建立StringBuilder對象的位置卻在for語句內部。這就意味着每執行一次循環,就會建立一個StringBuilder對象(對於本例來講,是建立了10StringBuilder對象),雖然Java有垃圾回收器,但這個回收器的工做時間是不定的。若是不斷產生這樣的垃圾,那麼仍然會佔用大量的資源。解決這個問題的方法就是在程序中直接使用StringBuilder來鏈接字符串,代碼以下:

import java.util.Random;
public class StringTest {
    public static void main(String[] args) {
        Random rand = new Random();
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            result.append(rand.nextInt(1000));
            result.append(" ");
        }
        System.out.println(result.toString());
    }
}

上面代碼反編譯後的結果以下:

C:\Java\jdk1.8.0_171\bin>javap -c E:\Dubbo\Demo\bin\StringTest.class
Picked up _JAVA_OPTIONS: -Xmx512M
Compiled from "StringTest.java"
public class StringTest {
  public StringTest();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       //Random rand = new Random();
       0: new           #16                 // class java/util/Random
       3: dup
       4: invokespecial #18                 // Method java/util/Random."<init>":()V
       7: astore_1
       //StringBuilder result = new StringBuilder();
       8: new           #19                 // class java/lang/StringBuilder
      11: dup
      12: invokespecial #21                 // Method java/lang/StringBuilder."<init>":()V
      15: astore_2
      //for(int i = 0; i < 10; i++)
      16: iconst_0
      17: istore_3
      18: goto          43
      //result.append(rand.nextInt(1000));
      21: aload_2
      22: aload_1
      23: sipush        1000
      26: invokevirtual #22                 // Method java/util/Random.nextInt:(I)I
      29: invokevirtual #26                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      32: pop
      //result.append(" ");
      33: aload_2
      34: ldc           #30                 // String
      36: invokevirtual #32                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      39: pop
      40: iinc          3, 1
      43: iload_3
      44: bipush        10
      46: if_icmplt     21
      //System.out.println(result.toString());
      49: getstatic     #35                 // Field java/lang/System.out:Ljava/io/PrintStream;
      52: aload_2
      53: invokevirtual #41                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      56: invokevirtual #45                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      59: return
}

從上面的反編譯結果能夠看出,建立StringBuilder的代碼被放在了for語句外。雖然這樣處理在源程序中看起來複雜,但卻換來了更高的效率,同時消耗的資源也更少了。

 

因此,從上述幾個例子中咱們得出的結論是:String採用鏈接運算符(+)效率低下,都是上述循環、大批量數據狀況形成的,每作一次"+"就產生個StringBuilder對象,而後append後就扔掉。下次循環再到達時從新產生個StringBuilder對象,而後append字符串,如此循環直至結束。若是咱們直接採用StringBuilder對象進行append的話,咱們能夠節省建立和銷燬對象的時間。若是隻是簡單的字面量拼接或者不多的字符串拼接,性能都是差很少的。

C:\Users\GRACE\Documents>javac StringTest.java
2C:\Users\GRACE\Documents>javap -verbose StringTest
3Classfile /C:/Users/GRACE/Documents/StringTest.class
4  Last modified 2018-7-21; size 607 bytes
5  MD5 checksum a2729f11e22d7e1153a209e5ac968b98
6  Compiled from "StringTest.java"
7public class StringTest
8  minor version: 0
9  major version: 52
10  flags: ACC_PUBLIC, ACC_SUPER
11Constant pool:
12   #1 = Methodref          #11.#20        // java/lang/Object."<init>":()V
13   #2 = String             #21            // abc
14   #3 = String             #22            // def
15   #4 = Class              #23            // java/lang/StringBuilder
16   #5 = Methodref          #4.#20         // java/lang/StringBuilder."<init>":()V
17   #6 = Methodref          #4.#24         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18   #7 = Methodref          #4.#25         // java/lang/StringBuilder.toString:()Ljava/lang/String;
19   #8 = Fieldref           #26.#27        // java/lang/System.out:Ljava/io/PrintStream;
20   #9 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
21  #10 = Class              #30            // StringTest
22  #11 = Class              #31            // java/lang/Object
23  #12 = Utf8               <init>
24  #13 = Utf8               ()V
25  #14 = Utf8               Code
26  #15 = Utf8               LineNumberTable
27  #16 = Utf8               main
28  #17 = Utf8               ([Ljava/lang/String;)V
29  #18 = Utf8               SourceFile
30  #19 = Utf8               StringTest.java
31  #20 = NameAndType        #12:#13        // "<init>":()V
32  #21 = Utf8               abc
33  #22 = Utf8               def
34  #23 = Utf8               java/lang/StringBuilder
35  #24 = NameAndType        #32:#33        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36  #25 = NameAndType        #34:#35        // toString:()Ljava/lang/String;
37  #26 = Class              #36            // java/lang/System
38  #27 = NameAndType        #37:#38        // out:Ljava/io/PrintStream;
39  #28 = Class              #39            // java/io/PrintStream
40  #29 = NameAndType        #40:#41        // println:(Ljava/lang/String;)V
41  #30 = Utf8               StringTest
42  #31 = Utf8               java/lang/Object
43  #32 = Utf8               append
44  #33 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
45  #34 = Utf8               toString
46  #35 = Utf8               ()Ljava/lang/String;
47  #36 = Utf8               java/lang/System
48  #37 = Utf8               out
49  #38 = Utf8               Ljava/io/PrintStream;
50  #39 = Utf8               java/io/PrintStream
51  #40 = Utf8               println
52  #41 = Utf8               (Ljava/lang/String;)V
53{
54  public StringTest();
55    descriptor: ()V
56    flags: ACC_PUBLIC
57    Code:
58      stack=1, locals=1, args_size=1
59         0: aload_0
60         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
61         4return
62      LineNumberTable:
63        line 10
64
65  public static void main(java.lang.String[]);
66    descriptor: ([Ljava/lang/String;)V
67    flags: ACC_PUBLIC, ACC_STATIC
68    Code:
69      stack=2, locals=4, args_size=1
70         0: ldc           #2                  // String abc
71         2: astore_1
72         3: ldc           #3                  // String def
73         5: astore_2
74         6: new           #4                  // class java/lang/StringBuilder
75         9: dup
76        10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
77        13: aload_1
78        14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
79        17: aload_2
80        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
81        21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
82        24: astore_3
83        25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
84        28: aload_3
85        29: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
86        32return
87      LineNumberTable:
88        line 30
89        line 43
90        line 56
91        line 625
92        line 732
93}
94SourceFile: "StringTest.java"

相關文章
相關標籤/搜索