Java字符串拼接效率的比較和對String.valueOf的思考

這兩天看到的關於Java的一篇文章(《阿里資深工程師教你如何優化Java代碼》)中有寫到使用String.valueOf(value)代替 “” + value的建議,原因是“當要把其他對象或類型轉換爲字符串時,使用String.valueOf(value)比""+value的效率更高。”
反例:

int i = 1;
String s = "" + i;

正例:

int i = 1;
String s = String.valueOf(i);

當看到上面的建議的時候是認同的,後來仔細一想Android framework上面log的寫法都是下面的方式:

int num = 1;
int count = 2;
Rlog.d(LOG_TAG, "the num:" + num + ", the count:" + count);

而Rlog.d的原型函數如下:

\frameworks\base\telephony\java\android\telephony\Rlog.java
public static int (String tag, String msg) {
    // native實現
}

函數實現上看,打印的日誌信息就是String類型的,Android中的使用方法就是反例,效率不高。是不是應該改成

int num = 1;
int count = 2;
Rlog.d(LOG_TAG, "the num:" + String.valueOf(num) + ", the count:" + String.valueOf(count));

但是Android code中幾乎看不到上面這種寫法,那就奇怪了,Android中打印的log數量還是很可觀的,尤其是測試版本上,log更是很多。那Android爲什麼不使用String.valueOf呢?難道Android錯了?當問題提出後,我打算通過字節碼看一下反例和正例的差別,另外就是通過實際測試看一下兩種效率的差異。

有些問題通過字節碼就能看出端倪。下面是需要轉譯字節碼的原始code,比較常用,尤其是打印log,字符串和整數拼接出一個新的自付出。

public class TestValueOf {
public static void main(String[] args) {
    int i = 0;
    String s = "abcdef";
    String s1 = s + i;
    System.out.println(s1);
    String d = "abcdef";
    String d1 = d + String.valueOf(i);
    System.out.println(d1);
}
}

下面是對應生成的字節碼:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=6, args_size=1
0: iconst_0
1: istore_1
2: ldc #2 // String abcdef
4: astore_2
5: new #3 // class java/lang/StringBuilder
8: dup
9: invokespecial #4 // Method java/lang/StringBuilder.""?)V
12: aload_2
13: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: iload_1
17: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
20: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
23: astore_3
24: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
27: aload_3
28: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: ldc #2 // String abcdef
33: astore 4
35: new #3 // class java/lang/StringBuilder
38: dup
39: invokespecial #4 // Method java/lang/StringBuilder.""?)V
42: aload 4
44: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: iload_1
48: invokestatic #10 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
51: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

54: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
57: astore 5
59: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
62: aload 5
64: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
67: return

從字節碼可以看出String的拼接都是使用StringBuilder來實現的,最後的輸出結果是StringBuilder.toString轉成String類型。("" + i)是通過StringBuilder.append(int)實現的,而""+String.valueOf(i)是先調用String.ValueOf(i)之後調用StringBuilder.append(String)實現的。我們通過字節碼翻譯成Java語言,這樣能夠更清楚理解上面的例子到底差別在哪裏。下面的code是通過字節碼寫出來的,重新編譯再次重新獲取一次字節碼

public class TestStringBuilder {
public static void main(String[] args) {
    int i = 0;
    String s = "abcdef";
    String s1 = (new StringBuilder()).append(s).append(i).toString();
    System.out.println(s1);
    String d = "abcdef";
    String d1 = (new StringBuilder()).append(d).append(String.valueOf(i)).toString();
    System.out.println(d1);
}
}

這個例子對應的字節碼,可以看到和上面的字節碼一模一樣,可不是重新copy了一次,這是重新編譯重新獲取到的,大家可以測試一下。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=6, args_size=1
0: iconst_0
1: istore_1
2: ldc #2 // String abcdef
4: astore_2
5: new #3 // class java/lang/StringBuilder
8: dup
9: invokespecial #4 // Method java/lang/StringBuilder.""?)V
12: aload_2
13: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: iload_1
17: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
20: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
23: astore_3
24: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
27: aload_3
28: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: ldc #2 // String abcdef
33: astore 4
35: new #3 // class java/lang/StringBuilder
38: dup
39: invokespecial #4 // Method java/lang/StringBuilder.""?)V
42: aload 4
44: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: iload_1
48: invokestatic #10 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
51: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
54: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
57: astore 5
59: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
62: aload 5
64: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
67: return

這裏的字節碼分析只是想提供一種思路,另外能夠通過字節碼瞭解實際運行程序的邏輯,寫出的java代碼和字節碼邏輯差別還是很大的,通過字節碼能夠更好的理解原始的code,如果能夠達到寫code的時候就能想象到對應的字節碼,那必定是大師的了,寫的code必定是極好的。
字節碼分析並不能找到爲什麼String.valueOf效率更高的原因。那我們通過實驗測試一下效率吧。code中用中文寫的註釋大家一定要高度重視,都是測試走過的坑。

public class TestV {
    private static final int NUM_COUNT = 10000000;
public static void main(String[] args) {
    long t1 = 0L;
    long t2 = 0L;
    String s = "abcdef";
    String s1 = "abcdef";

    // 下面主要是爲了避免thread start過程對測試的影響,我們需要thread穩定後的測試結構,
    // 可以將這段code刪除後,看一下測試結果
    for (int i = 0; i < 10000 * 1000; i++) {
        s ="" + i;
    }

    t1 = System.currentTimeMillis();
    // 千萬不要使用下面函數打印開始結束的時間,因爲這個很浪費時間
    // 可能把String.valueOf或者"" + i的時間覆蓋掉,那就非常不準確了
    //System.out.println(t1);
    // 測試的時候可以將下面代碼段重複幾遍。
    for (int i = 0; i < NUM_COUNT; i++) {
        s1 ="" + String.valueOf(i);
    }
    t2 = System.currentTimeMillis();
    //System.out.println(t2);
    System.out.println(t2-t1);

    t1 = System.currentTimeMillis();
    for (int i = 0; i < NUM_COUNT; i++) {
        s ="" + i;
    }
    t2 = System.currentTimeMillis();
    System.out.println(t2-t1);
}
}

當NUM_COUNT=10000000,""+String.valueOf的時間在450毫秒左右,而"" + i在200毫秒左右,這和我們的認知有很大的區別,大家可以實測一下,我嘗試了多次,基本是穩定的,當改變NUM_COUNT的值,""+String.valueOf效率都不如"" + i。我以爲是由於使用for循環的自變量引起的,又改成下面的code,進行了測試。

public class TestVValue {
    private static final int NUM_COUNT = 10000000;
public static void main(String[] args) {
    long t1 = 0L;
    long t2 = 0L;
    String s = "abcdef";
    String s1 = "abcdef";
    
   nt testValue = 1;
    // 下面主要是爲了避免thread start過程對測試的影響,我們需要thread穩定後的測試結構,
    // 可以將這段code刪除後,看一下測試結果
    for (int i = 0; i < 10000 * 1000; i++) {
        s ="" + i;
    }

    t1 = System.currentTimeMillis();
    // 千萬不要使用下面函數打印開始結束的時間,因爲這個很浪費時間
    // 可能把String.valueOf或者"" + i的時間覆蓋掉,那就非常不準確了
    //System.out.println(t1);
    // 測試的時候可以將下面代碼段重複幾遍。
    for (int i = 0; i < NUM_COUNT; i++) {
        s1 ="" + String.valueOf(testValue);
    }
    t2 = System.currentTimeMillis();
    //System.out.println(t2);
    System.out.println(t2-t1);

    t1 = System.currentTimeMillis();
    for (int i = 0; i < NUM_COUNT; i++) {
        s ="" + testValue;
    }
    t2 = System.currentTimeMillis();
    System.out.println(t2-t1);
}
}

上面的效率上比自變量的對應測試都要高,但是""+String.valueOf的效率還是比"" + testValue的效率差,當NUM_COUNT = 10000000,""+String.valueOf的時間在200多一點,而"" + testValue在90不到100毫秒。
現在至少可以得出一個結論,"" + String.valueOf的時間其實是高於"" + testValue的。當我得出這個結論的時候,感覺不可思議,難道是大牛錯了,不應該呀又仔細看了大牛的文章和自己的code,看到了差異,上面寫的測試code

s1 ="" + String.valueOf(testValue);

前面都帶了"",上面引用的文章中正例中是沒有"",將code修改爲s1 =String.valueOf(testValue);再次測試,結果就對了,String.valueOf的時間馬上比"" + testValue少多了,大概在20毫秒,而"" + testValue在90不到100毫秒。
這樣就對了,爲什麼少呢?我們再寫一個sample,看一下字節碼。code如下:

public class TestS {
public static void main(String[] args) {
    int i = 0;
    String s1 = "" + i;
    System.out.println(s1);
    String d1 = String.valueOf(i);
    System.out.println(d1);
}
}

上面對應的字節碼:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_0
1: istore_1
2: new #2 // class java/lang/StringBuilder
5: dup
6: invokespecial #3 // Method java/lang/StringBuilder.""?)V
9: ldc #4 // String
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: iload_1
15: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;

18: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: astore_2
22: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
25: aload_2
26: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
29: iload_1
30: invokestatic #10 // Method java/lang/String.valueOf:(I)Ljava/lang/String;
33: astore_3
34: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
37: aload_3
38: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
41: return

從字節碼可以看到String s1 = “” + i;使用了兩次StringBuilder.append和一次StringBuilder.toString,但是String d1 = String.valueOf(i);只使用了一次String.valueOf,我們有足夠的理由說明String d1 = String.valueOf(i)的確比String s1 = “” + i效率高多了。
最後在測試一種情況,也就是常量的情形。s1 ="" + String.valueOf(1)s ="" + 1 通過測試我們發現如果是常量那麼s1 ="" + String.valueOf(1)速度和是變量效率基本相同,而s ="" + 1的速度是非常快的,大約在20毫秒。
現在我們再次分析和猜測一下s1 ="" + String.valueOf(1)效率比s ="" + 1差的原因,s1 ="" + String.valueOf(1)會執行一次String.valueOf,兩次append string,而s ="" + 1執行了一次append String,一次append int,只有兩次append,而沒有String.valueOf。那麼String.valueOf和一次append string的時間看樣子是要大於一次apped int的時間。
那麼Android中打印log是沒有問題的,解除了我們的疑惑,千萬不要寫成開頭那個樣子,如下,這是不對的,Android code中的寫法沒有任何問題,現在是豁然開朗了。

int num = 1;
int count = 2;
Rlog.d(LOG_TAG, "the num:" + String.valueOf(num) + ", the count:" + String.valueOf(count));

我們總結一下:
1 如果只需要將一個整數轉換爲String,請使用String.valueOf。
反例:
int i = 1;
String s = “” + i;

正例:
int i = 1;
String s = String.valueOf(i);

2 凡是需要將整數和String類型的變量進行拼接的,無論這個整數是常量還是變量,請直接使用str + i的方法,不要使用String.valueOf。如果這個整數能夠確定,那麼請直接使用整數常數,不要使用變量。
反例
int i = 1;
String s = “abc” + String.valueOf(i);
或者
String s = “abc” + String.valueOf(1);

正例:
int i = 1;
String s = “abc” + i;
或者
//如果能夠確定i肯定等於1
String s = “abc” + 1;

在日常學習和生活中,我們還是要懷着懷疑的精神看別人的文章,要質疑權威,當然不是盲目的,自己多寫多測多思考,只有這樣才能進步,離着大牛越來越近,如果是自己錯了,要勇敢承認,查找原因,並記錄下來。

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