Java 字串連線運算子幹了什麼?

悟三空發表於2020-06-24

和其他多數程式設計語言一樣,Java 語言允許使用 + 連線兩個字串。

String name = "stephen";
String foo = "Hey, " + name;

當我們將一個字串和一個非字串的值進行拼接時,並不會報錯:

String name = "Stephen";
int age = 25;
String foo = name + age; // 結果為 Stephen25

其原因是當 + 運算子左右兩邊有一個值是字串時,會將另一個值嘗試轉化為字串。

字串轉換機制

我們在瞭解字串連線運算子前,先了解一下字串轉換機制(String Conversion)。

Any type may be converted to type String by string conversion.

如果值 x 是基本資料型別 T,那麼在字串轉換前,首先會將其轉換成一個引用值,舉幾個例子:

• 如果 T 是 boolean 型別的,那麼就會用 new Boolean(x) 封裝一下;

• 如果 T 是 char 型別的,那麼就會用 new Character(x) 封裝一下;

• 如果 T 是 byte、short、int 型別的,那麼就會用 new Integer(x) 封裝一下;

我們知道,對於基本資料型別,Java 都對應有一個包裝類(比如 int 型別對應有 Integer 物件),這樣操作以後,每個基礎資料型別的值 x 都變成了一個物件的引用。

為什麼這麼做?為了統一對待,當我們把基礎資料型別轉換成對應的包裝類的一個例項後,所有的值都是統一的物件引用。

此時才開始真正進行字串轉換。我們需要考慮兩種情況:空值和非空值。

如果此時的值 x 是 null,那麼最終的字串轉換結果就是一個字串 null

否則就會呼叫這個物件的 toString() 的無參方法

前者很好理解,後者我們一起來看看:

在 Java 所有的父類 Object 中,有一個重要的方法就是 toString 方法,它返回表示物件值的一個字串。在 Object 類中對 toString 的定義如下:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

該方法返回物件的類名和雜湊碼。如果類沒有重寫 toString 方法,預設就會呼叫它的父類的 toString 方法,而此時我們的值 x 統一都是物件值,所以一定有 toString 方法可以呼叫並列印出值(也有個特殊,如果呼叫 toString 返回的值是一個 null 值,那麼就會用字串 null 代替)。

字串連線符

+ 運算子左右兩邊參與運算的表示式的值有一個為字串時,那麼在程式執行時會對另一個值進行字串轉換

這裡需要注意的是 + 運算子同時作為算術運算子,在含有多個值參與運算的時候,要留意優先順序,比如下面這個例子:

String a = 1 + 2 + " equals 3";
String b = "12 eqauls " + 1 + 2;

變數 a 的結果是 3 equals 3,變數 b 的結果是 12 equals 12

有些人這裡可能會有疑問,解釋一下,第一種情況根據運算優先順序是先計算 1+2 那麼此時的 + 運算子是算術運算子,所以結果是 3,然後再和 " equals 3" 運算,又因為 3 + " equals 3" 有一個值為字串,所以 + 運算子是字串連線運算子。

在執行時,Java 編譯器一般會使用類似 StringBuffer/StringBuilder 這樣帶緩衝區的方式來減少通過執行表示式時建立的中間 String 物件的數量,從而提高程式效能。

我們可以用 Java 自帶的反彙編工具 javap 簡單的看一下:

假設有如下這段程式碼:

public class Demo {
    public static void main(String[] args) {
        int i = 10;
        String words = "stephen" + i;
    }
}

然後編譯,再反彙編一下:

javac Demo.java
javap -c Demo

可以得到如下內容:

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

  public static void main(java.lang.String[]);
    Code:
       0: bipush        10
       2: istore_1
       3: new           #2                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
      10: ldc           #4                  // String stephen
      12: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      15: iload_1
      16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_2
      23: return
}

我們可以發現,Java 編譯器在執行字串連線運算子所在表示式的時候,會先建立一個 StringBuilder 物件,然後將運算子左邊的字串 stephen 拼接(append)上去,接著在拼接右邊的整型 10,然後呼叫 StringBuilder 的 toString 方法返回結果。

如果我們拼接的是一個物件呢?

public class Demo {
    public static void main(String[] args) {
        Demo obj = new Demo();
        String words = obj + "stephen";
    }

    @Override
    public String toString() {
        return "App{}";
    }
}

一樣的做法,我們會發現此時 Method java/lang/StringBuilder.append:(Ljava/lang/Object;) 也就是 StringBuilder 呼叫的是 append(Object obj) 這個方法,我們檢視 StringBuilder 類的 append 方法:

public StringBuilder append(Object obj) {
    return append(String.valueOf(obj));
}

String.valueOf(obj) 的實現程式碼如下:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();
}

也就是會呼叫物件的 toString() 方法。

可能到這裡大家會有一個疑問:上面不是說字串轉換對於基本型別是先轉換成對應的包裝類,然後呼叫它的 toString 方法嗎,這邊怎麼都是呼叫 StringBuilder 的 append 方法了呢?

實現方式不同,其實是本質上是一樣的,只不過為了提高效能(減少建立中間字串等的損耗),Java 編譯器採用 StringBuilder 來做。感興趣的可以自己去追蹤下 Integer 包裝類的 toString 方法,其實和 StringBuilder 的 append(int i) 方法的程式碼是幾乎一樣的。

參考連結

String Concatenation Operator +

String Conversion

相關文章