本文故事構(gòu)思來(lái)源于脈脈上的一篇帖子“一行代碼引發(fā)的血案”。

其實(shí)關(guān)于字符串的文章,我之前也寫(xiě)過(guò)一篇《詭異的字符串問(wèn)題》,字符串對(duì)于我們開(kāi)發(fā)者而言,可以用最近很流行的一句話“用起來(lái)好嗨喲,仿佛人生達(dá)到了巔峰”。
確實(shí)大家都用的很嗨,很便利,但 JDK 的工程師在背后付出了努力又有幾個(gè)人真的在意呢?
咱們今天就通過(guò)一個(gè)例子來(lái)詳細(xì)的說(shuō)明。
public class StringTest {
public static void main(String[] args) {
// 無(wú)變量的字符串拼接
String s = "aa"+"bb"+"dd";
System.out.println(s);
// 有變量的字符串拼接
String g = "11"+s+5;
System.out.println(g);
// 循環(huán)中使用字符串拼接
String a = "0";
for (int i = 1; i < 10; i++) {
a = a + i;
}
System.out.println(a);
// 循環(huán)外定義StringBuilder
StringBuilder b = new StringBuilder();
for (int i = 1; i < 10; i++) {
b.append(i);
}
System.out.println(b);
}
}
有同學(xué)可能會(huì)說(shuō),這么一段代碼,怎么來(lái)區(qū)分呢?既然我在對(duì)話中說(shuō)了 Java 從 JDK5 開(kāi)始,便在編譯期間進(jìn)行了優(yōu)化,那么編譯期間 javac 命令主要干了什么事情呢?一句話歸根結(jié)底,那么肯定就是把 .java 源碼編譯成 .class 文件,也就是我們常說(shuō)的中間語(yǔ)言——字節(jié)碼。然后 JVM 引擎再對(duì) .class 文件進(jìn)行驗(yàn)證,解析,翻譯成本地可執(zhí)行的機(jī)器指令,這只是一個(gè)最簡(jiǎn)單的模型,其實(shí)現(xiàn)在的 JVM 引擎后期還會(huì)做很多優(yōu)化,比如代碼熱點(diǎn)分析,JIT編譯,逃逸分析等。
說(shuō)到這里,我記得之前群里有同學(xué)說(shuō),字節(jié)碼長(zhǎng)得太丑了,看了第一眼就不想看第二眼,哈哈,丑是丑點(diǎn),但是很有內(nèi)涵,能量強(qiáng)大,實(shí)現(xiàn)了跨平臺(tái)性。
關(guān)于怎么查看字節(jié)碼,我之前分享過(guò)兩個(gè)工具,一個(gè) JDK 自帶的 javap,另一個(gè)IDEA的插件 jclasslib Bytecode viewer。今天給你再分享一個(gè),我之前破解 apk 常用的工具 jad,它會(huì)讓你看字節(jié)碼文件輕松很多。
先說(shuō)一下,我分別用 Jdk 1.6 - 1.8 自帶的 javap 工具進(jìn)行了反編譯,發(fā)現(xiàn)生成的 JVM 指令是一樣的,所以在此處不會(huì)列出每一個(gè)版本生成的指令文件。為了便于大家閱讀指令文件,這里用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
}
}
這里說(shuō)一下分析結(jié)果。
1、無(wú)變量的字符串拼接,在編譯期間值都確定了,所以 javac 工具幫我們把它直接編譯成一個(gè)字符常量。
2、有變量的字符串拼接,在編譯期間變量的值無(wú)法確定,所以運(yùn)行期間會(huì)生成一個(gè)StringBuilder 對(duì)象。
3、循環(huán)中使用字符串拼接,循環(huán)內(nèi),每循環(huán)一次就會(huì)產(chǎn)生一個(gè)新的 StringBuilder 對(duì)象,對(duì)資源有一定的損耗。
4、循環(huán)外使用 StringBuilder,循環(huán)內(nèi)再執(zhí)行 append() 方法拼接字符串,只會(huì)成一個(gè) StringBuilder 對(duì)象。
因此,對(duì)于有循環(huán)的字符串拼接操作,建議使用 StringBuilder 和 StringBuffer,對(duì)性能會(huì)有一定的提升。
其實(shí)上面的結(jié)論,《阿里巴巴Java開(kāi)發(fā)手冊(cè)》中有所提到,此文正好與該條結(jié)論相對(duì)應(yīng)。

一個(gè)簡(jiǎn)單的字符串,用起來(lái)確實(shí)簡(jiǎn)單,背后付出了多少工程師的心血,在此,深深地佩服詹爺。

博客已遷移,歡迎關(guān)注 最新博客