前言
基于字元串String在java中的地位,關于String的常識性知識就不多做介紹了,我們先來看一段代碼
public class Test {
public static void main(String[] args) {
String a = "abc";
String b = "abc";
String c = new String("abc");
System.out.println(a==b);
System.out.println(a.equals(b));
System.out.println(a==c);
System.out.println(a.equals(c));
}
}
那麼上段代碼的結果是什麼呢?答案是:true true false true,有初學java的朋友肯定會納悶,a==c為什麼會是false呢?equals判斷的為什麼都是true呢?
根據這些問題,我們就通過對String的解讀來一步一步的了解。
為什麼a==c的結果是false
明白這個問題需要對JVM的記憶體結構有一定的了解,說是了解也不需要太多,能夠get到下圖的知識點就行了。
ps:本文中所有的圖示均是為了友善了解,畫出來的大緻樣子,如果想要了解的更加清楚,請自行研究虛拟機原理。

java文法設計的時候針對String,提供了兩種建立方式和一種特殊的存儲機制(String intern pool )。
兩種建立字元串對象的方式:
- 字面值的方式指派
- new關鍵字建立一個字元串對象
這兩種方法在性能和記憶體占用方面存在這差異
String Pool串池:是在記憶體堆中專門劃分一塊空間,用來儲存所有String對象資料,當構造一個新字元串String對象時(通過字面量指派的方法),Java編譯機制會優先在這個池子裡查找是否已經存在能滿足需要的String對象,如果有的話就直接傳回該對象的位址引用(沒有的話就正常的構造一個新對象,丢進去存起來),這樣下次再使用同一個String的時候,就可以直接從串池中取,不需要再次建立對象,也就避免了很多不必要的空間開銷。
根據以上的概念,我們再來看前言中的代碼,當JVM執行到
String a = "abc";
的時候,會先看常量池裡有沒有字元串剛好是“abc”這個對象,如果沒有,在常量池裡建立初始化該對象,并把引用指向它,如下圖。
當執行到
String b = "abc";
時,發現常量池已經有了abc這個值,于是不再在常量池中建立這個對象,而是把引用直接指向了該對象,如下圖:
繼續執行到
String c = new String("abc");
這時候我們加了一個new關鍵字,這個關鍵字呢就是告訴JVM,你直接在堆記憶體裡給我開辟一塊新的記憶體,如下圖所示:
這時候我們執行四個列印語句,我們需要知道==比較的是位址,equals比較的是内容(String中的重寫過了),abc三個變量的内容完全一樣,是以equals的結果都是true,ab是一個同一個對象,是以位址一樣,a和c很顯然不是同一個對象,那麼此時為false也是很好了解的。
String相關源碼
在本文中隻有String的部分源碼,畢竟String的源碼有3000多行,全部來寫進來不那麼現實,我們挑一些比較有意思的代碼來做一定的分析說明。
屬性
我們先來看一下String都有哪些成員變量,比較關鍵的屬性有兩個,如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
char數組
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
從源碼中我們能夠看到,在String類中聲明了一個char[]數組,變量名value,聲明了一個int類型的變量hash(該String對象的哈希值的緩存)。也就是說java中的String類其實就是對char數組的封裝。
構造方法
接下來我們通過一句代碼來了解一下字元串建立的過程,
String c = new String("abc");
我們知道使用new關鍵字就會使用到構造方法,是以如下。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
構造方法中的代碼非常簡單,把傳進來的字元串的value值,也就是char數組指派給目前對象,hash同樣處理,那麼問題來了WTF original?
在這裡需要注意的是java中的一個機制,在Java中,當值被雙引号引起來(如本示例中的"abc"),JVM會去先檢檢視一看常量池裡有沒有abc這個對象,如果沒有,把abc初始化為對象放入常量池,如果有,直接傳回常量池内容。是以也就是說在沒有“abc”的基礎上,執行代碼會在串池中建立一個abc,也會在堆記憶體中再new出來一個。最終的結果如下圖:
那麼這時候如果再有一個
String c2 = new String("abc");
呢?如圖
關于這一點我們通過IDEA的debug功能也能夠看到,你會發現,c和c2其中的char數組的位址是相同的。足以說明在建立c和c2的時候使用的是同一個數組。
equals方法
public boolean equals(Object anObject) {
//如果兩個對象是同一個引用,那麼直接傳回true
if (this == anObject) {
return true;
}
/*
1.判斷傳入的對象是不是String類型
2.判斷兩個對象的char數組長度是否一緻
3.循環判斷char數組中的每一個值是否相等
以上條件均滿足才會傳回true
*/
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
為什麼String不可變?
串池需要
為什麼說是串池需要呢?在開篇的時候我們提到過,串池中的字元串會被多個變量引用,這樣的機制讓字元串對象得到了複用,避免了很多不必要的記憶體消耗。
那麼大家試想一下,如果String對象本身允許二次修改的話,我有一個字元串“abc”同時被100個變量引用,其中一個引用修改了String對象,那麼将會影響到其他99個引用該對象的變量,這樣會對其他變量造成不可控的影響。
不可變性的優點
安全性
字元串不可變安全性的考慮處于兩個方面,資料安全和線程安全。
資料安全,大家可以回憶一下,我們都在哪些地方大量的使用了字元串?網絡資料傳輸,檔案IO等,也就是說當我們在傳參的時候,使用不可變類不需要去考慮誰可能會修改其内部的值,如果使用可變類的話,可能需要每次記得重新拷貝出裡面的值,性能會有一定的損失。
線程安全,因為字元串是不可變的,是以是多線程安全的,同一個字元串執行個體可以被多個線程共享,這樣便不用因為線程安全問題而使用同步。
性能效率
關于性能效率一方面是複用,另一方面呢需要從hash值的緩存方向來說起了。
String的Hash值在很多的地方都會被使用到,如果保證了String的不可變性,也就能夠保證Hash值始終也是不可變的,這樣就不需要在每次使用的時候重新計算hash值了。
String不可變性是如何實作的?
通過對屬性私有化,final修飾,同時沒有提供公開的get set方法以及其他的能夠修改屬性的方法,保證了在建立之後不會被從外部修改。
同時不能忘了,String也是被final修飾的,在之前的文章中我們提到過,final修飾類的結果是String類沒有子類。
那麼String真的不能改變嗎?不是,通過反射我們可以,代碼如下:
String c = new String("abc");
System.out.println(c);
//擷取String類中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改變value屬性的通路權限
valueFieldOfString.setAccessible(true);
//擷取s對象上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(c);
//改變value所引用的數組中的第5個字元
value[1] = '_';
System.out.println(c);
執行的結果是
abc
a_c
也就是說我們改變了字元串對象的值,有什麼意義呢?沒什麼意義,我們從來不會這麼做。
其他問題
不是特别需要請不要使用new關鍵字建立字元串
從前文我們知道使用new關鍵字建立String的時候,即便串池中存在相同String,仍然會再次在堆記憶體中建立對象,會浪費記憶體,另一方面對象的建立相較于從串池中取效率也更低下。
String StringBuffer StringBuilder的差別
關于三者的差別,在面試題中經常的出現,String對象不可變,是以在進行任何内容上的修改時都會建立新的字元串對象,一旦修改操作太多就會造成大量的資源浪費。
StringBuffer和StringBuilder在進行字元串拼接的時候不會建立新的對象,而是在原對象上修改,不同之處在于StringBuffer線程安全,StringBuilder線程不安全。是以在進行字元串拼接的時候推薦使用StringBuffer或者StringBuilder。
我不能保證每一個地方都是對的,但是可以保證每一句話,每一行代碼都是經過推敲和斟酌的。希望每一篇文章背後都是自己追求純粹技術人生的态度。
永遠相信美好的事情即将發生。