本文主要參考黑馬程序員的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的區別。