Java—String字符串運算符"+"重載分析

引言

本章主要是分析字符串重載“+”背後做了些什麼;深度分析爲什麼循環拼接字符串時應使用StringBuilder,而不應使用“+”。

重載運算符“+”背後做了什麼?

首先我們都需要明確字符串對象是不可改變的,所有字符串對象的拼接和修改,實際上都是創建了一個新的字符串對象。在運行時,編譯器會將所有的以“a”形式聲明的字符串加載到字符串常量池中,之後再次使用時都會從常量池中獲取。我們看下面的例子:
(1)字符串直接+
StringD類爲例
源碼:

public class StringD {
    public static void main(String[] args) {
        String str = "a" + "b";
    }
}

jvm字節碼:

Compiled from "StringD.java"
public class com.owl.zookeeper.string.StringD {
  public com.owl.zookeeper.string.StringD();
    Code:
       0: aload_0       
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return        

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String ab 解釋:這裏編譯器會在編譯時優化,直接將ab字符串保存在常量池中,之後從常量池中取出,然後入棧
       2: astore_1      // 解釋:這裏將ab出棧,然後賦值給變量str
       3: return        
}

可以看出編譯器會對字符串運算符進行優化,使得不需要創建多餘的字符串對象。

(2)字符串變量+
StringAppend對象實例
源代碼:

public class StringAppend {

    public static void main(String[] args) {
        String a = "1";
        String b = "2" + a + "3";
        System.out.println(b);
    }
}

Jvm字節碼:

 Compiled from "StringAppend.java"
 public class com.owl.zookeeper.string.StringAppend {
 public com.owl.zookeeper.string.StringAppend();
 Code:
 0: aload_0  //將this入棧
 1: invokespecial #1                  // Method java/lang/Object."<init>":()V //調用Object的構造方法
 4: return                            //返回void

 public static void main(java.lang.String[]);
 Code:
 0: ldc           #2                  // String 1  解釋:LDC cst:將常量池偏移量爲cst的值入棧,譬如LDC #12,在操作棧中會佔用1個字長
 2: astore_1                          //解釋:將棧頂的String 1賦值給第一個變量a
 3: new           #3                  // class java/lang/StringBuilder 解釋:創建對象,並將該對象入棧頂
 6: dup                               // 解釋:複製棧頂數據StringBuilder。因爲方法調用會彈出參數(這裏是Object對象),因此需要上面的dup指令,保證在調用構造函數之後棧頂上還是 Object對象的引用,很多種情況下dup指令都是爲這個目的而存在的。
 7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V 解釋:調用StringBuilder構造方法
 10: ldc           #5                  // String 2 解釋:將String 2壓入棧頂
 12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 解釋:棧頂2出棧,作爲方法入參,調用java/lang/StringBuilder.append()方法
 15: aload_1                           // 解釋:將變量a的1入棧
 16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 解釋:將變量a的1出棧,作爲方法入參,調用java/lang/StringBuilder.append()方法
 19: ldc           #7                  // String 3 解釋:將常量池中的字符串3入棧
 21: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 解釋:將字符串3出棧,作爲方法入參,調用java/lang/StringBuilder.append()方法
 24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 解釋:調用StringBuilder.toString
 27: astore_2                          // 解釋:將返回值保存在變量b
 28: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream; 解釋:獲得靜態變量,System.out
 31: aload_2                           // 解釋:變量2入棧
 32: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V // 解釋:變量2出棧,作爲println的方法
 35: return                            // 解釋:返回void
 }

可以看出只要+拼接時,存在已經定義的變量,則會調用StringBuilder進行拼接操作,所以在定義字符串時,儘量不應使用變量。

(3)字符串循環+
StringForAppend類實例
源代碼:

public class StringForAppend {

    public static void main(String[] args) {
        String a = "";
        for(int i = 0; i < 3; i ++) {
            a += "1";
        }
        System.out.println(a);
    }
}

Jvm字節碼:

 Compiled from "StringForAppend.java"
 public class com.owl.zookeeper.string.StringForAppend {
 public com.owl.zookeeper.string.StringForAppend();
 Code:
 0: aload_0
 1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 4: return

 public static void main(java.lang.String[]);
 Code:
 0: ldc           #2                  // String 解釋:將常量空字符串入棧
 2: astore_1                          // 解釋:將空字符串賦值給a
 3: iconst_0                          // 解釋:將常量integer 0入棧
 4: istore_2                          // 解釋:出棧0,賦值給變量0
 5: iload_2                           // 解釋:將變量i=0,入棧
 6: iconst_3                          // 解釋:將integer 3入棧
 7: if_icmpge     36                  // 解釋:比較棧頂兩個元素,如果相等則跳轉到36
 10: new           #3                  // class java/lang/StringBuilder 創建StringBuilder,併入棧
 13: dup                               // 解釋:複製棧頂
 14: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V  // 解釋:出棧調用StringBuilder的構造方法
 17: aload_1                           // 解釋:入棧空字符串
 18: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 出棧空字符串,出棧StringBuilder,執行append方法
 21: ldc           #6                  // String 1 解釋:常量1入棧
 23: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 出棧字符串1,調用append方法
 26: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 出棧StringBuilder,調用append方法,將結果入棧
 29: astore_1                          // 解釋:出棧""+1,保存到變量a
 30: iinc          2, 1                // 解釋:將變量i,遞增1
 33: goto          5                   // 解釋:跳轉到步驟5
 36: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;      // 解釋:獲得對象PrintStream,入棧
 39: aload_1                           // 入棧變量a
 40: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V // 出棧變量a,出棧PrintStream,調用println方法
 43: return                            //解釋:返回
 }

可以看出在 33會調回會位置5,位置10會創建多個StringBuilder,所以不應該使用for循環還+的形式拼接,應該在外部創建一個StringBuilder的形式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章