1. 程式人生 > 其它 >深入理解 new String()

深入理解 new String()

前言

在一些面試中,我們經常會看到一些關於String的面試題,如String s1 = new String("a") + "b" 這個過程建立了幾個字串物件? 本章就詳細來講述一下不同方式建立String的過程。

示例一

String s1 = new String("Hello");

首先這裡由於使用new來建立的物件,肯定會在堆中建立1個字串物件,在Java中,所有的字串字面量會快取在字串常量池,所以每個字串建立時都會在池內檢視是否存在該字串,如果不存在則會在堆中再新建1個字串物件,然後將它的引用放入字串常量池依次推斷,這裡會創建出2個字串物件。

接下來我們通過外掛來看一下這段程式碼的位元組碼執行流程,外掛名稱: jclasslib

 0 new #2 <java/lang/String>
 3 dup
 4 ldc #3 <Hello>
 6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
 9 astore_1
10 return
  1. 0 new #2 <java/lang/String> 表示是通過new建立了一個String物件,#2 就是經過類載入解析後的直接引用,解析前是java.lang.String
  2. ldc #3 <Hello> 這段指令會做兩件事 判斷 #3是否已經被解析成直接引用,如果沒有則進行解析。然後檢測字串常量池
    中是否有Hello字元常量,如果有則直接獲取引用,反之則在堆中建立一個Hello字串物件,然後把引用放入字串常量池
  3. invokespecial #4... 這行指令會執行String的構造方法進行初始化。
  4. 9 astore_1 將建立好的字串引用掛載到區域性變量表中。

示例二

String s1 = "Hello";

這種方式,沒有通過new關鍵字,所以它的結果只會是建立0或者1個物件,如果字串常量池中不存在Hello常量物件,則進行建立並且放入。如果已經存在,則直接返回引用。

0 ldc #2 <Hello>
2 astore_1
3 return

示例三

String s1 = "He" + "llo";

話不多說,直接看位元組碼

0 ldc #2 <Hello>
2 astore_1
3 return

String s1 = "Hello";的位元組碼編譯一模一樣,所以說結果也是一模一樣,會將Hello作為一個字串整體,檢視是否已存在字元常量,無則建立。
由此得出字串通過+拼接,與直接完整建立沒有任何差異。

String s1 = "Hello";
String s2 = "He" + "llo";
String s3 = "H" + "e" + "l" + "l" + "o";
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//true

示例四

String s1 = new String("He") + "llo";

這種場景下的字串拼接就會有些不同,我們先來看一下是如何拼接的,位元組碼如下:

 0 new #2 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
 7 new #4 <java/lang/String>
10 dup
11 ldc #5 <He>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 ldc #8 <llo>
21 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
24 invokevirtual #9 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
27 astore_1
28 return

很顯然,Java在底層是通過StringBuilder來進行字串的拼接,通過append()方法,最後直接toString()返回。
那麼我們結合之前講到的,推斷一下,這種場景會建立幾個物件呢?

  1. 建立StringBuilder物件
  2. new String("He") 建立1或者2個物件,過程直接參考示例一
  3. + "llo" 建立0或1個物件,過程參考示例二和示例三
    總結: 如果字串常量池中沒有Hello,則會建立4個物件。如果已經存在常量,則建立2個物件

示例五 通過new 建立String的坑

String s1 = "1" + "1";
String s2 = "11";
System.out.println(s1 == s2);

通過以上示例二和三我們知道結果為true,因為"1" + "1"會建立一個11字串物件,s2在建立時11已經存在在字串常量池,所以直接獲取該引用。
接下來我們換一種方式

String s1 = new String("1") + new String("1");
String s2 = "11";
System.out.println(s1 == s2);

那麼這種方式的結果呢?大家可以試一下,並且思考一下,其實在示例四中已經有了答案,但這裡我需要再強調一下,因為這是一個很容易忽略卻很影響結果的要點。
結果:false
那麼為什麼呢? 原因如下:

  1. "1" + "1"; 這種會直接建立一個值為11的字串物件,並且會將該物件快取到字串常量池

  2. new String("1") + new String("1");而這種方式是通過StringBuilder進行拼接的,他是通過建立兩個1字串進行拼接,最終結果並沒有建立11這個字串物件,所以這種方式建立後,字串常量池中並沒有11字元常量存在。

再來回顧一下String s1 = "1" + "1";的位元組碼內容

0 ldc #2 <11>
2 astore_1
3 return

顯然,這種方式最終建立了11字串物件,並且會快取到字串常量池

我們來再次看一下String s1 = new String("1") + new String("1");的位元組碼檔案,來回顧一下

 0 new #2 <java/lang/StringBuilder>
 3 dup
 4 invokespecial #3 <java/lang/StringBuilder.<init> : ()V>
 7 new #4 <java/lang/String>
10 dup
11 ldc #5 <1>
13 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
16 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
19 new #4 <java/lang/String>
22 dup
23 ldc #5 <1>
25 invokespecial #6 <java/lang/String.<init> : (Ljava/lang/String;)V>
28 invokevirtual #7 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
31 invokevirtual #8 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
34 astore_1
35 return

看這段位元組碼,這段位元組碼執行了兩次,分別建立兩個String物件,值都是為1。並沒有建立11字串物件。

7 new #4 <java/lang/String>
10 dup
11 ldc #5 <1>

總結:
底層通過StringBuilder拼接的字串,最終的結果不會建立一個新的字串物件。

詳細描述:
String s1 = "1" + "1" 會直接建立一個值為11字串,然後將引用放入字串常量池
String s2 = "11" 然後我們再宣告一個值為11字串,其實它會在字串常量池中獲取s1的引用,所以它們的引用相等。

String s1 = new String("1") + new String("1") 只會建立一個值為1的字串,然後將引用放入字串常量池
String s2 = "11" 建立一個值為11的字串,由於字串常量池中不存在11常量,則會重新建立一個新的11字串物件,然後將引用放入字串常量池,所以它們的引用不相等。

類似的案例如下:

String s1 = new String("1") + new String("1");
String s2 = new String("1") + "1";
String s3 = new StringBuilder("1").append("1").toString();
String s4 = "1" + "1";
System.out.println(s1 == s4); //false
System.out.println(s2 == s4);//false
System.out.println(s3 == s4);//false

結果統統為false,因為它們底層都是由StringBuilder拼接,不會產生11字串物件。

示例六

String s1 = new String("1") + new String("1");
s1.intern();
String s2 = "11";
System.out.println(s1 == s2);

通過示例五的學習,我們知道,這裡應該會輸出false,但這裡結果為true,主要原因就是s1執行了intern()方法.
intern()作用: 主動將字串自身的引用新增到字串常量池,如果字串常量池中已存在,則直接返回已存在的引用,不存在則新增。