一、區別
String類是不可變類,每次對String的改變都會創建一個新的對象;StringBuffer和StringBuilder都是可變類,當對它們進行改變時不會創建新的對象,它們倆的區別就在於StringBuffer是線程安全的,而StringBuilder是線程不安全的,因此在多線程中應該使用StringBuffer,而在單線程中則推薦使用StringBuilder,因為它的效率會更高,下面看一下對它們效率的測試。
/** * @author 一池春水傾半城 * @date 2019/9/27 */ public class StringDemo1 { public static String BASEINFO = "Mr.Y"; public static final int COUNT = 2000000; /** * 執行一項String賦值測試 */ public static void doStringTest() { String str = new String(BASEINFO); long starttime = System.currentTimeMillis(); for (int i = 0; i < COUNT / 100; i++) { str = str + "miss"; } long endtime = System.currentTimeMillis(); System.out.println((endtime - starttime) + " millis has costed when used String."); } /** * 執行一項StringBuffer賦值測試 */ public static void doStringBufferTest() { StringBuffer sb = new StringBuffer(BASEINFO); long starttime = System.currentTimeMillis(); for (int i = 0; i < COUNT; i++) { sb = sb.append("miss"); } long endtime = System.currentTimeMillis(); System.out.println((endtime - starttime) + " millis has costed when used StringBuffer."); } /** * 執行一項StringBuilder賦值測試 */ public static void doStringBuilderTest() { StringBuilder sb = new StringBuilder(BASEINFO); long starttime = System.currentTimeMillis(); for (int i = 0; i < COUNT; i++) { sb = sb.append("miss"); } long endtime = System.currentTimeMillis(); System.out.println((endtime - starttime) + " millis has costed when used StringBuilder."); } /** * 測試StringBuffer遍歷賦值結果 * * @param mlist */ public static void doStringBufferListTest(List<String> mlist) { StringBuffer sb = new StringBuffer(); long starttime = System.currentTimeMillis(); for (String string : mlist) { sb.append(string); } long endtime = System.currentTimeMillis(); System.out.println("buffer cost:" + (endtime - starttime) + " millis"); } /** * 測試StringBuilder迭代賦值結果 * * @param mlist */ public static void doStringBuilderListTest(List<String> mlist) { StringBuilder sb = new StringBuilder(); long starttime = System.currentTimeMillis(); for (Iterator<String> iterator = mlist.iterator(); iterator.hasNext();) { sb.append(iterator.next()); } long endtime = System.currentTimeMillis(); System.out.println("builder cost:" + (endtime - starttime) + " millis"); } public static void main(String[] args) { doStringTest(); doStringBufferTest(); doStringBuilderTest(); List<String> list = new ArrayList<String>(); for (int i = 0; i < 10000000; i++) { list.add("aaaa"); } doStringBufferListTest(list); doStringBuilderListTest(list); } }
運行結果
1015 millis has costed when used String.
60 millis has costed when used StringBuffer.
20 millis has costed when used StringBuilder.
buffer cost:307 millis
builder cost:120 millis
可以看到,不考慮多線程,對String對象進行改變耗費的時間遠大於另外兩個,而使用StringBuilder耗費的時間也很明顯的小於StringBuffer。因此,如果在
單線程下遇到需要
頻繁改變字符串
的情況時,應該優先使用StringBuilder;而如果要
保證線程安全時,則應該使用StringBuffer。
二、String創建對象過程
看到了效率對比之后,可能會有疑問,為什么使用String和StringBuffer/StringBuiler進行對象的改變之間的差別會如此明顯?其實,這主要是由於它們在創建對象時遵循的機制造成的,下面就來分析一下。
先來舉幾個栗子:
String s = "abc";
對象創建過程分析:在class文件被JVM裝載到內存中,JVM會創建一塊String Pool(字符串常量池)。當執行String s = "abc"時,JVM會首先在String Pool中查看是否已經存在字符串對象“abc”,如果已存在,則不用創建新的對象,直接將s指向String Pool中已存在的對象"abc"。如果不存在,則先在String Pool中創建一個新的字符串"abc",然后將s指向它。
String s = new String("abc");
創建過程分析:當執行String s = new String("abc")時,JVM會首先在String Pool中查看是否已經存在字符串對象“abc”,如果已存在,則不用在String Pool中創建新的對象,直接執行new String("abc")構造方法,在堆里創建一個字符串對象"abc",然后將引用s指向它。如果在String Pool中不存在該對象,則先在String Pool中創建一個新的字符串"abc",然后再進行new String("abc")等操作。
知道了String的創建過程后,來讓我們看幾段代碼:
public class TestString{ public static void main(String args[]){ String s1 = new String("abc");//語句1 String s2 = "abc";//語句2 String s3 = new String("abc");//語句3 System.out.println(s1 == s2);//語句4 System.out.println(s1 == s3);//語句5 System.out.println(s2 == s3);//語句6 System.out.println(s1 == s1.intern());//語句7 System.out.println(s2 == s2.intern());//語句8 System.out.println(s1.intern() == s2.intern());//語句9 String hello = "hello";//語句10 String hel = "hel";//語句11 String lo = "lo";//語句12 System.out.println(hello == "hello");//語句13 System.out.println(hello == "hel" + "lo");//語句14 System.out.println(hello == "hel" + lo);//語句15 System.out.println(hello == hel + lo);//語句16 } }
問題1:當執行完語句(1)時,在內存里面生成幾個對象?它們是什么?在什么地方?
--->當執行完語句(1)時,在內存里面創建了兩個對象,它們的內容分別都是abc,分別在String Pool(常量池)和Heap(堆)里。
其字符串的創建過程如下:首先在String Pool里面查找查找是否有 "abc",如果有就直接使用,但這是本程序的第一條語句,故不存在一個對象"abc",所以要在String Pool中生成一個對象"abc",接下來,執行new String("abc")構造方法,new出來的對象都放在Heap里面。在Heap里又創建了一個"abc"的對象。這時內存里就有兩個對象了,一個在String Pool 里面,一個在Heap里面。
問題2:當執行完語句(2)時,在內存里面一共有幾個對象?它們是什么?在什么地方?
當執行完語句(2)時,在內存里面一個對象也沒有創建。當我們定義語句(2)的時候,如果我們用字符串的常量值(字面值)給s2賦值的話,那么首先JVM還是從String Pool里面去查找有沒有內容為abc的這樣一個對象存在,我們發現當我們執行完語句(1)的時候,StringPool里面已經存在了內容為abc的對象,那么就不會再在String Pool里面去生成內容為abc的字符串對象了。而是會使用已經存在String Pool里面的內容為abc的字符串對象,並且會將s2這個引用指向String Pool里面的內容為abc的字符串對象,s2存放的是String Pool里面的內容為abc的字符串對像的地址。也就是說當你使用String s2 = "abc",即使用字符串常量("abc")給定義的引用(str2)賦值的話,那么它首先是在String Pool里面去找有沒有內容為abc的字符串對象存在,如果有的話,就不用創建新的對象,直接引用String Pool里面已經存在的對象;如果沒有的話,就在 String Pool里面去創建一個新的對象,接着將引用指向這個新創建的對象。所以,當執行完語句(2)時內存里面一共有2個對象,它們的內容分別都是abc,在String Pool里面一個內容abc的對象,在Heap里面有一個內容為abc的對象。
問題3:當執行完語句(3)時,在內存里面一共有幾個對象?它們是什么?在什么地方?
當執行完語句(3)時,其執行過程是這樣的:它首先在String Pool里面去查找有沒有內容為abc的字符串對象存在,發現有這個對象存在,它就不去創建 一個新的對象。接着執行new...,只要在java里面有關鍵字new存在,不管內容是否相同,都表示它將生成一個新的對象,new多少次,就生成多少個對象,而且新生成的對象都是在Heap里面,所以它會在Heap里面生成一個內容為abc的對象,並且將它的地址賦給了引用s3,s3就指向剛在Heap里面生成的內容為abc的對象。所以,當執行完語句(3)時,內存里面一共有3個對象,其中包含了在String Pool里面一個內容為abc的字符串對象和在Heap里面包含了兩個內容為abc的字符串對象。
問題4:當執行完語句(4)(5)(6)后,它們的結果分別是什么?
在java里面,對象用"=="永遠比較的是兩個對象的內存地址,換句話說,是比較"=="左右兩邊的兩個引用是否指向同一個對象。對於java里面的8種原生數據類型來說,"=="比較的是它們的字面值是不是一樣的;對應用類型來說,比較的是它們的內存地址是不是一樣的。在語句(1)(2)(3)中,由於s1、s2、s3指向不同的對象,它們的內存地址就不一樣,因此可以說當執行完語句(4)(5)(6),它們返回的結果都是false。
問題5:當執行完語句(7)(8)(9)后,它們的結果分別是什么?
首先,s1這個對象指向的是堆中第一次new...生成的對象,當調用 intern 方法時,如果String Pool已經包含一個等於此 String 對象的字符串(該對象由equals(Object)方法確定),則返回指向String Pool中的字符串對象的引用。因為String Pool中有內容為abc的對象,所以s1.intern()返回的是String Pool中的內容為abc的字符串對象的內存地址,而s1卻是指向Heap上內容為abc的字符串對象的引用。因而,兩個引用指向的對象不同,所以,s1 == s1.intern() 為false,即語句(7)結果為false。
對於s2.intern(),它還是會首先檢查String Pool中是否有內容為abc的對象,發現有,則將String Pool中內容為abc的對象的地址賦給s2.intern()方法的返回值。因為s2和s2.intern()方法的返回值指向的是同一個對象,所以,s2 == s2.intern()的結果為true,,即語句(8)結果為true。
對於s1.intern(),它首先檢查String Pool中是否有內容為abc的對象,發現有,則將String Pool中內容為abc的對象的賦給s1.intern()方法的返回值。對於s2.intern(),首先檢查String Pool中是否有內容為abc的對象,發現有,則將String Pool中內容為abc的對象的地址賦給s2.intern()方法的返回值。因為兩者返回的地址都指向同一個對象,所以,s1.intern() == s2.intern()的結果為true,,即是語句(9)結果為true。
因此,當執行完語句(7)(8)(9)后,它們的結果分別是false、true、true。
問題6:當執行完語句(13)(14) (15)(16)后,它們的結果分別是什么?
hello == "hello"引用hello指向的對象就是String Pool中的“hello”,即語句(13)的結果為true。
hello == "hel" + "lo"當加號兩邊都是常量值時,就會組成一個新的常量值"hello"在String Pool里面,如果String Pool已經有相同內容的就不會再創建,則直接返回String Pool里面的內容為"hello"的字符串對象的內存地址,所以,hello == "hel" + "lo"結果為true。
hello =="hel" + lo 當加號兩邊有一個不是常量值,會在堆里面創建一個新的"hello"對象,一個在String Pool中,一個在Heap中,故輸出false 。
hel + lo 同上,輸出false。
因此,當執行完語句(7)(8)(9)后,它們的結果分別是true、true、false、false。
三、從intern()方法看String字符串進入常量池的時機
在String類中有這么一個方法intern(),我們先來解釋下這個方法,在調用"ab".intern()方法的時候會返回"ab",但是這個方法會首先檢查字符串池中是否有"ab"這個字符串,如果存在則返回這個字符串的引用,否則就將這個字符串添加到字符串池中,然會返回這個字符串的引用。是不是有點暈?沒事,來一段代碼解釋下。
public static void main(String[] args) { String s1=new String("he")+new String("llo"); // 1 s1.intern(); // 2 String s2="hello"; // 3 System.out.println(s1==s2); // 4 }
我們一行行來看。執行第1行代碼時,首先會在堆中創建"he"和"llo"的對象,然后再將它們引用保存到字符串常量池中,然后有個+號對吧,內部是創建了一個StringBuilder對象,一路append,最后調用StringBuilder對象的toString方法得到一個String對象(內容是hello,注意這個toString方法會new一個String對象),並把它賦值給s1,因此s1表示堆中對象"hello"的引用。注意啊,沒有把hello的引用放入字符串常量池。
然后來到第2行,執行s1.intern(),jvm首先會到字符串常量池中尋找字符串"hello",發現並不存在,這時jvm會將s1對象的引用保存到字符串常量池中,然后返回這個引用,但這個引用沒有被接收,所以沒有用。
到了第3行,這時字符串常量池中已經有"hello"了,直接用。
第4行,s1表示在堆中的對象"hello"的引用,而s2拿到的"hello"則是堆中"hello"對象在字符串常量池中的一個引用,所以它們指向了同一個對象,所以返回為true。
String s1=new String("he")+new String("llo"); // 1. 新建一個引用s1指向堆中的對象s1,值為"hello" String s2=new String("h")+new String("ello"); // 2. 新建一個引用s2指向堆中的對象s2,值為"hello" String s3=s1.intern(); // 3. 執行s1.intern()會在字符串常量池中新建一個引用"hello",該引用指向s1在堆中的地址,並新建一個引用s3指向字符串常量池中的"hello" String s4=s2.intern(); // 4. 執行s2.intern()不會在字符串常量池中創建新的引用,因為"hello"已存在,因此只執行了新建一個引用s4指向字符串常量池中"hello"的操作 System.out.println(s1==s3); // s3和s4指向的都是字符串常量池中的"hello",而這個"hello"都指向堆中s1的地址,因此下面兩句代碼都為true System.out.println(s1==s4); System.out.println(s2 == s3);// s3和s4最終關聯堆中的地址是對象s1,因此下面兩句為false System.out.println(s2 == s4);
再來看上面這幾行代碼。第1行首先在堆中創建"he"和"llo"的對象,並將它們的引用放入字符串常量池,然后在堆中創建一個"hello"對象,
沒有放到字符串常量池,s1指向這個"hello"對象。
第2行在堆中創建"h"和"ello"對象,並放入字符串常量池,然后在堆中創建一個"hello"對象,
沒有放到字符串常量池,s2指向這個"hello"對象。
第3行,字符串常量池里沒有"hello",因此會把s1指向的堆中的"hello"對象的引用放入字符串常量池(也就是說字符串常量池中的引用和s1指向了同一個對象),然后把這個引用返回給了s3,所以呢執行s3 == s1為true;
第4行,字符串常量池里已經有"hello"了,因此直接將它返回給了s4,所以s4 == s1也為true。
至於s2 == s3和s2 == s4為false則很明顯了吧,s3和s4指向的字符串常量池中的引用和s1指向的對象是同一個,而s2則指向了另一個對象,因此返回false。
看到這兒,有些小伙伴可能還會有些疑問,按照上面的說法,下面這段代碼應該返回true才對啊---先在堆中創建一個"hello"對象,將它的引用放到字符串常量池中並賦給s,s.intern()返回的是字符串常量池中的引用,s.intern()和s最終指向堆中的對象相同。但程序仍然返回false,為什么呢?下面就來解釋一下。
String s = new Sting("hello"); System.out.println(s.intern()==s); // false
第一行代碼會在堆中創建兩個對象(記為一號本體a1和二號本體a2),字符串常量池中存的是a1的引用,二號本體a2在字符串常量池無引用,s指向的是二號本體a2。然后s.intern()返回的是一號本體a1與二號本體肯定不同,所以返回false。
再看下面這段代碼
String s1=new String("he")+new String("llo"); System.out.println(s1.intern()==s1);
這段代碼不會像上面那段一樣在堆中產生兩個本體,執行第一段代碼在堆中創建了三個對象,產生了三個引用。s1指向堆中的"hello",常量池中放了"he"和"llo"的引用,並沒有放"hello"的引用。堆中的每個對象都只有一個外部引用,所以堆中僅存在一個本體。執行到s1.intern()時,會把s1的引用保存到常量池中,因此s1.intern()返回的引用與s1指向同一個地址,因此為true。

參考:
Java 中new String("字面量") 中 "字面量" 是何時進入字符串常量池的? - 木女孩的回答 - 知乎 https://www.zhihu.com/question/55994121/answer/147296098
https://www.huberylee.com/2017/02/13/Java%E4%B8%ADString%E5%AF%B9%E8%B1%A1%E5%88%9B%E5%BB%BA%E8%BF%87%E7%A8%8B%E6%8E%A2%E7%A9%B6/