漫畫:老闆扣了我1000,因為我沒記住阿里巴巴開發手冊的這條規則。

Java面試那些事兒發表於2019-01-17

本文故事構思來源於脈脈上的一篇帖子“一行程式碼引發的血案”。

其實關於字串的文章,我之前也寫過一篇《詭異的字串問題》,字串對於我們開發者而言,可以用最近很流行的一句話“用起來好嗨喲,彷彿人生達到了巔峰”。

確實大家都用的很嗨,很便利,但 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」,免費領學習資料。 原文地址


相關文章