Java中關(guān)于String的全面解析
前言
基于字符串String在java中的地位,關(guān)于String的常識(shí)性知識(shí)就不多做介紹了,我們先來(lái)看一段代碼
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));
}
}
那么上段代碼的結(jié)果是什么呢?答案是:true true false true,有初學(xué)java的朋友肯定會(huì)納悶,a==c為什么會(huì)是false呢?equals判斷的為什么都是true呢?
根據(jù)這些問(wèn)題,我們就通過(guò)對(duì)String的解讀來(lái)一步一步的了解。
為什么a==c的結(jié)果是false
明白這個(gè)問(wèn)題需要對(duì)JVM的內(nèi)存結(jié)構(gòu)有一定的了解,說(shuō)是了解也不需要太多,能夠get到下圖的知識(shí)點(diǎn)就行了。
ps:本文中所有的圖示均是為了方便理解,畫出來(lái)的大致樣子,如果想要了解的更加清楚,請(qǐng)自行研究虛擬機(jī)原理。

java語(yǔ)法設(shè)計(jì)的時(shí)候針對(duì)String,提供了兩種創(chuàng)建方式和一種特殊的存儲(chǔ)機(jī)制(String intern pool )。
兩種創(chuàng)建字符串對(duì)象的方式:
1.字面值的方式賦值
2.new關(guān)鍵字新建一個(gè)字符串對(duì)象
這兩種方法在性能和內(nèi)存占用方面存在這差異
String Pool串池:是在內(nèi)存堆中專門劃分一塊空間,用來(lái)保存所有String對(duì)象數(shù)據(jù),當(dāng)構(gòu)造一個(gè)新字符串String對(duì)象時(shí)(通過(guò)字面量賦值的方法),Java編譯機(jī)制會(huì)優(yōu)先在這個(gè)池子里查找是否已經(jīng)存在能滿足需要的String對(duì)象,如果有的話就直接返回該對(duì)象的地址引用(沒(méi)有的話就正常的構(gòu)造一個(gè)新對(duì)象,丟進(jìn)去存起來(lái)),這樣下次再使用同一個(gè)String的時(shí)候,就可以直接從串池中取,不需要再次創(chuàng)建對(duì)象,也就避免了很多不必要的空間開(kāi)銷。
根據(jù)以上的概念,我們?cè)賮?lái)看前言中的代碼,當(dāng)JVM執(zhí)行到String a = "abc";的時(shí)候,會(huì)先看常量池里有沒(méi)有字符串剛好是“abc”這個(gè)對(duì)象,如果沒(méi)有,在常量池里創(chuàng)建初始化該對(duì)象,并把引用指向它,如下圖。

當(dāng)執(zhí)行到String b = "abc";時(shí),發(fā)現(xiàn)常量池已經(jīng)有了abc這個(gè)值,于是不再在常量池中創(chuàng)建這個(gè)對(duì)象,而是把引用直接指向了該對(duì)象,如下圖:

繼續(xù)執(zhí)行到 String c = new String("abc");這時(shí)候我們加了一個(gè)new關(guān)鍵字,這個(gè)關(guān)鍵字呢就是告訴JVM,你直接在堆內(nèi)存里給我開(kāi)辟一塊新的內(nèi)存,如下圖所示:

這時(shí)候我們執(zhí)行四個(gè)打印語(yǔ)句,我們需要知道==比較的是地址,equals比較的是內(nèi)容(String中的重寫過(guò)了),abc三個(gè)變量的內(nèi)容完全一樣,因此equals的結(jié)果都是true,ab是一個(gè)同一個(gè)對(duì)象,因此地址一樣,a和c很顯然不是同一個(gè)對(duì)象,那么此時(shí)為false也是很好理解的。
String相關(guān)源碼
在本文中只有String的部分源碼,畢竟String的源碼有3000多行,全部來(lái)寫進(jìn)來(lái)不那么現(xiàn)實(shí),我們挑一些比較有意思的代碼來(lái)做一定的分析說(shuō)明。
屬性
我們先來(lái)看一下String都有哪些成員變量,比較關(guān)鍵的屬性有兩個(gè),如下:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
char數(shù)組
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
從源碼中我們能夠看到,在String類中聲明了一個(gè)char[]數(shù)組,變量名value,聲明了一個(gè)int類型的變量hash(該String對(duì)象的哈希值的緩存)。也就是說(shuō)java中的String類其實(shí)就是對(duì)char數(shù)組的封裝。
構(gòu)造方法
接下來(lái)我們通過(guò)一句代碼來(lái)了解一下字符串創(chuàng)建的過(guò)程,String c = new String("abc");我們知道使用new關(guān)鍵字就會(huì)使用到構(gòu)造方法,所以如下。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
構(gòu)造方法中的代碼非常簡(jiǎn)單,把傳進(jìn)來(lái)的字符串的value值,也就是char數(shù)組賦值給當(dāng)前對(duì)象,hash同樣處理,那么問(wèn)題來(lái)了WTF original?
在這里需要注意的是java中的一個(gè)機(jī)制,在Java中,當(dāng)值被雙引號(hào)引起來(lái)(如本示例中的"abc"),JVM會(huì)去先檢查看一看常量池里有沒(méi)有abc這個(gè)對(duì)象,如果沒(méi)有,把a(bǔ)bc初始化為對(duì)象放入常量池,如果有,直接返回常量池內(nèi)容。所以也就是說(shuō)在沒(méi)有“abc”的基礎(chǔ)上,執(zhí)行代碼會(huì)在串池中創(chuàng)建一個(gè)abc,也會(huì)在堆內(nèi)存中再new出來(lái)一個(gè)。最終的結(jié)果如下圖:

那么這時(shí)候如果再有一個(gè)String c2 = new String("abc");呢?如圖

關(guān)于這一點(diǎn)我們通過(guò)IDEA的debug功能也能夠看到,你會(huì)發(fā)現(xiàn),c和c2其中的char數(shù)組的地址是相同的。足以說(shuō)明在創(chuàng)建c和c2的時(shí)候使用的是同一個(gè)數(shù)組。

equals方法
public boolean equals(Object anObject) {
//如果兩個(gè)對(duì)象是同一個(gè)引用,那么直接返回true
if (this == anObject) {
return true;
}
/*
1.判斷傳入的對(duì)象是不是String類型
2.判斷兩個(gè)對(duì)象的char數(shù)組長(zhǎng)度是否一致
3.循環(huán)判斷char數(shù)組中的每一個(gè)值是否相等
以上條件均滿足才會(huì)返回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不可變?
串池需要
為什么說(shuō)是串池需要呢?在開(kāi)篇的時(shí)候我們提到過(guò),串池中的字符串會(huì)被多個(gè)變量引用,這樣的機(jī)制讓字符串對(duì)象得到了復(fù)用,避免了很多不必要的內(nèi)存消耗。
那么大家試想一下,如果String對(duì)象本身允許二次修改的話,我有一個(gè)字符串“abc”同時(shí)被100個(gè)變量引用,其中一個(gè)引用修改了String對(duì)象,那么將會(huì)影響到其他99個(gè)引用該對(duì)象的變量,這樣會(huì)對(duì)其他變量造成不可控的影響。
不可變性的優(yōu)點(diǎn)
安全性
字符串不可變安全性的考慮處于兩個(gè)方面,數(shù)據(jù)安全和線程安全。
數(shù)據(jù)安全,大家可以回憶一下,我們都在哪些地方大量的使用了字符串?網(wǎng)絡(luò)數(shù)據(jù)傳輸,文件IO等,也就是說(shuō)當(dāng)我們?cè)趥鲄⒌臅r(shí)候,使用不可變類不需要去考慮誰(shuí)可能會(huì)修改其內(nèi)部的值,如果使用可變類的話,可能需要每次記得重新拷貝出里面的值,性能會(huì)有一定的損失。
線程安全,因?yàn)樽址遣豢勺兊?,所以是多線程安全的,同一個(gè)字符串實(shí)例可以被多個(gè)線程共享,這樣便不用因?yàn)榫€程安全問(wèn)題而使用同步。
性能效率
關(guān)于性能效率一方面是復(fù)用,另一方面呢需要從hash值的緩存方向來(lái)說(shuō)起了。
String的Hash值在很多的地方都會(huì)被使用到,如果保證了String的不可變性,也就能夠保證Hash值始終也是不可變的,這樣就不需要在每次使用的時(shí)候重新計(jì)算hash值了。
String不可變性是如何實(shí)現(xiàn)的?
通過(guò)對(duì)屬性私有化,final修飾,同時(shí)沒(méi)有提供公開(kāi)的get set方法以及其他的能夠修改屬性的方法,保證了在創(chuàng)建之后不會(huì)被從外部修改。
同時(shí)不能忘了,String也是被final修飾的,在之前的文章中我們提到過(guò),final修飾類的結(jié)果是String類沒(méi)有子類。
那么String真的不能改變嗎?不是,通過(guò)反射我們可以,代碼如下:
String c = new String("abc");
System.out.println(c);
//獲取String類中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改變value屬性的訪問(wèn)權(quán)限
valueFieldOfString.setAccessible(true);
//獲取s對(duì)象上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(c);
//改變value所引用的數(shù)組中的第5個(gè)字符
value[1] = '_';
System.out.println(c);
執(zhí)行的結(jié)果是
abc a_c
也就是說(shuō)我們改變了字符串對(duì)象的值,有什么意義呢?沒(méi)什么意義,我們從來(lái)不會(huì)這么做。
其他問(wèn)題
不是特別需要請(qǐng)不要使用new關(guān)鍵字創(chuàng)建字符串
從前文我們知道使用new關(guān)鍵字創(chuàng)建String的時(shí)候,即便串池中存在相同String,仍然會(huì)再次在堆內(nèi)存中創(chuàng)建對(duì)象,會(huì)浪費(fèi)內(nèi)存,另一方面對(duì)象的創(chuàng)建相較于從串池中取效率也更低下。
String StringBuffer StringBuilder的區(qū)別
關(guān)于三者的區(qū)別,在面試題中經(jīng)常的出現(xiàn),String對(duì)象不可變,因此在進(jìn)行任何內(nèi)容上的修改時(shí)都會(huì)創(chuàng)建新的字符串對(duì)象,一旦修改操作太多就會(huì)造成大量的資源浪費(fèi)。
StringBuffer和StringBuilder在進(jìn)行字符串拼接的時(shí)候不會(huì)創(chuàng)建新的對(duì)象,而是在原對(duì)象上修改,不同之處在于StringBuffer線程安全,StringBuilder線程不安全。所以在進(jìn)行字符串拼接的時(shí)候推薦使用StringBuffer或者StringBuilder。
相關(guān)文章
springboot實(shí)現(xiàn)異步任務(wù)
這篇文章主要為大家詳細(xì)介紹了springboot實(shí)現(xiàn)異步任務(wù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
JDK1.7以上javaFTP上傳刪除文件的實(shí)現(xiàn)方法
下面小編就為大家分享一篇JDK1.7以上javaFTP上傳刪除文件的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
BaseDao封裝JavaWeb的增刪改查的實(shí)現(xiàn)代碼
Basedao 是一種基于數(shù)據(jù)訪問(wèn)對(duì)象(Data Access Object)模式的設(shè)計(jì)方法,它是一個(gè)用于處理數(shù)據(jù)庫(kù)操作的基礎(chǔ)類,負(fù)責(zé)封裝數(shù)據(jù)庫(kù)訪問(wèn)的底層操作,提供通用的數(shù)據(jù)庫(kù)訪問(wèn)方法,本文給大家介紹了BaseDao封裝JavaWeb的增刪改查的實(shí)現(xiàn)代碼,需要的朋友可以參考下2024-03-03
java(包括springboot)讀取resources下文件方式實(shí)現(xiàn)
這篇文章主要介紹了java(包括springboot)讀取resources下文件方式實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
windows下zookeeper配置java環(huán)境變量的方法
今天小編就為大家分享一篇關(guān)于windows下zookeeper配置java環(huán)境變量的方法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-03-03
java 動(dòng)態(tài)增加定時(shí)任務(wù)示例
本篇文章主要介紹了java 動(dòng)態(tài)增加定時(shí)任務(wù)示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03
IDEA錯(cuò)誤:找不到或無(wú)法加載主類的完美解決方法
使用IDEA開(kāi)始就一直在搭建java環(huán)境,許久沒(méi)有使用過(guò)java,剛開(kāi)始有些生疏,先建了一個(gè)最簡(jiǎn)單的類可是運(yùn)行的時(shí)候出現(xiàn)錯(cuò)誤:找不到或無(wú)法加載主類,下面這篇文章主要給大家介紹了關(guān)于IDEA錯(cuò)誤:找不到或無(wú)法加載主類的完美解決方法,需要的朋友可以參考下2022-07-07
Spring?Cloud?Alibaba?Nacos服務(wù)治理平臺(tái)服務(wù)注冊(cè)、RestTemplate實(shí)現(xiàn)微服務(wù)之間訪
這篇文章主要介紹了Spring?Cloud?Alibaba:Nacos服務(wù)治理平臺(tái),服務(wù)注冊(cè)、RestTemplate實(shí)現(xiàn)微服務(wù)之間訪問(wèn),負(fù)載均衡訪問(wèn),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06

