Java字符串連接原理

本文主要參考黑馬程序員的Java面試寶典上的內容

我們都知道,在Java中字符串可以用+連接,也可以使用StringBuilder或StringBuffer連接。

String str = "abc"+"xyz";

那麼這幾種方式由什麼區別呢。當然你可能會知道以下幾點

  • String是隻讀字符串,String引用的字符串內容是不能被改變的
  • StringBuffer/StringBuilder 表示的字符串對象可以直接進行修改
  • StringBuffer是線程安全的,他的方法都被synchronized修飾過,StringBuilder 是線程不安全的,通常效率要比StringBuffer要高一點

但是現在需要對String的+進行深層次的探索。

下面一段代碼

public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "xxx" + s1 + "zzz" +1;
        System.out.println(s2);
    }
}

從表面上看,對字符串和整型使用"+"號並沒有什麼區別,但實際上看看這段代碼的本質,你就會發現其中奧祕。

使用反編譯工具jad對代碼進行反編譯, 這裏分享一個jad百度雲下載地址https://pan.baidu.com/s/1_QDofa8t5thVAiPc5C5mzg 提取碼: 9fn3

jad -o -a -s java Main.class

其中 -o覆蓋輸出文件無需確認,-a生成JVM指令作爲註釋,-s 輸出文件後綴名(默認是.jad)。這裏生成JVM指令作爲參考。

import java.io.PrintStream;

public class Main
{

    public Main()
    {
    //    0    0:aload_0         
    //    1    1:invokespecial   #1   <Method void Object()>
    //    2    4:return          
    }

    public static void main(String args[])
    {
        String s1 = "abc";
    //    0    0:ldc1            #2   <String "abc">
    //    1    2:astore_1        
        String s2 = (new StringBuilder()).append("xxx").append(s1).append("zzz").append(1).toString();
    //    2    3:new             #3   <Class StringBuilder>
    //    3    6:dup             
    //    4    7:invokespecial   #4   <Method void StringBuilder()>
    //    5   10:ldc1            #5   <String "xxx">
    //    6   12:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //    7   15:aload_1         
    //    8   16:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //    9   19:ldc1            #7   <String "zzz">
    //   10   21:invokevirtual   #6   <Method StringBuilder StringBuilder.append(String)>
    //   11   24:iconst_1        
    //   12   25:invokevirtual   #8   <Method StringBuilder StringBuilder.append(int)>
    //   13   28:invokevirtual   #9   <Method String StringBuilder.toString()>
    //   14   31:astore_2        
        System.out.println(s2);
    //   15   32:getstatic       #10  <Field PrintStream System.out>
    //   16   35:aload_2         
    //   17   36:invokevirtual   #11  <Method void PrintStream.println(String)>
    //   18   39:return          
    }
}

從反編譯的代碼中可以看出,String的+拼接,實際上是StringBuilder拼接然後轉爲String的,因此,我們可以得出結論,在 Java 中無論使用何種方式進行字符串連接,實際上都使用的是 StringBuilder ,那這樣是不是就表示String的+拼接和StringBuilder的效果是一樣的呢?從運行結果上看,兩者是等效的。但是從效率和資源消耗上看,兩者區別很大。當使用簡單字符串相加使,沒有太大區別,但是在循環字符串中,這兩者的差距就很大。

看下面一段代碼,在循環中使用+拼接字符串

public class Main2 {
    public static void main(String[] args) {
        String s = "";
        for (int i = 0; i < 10; i++) {
            s = s + i + " ";
        }
        System.out.println(s);
    }
}

使用jad.exe -o -s java .\Main2.class反編譯,這裏不生成JVM指令.

import java.io.PrintStream;

public class Main2
{

    public Main2()
    {
    }

    public static void main(String args[])
    {
        String s = "";
        for(int i = 0; i < 10; i++)
            s = (new StringBuilder()).append(s).append(i).append(" ").toString();

        System.out.println(s);
    }
}

可以看出在循環中每次都創建了一個新的StringBuilder對象,佔用大量資源。對此我們將其改進一下,使用StringBuilder連接

public class Main3 {
    public static void main(String[] args) {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            s.append(i);
            s.append(" ");
        }
        System.out.println(s);
    }
}

生成的字節碼

import java.io.PrintStream;

public class Main3
{

    public Main3()
    {
    }

    public static void main(String args[])
    {
        StringBuilder s = new StringBuilder();
        for(int i = 0; i < 10; i++)
        {
            s.append(i);
            s.append(" ");
        }

        System.out.println(s);
    }
}

可以看出源碼和字節碼沒有區別,也沒有生成額外的對象。

但是要注意,使用StringBuilder拼接字符串時,不要和+混用,否則還會生成更多對象

例如

public class Main4 {
    public static void main(String[] args) {
        StringBuilder s = new StringBuilder();
        for (int i = 0; i < 10; i++) {
            s.append(i + " ");
        }
        System.out.println(s);
    }
}

反編譯源碼

import java.io.PrintStream;

public class Main4
{

    public Main4()
    {
    }

    public static void main(String args[])
    {
        StringBuilder s = new StringBuilder();
        for(int i = 0; i < 10; i++)
            s.append((new StringBuilder()).append(i).append(" ").toString());

        System.out.println(s);
    }
}

可以看出Java會將+連接的字符串通過StringBuilder對象連接,這樣還是生成了不必要的對象,造成資源浪費,在IDEA中,如果是使用這種方式拼接字符串,它會提出警告.

通過以上示例,我們明白String通過+拼接字符串的本質,拼接字符串時儘量使用StringBuilder,並且二者不要混用。

以下是一個判斷字符串相等的示例,讓我們從反編譯的角度看原因

public class StringEqualTest {
    public static void main(String[] args) {
        String s1 = "Programming";
        String s2 = new String("Programming");
        String s3 = "Program";
        String s4 = "ming";
        String s5 = "Program" + "ming";
        String s6 = s3 + s4;
        System.out.println(s1 == s2);           //false
        System.out.println(s1 == s5);           //true
        System.out.println(s1 == s6);           //false
        System.out.println(s2 == s6);           //false
        System.out.println(s1 == s2.intern());  //true
        System.out.println(s1 == s6.intern());  //true
        System.out.println(s2 == s2.intern());  //false
    }
}

我們都知道Java中有個常量池,並且符合以下條件

  • 由操作符 new 調用的 String的構造器產生的對象,如 String s = new String(“1”), JVM 會先使用常量池來管理 ,再調用String 類的構造器來創建一個新的 String 對象,新創建的 String 對象被保存在堆內存中 。
  • 字符串常量初始化的對象 (包括在編譯時就可以計算出來的字符串值) , 如, String s =“1”; 存到常量池中。
  • 堆中字符串相加的表達式,如:String s3 = new String(“1”) + new String(“1”);, 其結果 s3 仍存到堆中。
  • 字符串相加的表達式中,若包含字符串常量的算子,其結果仍存到常量池中。
  • String對象的intern()方法會得到字符串對象在常量池中對應的版本的引用,如果常量池中沒有對應的字符串,則該字符串將被添加到常量池中,然後返回常量池中字符串的引用;

對代碼進行反編譯

import java.io.PrintStream;

public class StringEqualTest
{

    public StringEqualTest()
    {
    }

    public static void main(String args[])
    {
        String s1 = "Programming";
        String s2 = new String("Programming");
        String s3 = "Program";
        String s4 = "ming";
        String s5 = "Programming";
        String s6 = (new StringBuilder()).append(s3).append(s4).toString();
        System.out.println(s1 == s2);
        System.out.println(s1 == s5);
        System.out.println(s1 == s6);
        System.out.println(s2 == s6);
        System.out.println(s1 == s2.intern());
        System.out.println(s1 == s6.intern());
        System.out.println(s2 == s2.intern());
    }
}
  • s1和s2一個在常量池,一個在堆中,所以是false
  • s5由兩個常量池中數據相加,反編譯後可以看到,值已經運算出來了,其結果仍保存在常量池中,所以s1==s5
  • s6是兩個字符串相加,通過StringBuilder的append連接而成,所以s1肯定不等於s6
  • s2和s6屬於兩個對象,肯定不相等
  • s2退回常量池後,返回常量池的引用,所以和s1相等
  • s6退回常量池後,返回常量池的引用,所以和s1相等
  • 一個在堆中,一個在常量池中,不相等

在未了解String的+運算符實質之前,對於s1==s6的判斷,我也是稍有疑惑,但是在反編譯瞭解其運行原理後就可以很清楚的瞭解s5,s6的區別。

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