本文故事構思來源於脈脈上的一篇帖子“一行程式碼引發的血案”。
其實關於字串的文章,我之前也寫過一篇《詭異的字串問題》,字串對於我們開發者而言,可以用最近很流行的一句話“用起來好嗨喲,彷彿人生達到了巔峰”。
確實大家都用的很嗨,很便利,但 JDK 的工程師在背後付出了努力又有幾個人真的在意呢?
我們們今天就通過一個例子來詳細的說明。
public class StringTest {
public static void main(String[] args) {
// 無變數的字串拼接
String s = "aa"+"bb"+"dd";
System.out.println(s);
// 有變數的字串拼接
String g = "11"+s+5;
System.out.println(g);
// 迴圈中使用字串拼接
String a = "0";
for (int i = 1; i < 10; i++) {
a = a + i;
}
System.out.println(a);
// 迴圈外定義StringBuilder
StringBuilder b = new StringBuilder();
for (int i = 1; i < 10; i++) {
b.append(i);
}
System.out.println(b);
}
}
複製程式碼
有同學可能會說,這麼一段程式碼,怎麼來區分呢?既然我在對話中說了 Java 從 JDK5 開始,便在編譯期間進行了優化,那麼編譯期間 javac 命令主要乾了什麼事情呢?一句話歸根結底,那麼肯定就是把 .java 原始碼編譯成 .class 檔案,也就是我們常說的中間語言——位元組碼。然後 JVM 引擎再對 .class 檔案進行驗證,解析,翻譯成本地可執行的機器指令,這只是一個最簡單的模型,其實現在的 JVM 引擎後期還會做很多優化,比如程式碼熱點分析,JIT編譯,逃逸分析等。
說到這裡,我記得之前群裡有同學說,位元組碼長得太醜了,看了第一眼就不想看第二眼,哈哈,醜是醜點,但是很有內涵,能量強大,實現了跨平臺性。
關於怎麼檢視位元組碼,我之前分享過兩個工具,一個 JDK 自帶的 javap,另一個IDEA的外掛 jclasslib Bytecode viewer。今天給你再分享一個,我之前破解 apk 常用的工具 jad,它會讓你看位元組碼檔案輕鬆很多。
先說一下,我分別用 Jdk 1.6 - 1.8 自帶的 javap 工具進行了反編譯,發現生成的 JVM 指令是一樣的,所以在此處不會列出每一個版本生成的指令檔案。為了便於大家閱讀指令檔案,這裡用jad工具生成,程式碼如下。
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) annotate
// Source File Name: StringTest.java
import java.io.PrintStream;
public class StringTest
{
public StringTest()
{
// 0 0:aload_0
// 1 1:invokespecial #1 <Method void Object()>
// 2 4:return
}
public static void main(String args[])
{
String s = "aabbdd";
// 0 0:ldc1 #2 <String "aabbdd">
// 1 2:astore_1
System.out.println(s);
// 2 3:getstatic #3 <Field PrintStream System.out>
// 3 6:aload_1
// 4 7:invokevirtual #4 <Method void PrintStream.println(String)>
String g = (new StringBuilder()).append("11").append(s).append(5).toString();
// 5 10:new #5 <Class StringBuilder>
// 6 13:dup
// 7 14:invokespecial #6 <Method void StringBuilder()>
// 8 17:ldc1 #7 <String "11">
// 9 19:invokevirtual #8 <Method StringBuilder StringBuilder.append(String)>
// 10 22:aload_1
// 11 23:invokevirtual #8 <Method StringBuilder StringBuilder.append(String)>
// 12 26:iconst_5
// 13 27:invokevirtual #9 <Method StringBuilder StringBuilder.append(int)>
// 14 30:invokevirtual #10 <Method String StringBuilder.toString()>
// 15 33:astore_2
System.out.println(g);
// 16 34:getstatic #3 <Field PrintStream System.out>
// 17 37:aload_2
// 18 38:invokevirtual #4 <Method void PrintStream.println(String)>
String a = "0";
// 19 41:ldc1 #11 <String "0">
// 20 43:astore_3
for(int i = 1; i < 10; i++)
//* 21 44:iconst_1
//* 22 45:istore 4
//* 23 47:iload 4
//* 24 49:bipush 10
//* 25 51:icmpge 80
a = (new StringBuilder()).append(a).append(i).toString();
// 26 54:new #5 <Class StringBuilder>
// 27 57:dup
// 28 58:invokespecial #6 <Method void StringBuilder()>
// 29 61:aload_3
// 30 62:invokevirtual #8 <Method StringBuilder StringBuilder.append(String)>
// 31 65:iload 4
// 32 67:invokevirtual #9 <Method StringBuilder StringBuilder.append(int)>
// 33 70:invokevirtual #10 <Method String StringBuilder.toString()>
// 34 73:astore_3
// 35 74:iinc 4 1
//* 36 77:goto 47
System.out.println(a);
// 37 80:getstatic #3 <Field PrintStream System.out>
// 38 83:aload_3
// 39 84:invokevirtual #4 <Method void PrintStream.println(String)>
StringBuilder b = new StringBuilder();
// 40 87:new #5 <Class StringBuilder>
// 41 90:dup
// 42 91:invokespecial #6 <Method void StringBuilder()>
// 43 94:astore 4
for(int i = 1; i < 10; i++)
//* 44 96:iconst_1
//* 45 97:istore 5
//* 46 99:iload 5
//* 47 101:bipush 10
//* 48 103:icmpge 120
b.append(i);
// 49 106:aload 4
// 50 108:iload 5
// 51 110:invokevirtual #9 <Method StringBuilder StringBuilder.append(int)>
// 52 113:pop
// 53 114:iinc 5 1
//* 54 117:goto 99
System.out.println(b);
// 55 120:getstatic #3 <Field PrintStream System.out>
// 56 123:aload 4
// 57 125:invokevirtual #12 <Method void PrintStream.println(Object)>
// 58 128:return
}
}
複製程式碼
這裡說一下分析結果。
1、無變數的字串拼接,在編譯期間值都確定了,所以 javac 工具幫我們把它直接編譯成一個字元常量。
2、有變數的字串拼接,在編譯期間變數的值無法確定,所以執行期間會生成一個StringBuilder 物件。
3、迴圈中使用字串拼接,迴圈內,每迴圈一次就會產生一個新的 StringBuilder 物件,對資源有一定的損耗。
4、迴圈外使用 StringBuilder,迴圈內再執行 append() 方法拼接字串,只會成一個 StringBuilder 物件。
因此,對於有迴圈的字串拼接操作,建議使用 StringBuilder 和 StringBuffer,對效能會有一定的提升。
其實上面的結論,《阿里巴巴Java開發手冊》中有所提到,此文正好與該條結論相對應。
一個簡單的字串,用起來確實簡單,背後付出了多少工程師的心血,在此,深深地佩服詹爺。
PS:本文原創釋出於微信公眾號 「Java面試那些事兒」,關注並回復「1024」,免費領學習資料。 原文地址