深入了解java中的string對象

這里來對Java中的String對象做一個稍微深入的了解肝箱。

Java對象實現(xiàn)的演進

String對象是Java中使用最頻繁的對象之一摧茴,所以Java開發(fā)者們也在不斷地對String對象的實現(xiàn)進行優(yōu)化绵载,以便提升String對象的性能。

Java6以及之前版本中String對象的屬性

在Java6以及之前版本中苛白,String對象是對char數(shù)組進行了封裝實現(xiàn)的對象娃豹,其主要有4個成員成員變量,分別是char數(shù)組购裙、偏移量offset懂版、字符數(shù)量count和哈希值hash。String對象是通過offset和count兩個屬性來定位char[]數(shù)組躏率,獲取字符串躯畴。這樣做可以高效、快速地共享數(shù)組對象禾锤,同時節(jié)省內(nèi)存空間私股,但是這種方式卻可能會導(dǎo)致內(nèi)存泄漏的發(fā)生。

Java7恩掷、8版本中String對象的屬性

從Java7版本開始倡鲸,Java對String類做了一些改變,具體是String類不再有offset和count兩個變量了黄娘。這樣做的好處是String對象占用的內(nèi)存稍微少了點峭状,同時String.substring()方法也不再共享char[]了,從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄漏問題逼争。

Java9以及之后版本中String對象的屬性

從Java9版本開始优床,Java將char[]數(shù)組改為了byte[]數(shù)組。我們都知道誓焦,char是兩個字節(jié)的胆敞,如果用來存一個字節(jié)的情況下就會造成內(nèi)存空間的浪費。而為了節(jié)約這一個字節(jié)的空間,Java開發(fā)者就改成了一個使用一個字節(jié)的byte來存儲字符串移层。

另外仍翰,在Java9中,String對象維護了一個新的屬性coder观话,這個屬性是編碼格式的標(biāo)識予借,在計算字符串長度或者調(diào)用indexOf()方法的時候,會需要根據(jù)這個字段去判斷如何計算字符串長度频蛔。coder屬性默認有0和1兩個值灵迫,其中0代表Latin-1(單字節(jié)編碼),1則表示UTF-16編碼晦溪。

String對象的創(chuàng)建方式與在內(nèi)存中的存放

在Java中瀑粥,對于基本數(shù)據(jù)類型的變量和對對象的引用,保存在棧內(nèi)存的局部變量表中尼变;而通過new關(guān)鍵字和Constructor創(chuàng)建的對象利凑,則是保存在堆內(nèi)存中。而String對象的創(chuàng)建方式一般為兩種嫌术,一種是字面量(字符串常量)的方式哀澈,一種則是構(gòu)造函數(shù)(String())的方式,兩種方式在內(nèi)存中的存放有所不同度气。

字面量(字符串常量)的創(chuàng)建方式

使用字面量的方式創(chuàng)建字符串時割按,JVM會在字符串常量池中先檢查是否存在該字面量,如果存在磷籍,則返回該字面量在內(nèi)存中的引用地址适荣;如果不存在,則在字符串常量池中創(chuàng)建該字面量并返回引用院领。使用這種方式創(chuàng)建的好處是避免了相同值的字符串在內(nèi)存中被重復(fù)創(chuàng)建弛矛,節(jié)約了內(nèi)存,同時這種寫法也會比較簡單易讀一些比然。

String str = "i like yanggb.";

字符串常量池

這里要特別說明一下常量池丈氓。常量池是JVM為了減少字符串對象的重復(fù)創(chuàng)建,特別維護了一個特殊的內(nèi)存强法,這段內(nèi)存被稱為字符串常量池或者字符串字面量池万俗。在JDK1.6以及之前的版本中,運行時常量池是在方法區(qū)中的饮怯。在JDK1.7以及之后版本的JVM闰歪,已經(jīng)將運行時常量池從方法區(qū)中移了出來,在Java堆(Heap)中開辟了一塊區(qū)域用來存放運行時常量池蓖墅。而從JDK1.8開始库倘,JVM取消了Java方法區(qū)临扮,取而代之的是位于直接內(nèi)存的元空間(MetaSpace)∮谡粒總結(jié)就是公条,目前的字符串常量池在堆中。

我們所知道的幾個String對象的特點都來源于String常量池迂曲。

1.在常量池中會共享所有的String對象,因此String對象是不可被修改的寥袭,因為一旦被修改路捧,就會導(dǎo)致所有引用此String對象的變量都隨之改變(引用改變),所以String對象是被設(shè)計為不可修改的传黄,后面會對這個不可變的特性做一個深入的了解杰扫。

2.String對象拼接字符串的性能較差的說法也是來源于此,因為String對象不可變的特性膘掰,每次修改(這里是拼接)都是返回一個新的字符串對象章姓,而不是再原有的字符串對象上做修改,因此創(chuàng)建新的String對象會消耗較多的性能(開辟另外的內(nèi)存空間)识埋。

3.因為常量池中創(chuàng)建的String對象是共享的凡伊,因此使用雙引號聲明的String對象(字面量)會直接存儲在常量池中,如果該字面量在之前已存在窒舟,則是會直接引用已存在的String對象系忙,這一點在上面已經(jīng)描述過了,這里再次提及惠豺,是為了特別說明這一做法保證了在常量池中的每個String對象都是唯一的银还,也就達到了節(jié)約內(nèi)存的目的。

構(gòu)造函數(shù)(String())的創(chuàng)建方式

使用構(gòu)造函數(shù)的方式創(chuàng)建字符串時洁墙,JVM同樣會在字符串常量池中先檢查是否存在該字面量蛹疯,只是檢查后的情況會和使用字面量創(chuàng)建的方式有所不同。如果存在热监,則會在堆中另外創(chuàng)建一個String對象捺弦,然后在這個String對象的內(nèi)部引用該字面量,最后返回該String對象在內(nèi)存地址中的引用狼纬;如果不存在羹呵,則會先在字符串常量池中創(chuàng)建該字面量,然后再在堆中創(chuàng)建一個String對象疗琉,然后再在這個String對象的內(nèi)部引用該字面量冈欢,最后返回該String對象的引用。

String str = newString("i like yanggb.");

這就意味著盈简,只要使用這種方式凑耻,構(gòu)造函數(shù)都會另行在堆內(nèi)存中開辟空間太示,創(chuàng)建一個新的String對象。具體的理解是香浩,在字符串常量池中不存在對應(yīng)的字面量的情況下类缤,new String()會創(chuàng)建兩個對象,一個放入常量池中(字面量)邻吭,一個放入堆內(nèi)存中(字符串對象)餐弱。

String對象的比較

比較兩個String對象是否相等,通常是有【==】和【equals()】兩個方法囱晴。

在基本數(shù)據(jù)類型中膏蚓,只可以使用【==】,也就是比較他們的值是否相同畸写;而對于對象(包括String)來說驮瞧,【==】比較的是地址是否相同,【equals()】才是比較他們內(nèi)容是否相同枯芬;而equals()是Object都擁有的一個函數(shù)论笔,本身就要求對內(nèi)部值進行比較。

String str = "i like yanggb.";

String str1 = newString("i like yanggb.");

System.out.println(str == str1); // falseSystem.out.println(str.equals(str1)); // true

因為使用字面量方式創(chuàng)建的String對象和使用構(gòu)造函數(shù)方式創(chuàng)建的String對象的內(nèi)存地址是不同的千所,但是其中的內(nèi)容卻是相同的狂魔,也就導(dǎo)致了上面的結(jié)果。

String對象中的intern()方法

我們都知道真慢,String對象中有很多實用的方法包各。為什么其他的方法都不說劈愚,這里要特別說明這個intern()方法呢,因為其中的這個intern()方法最為特殊。它的特殊性在于执隧,這個方法在業(yè)務(wù)場景中幾乎用不上舔涎,它的存在就是在為難程序員的秒拔,也可以說是為了幫助程序員了解JVM的內(nèi)存結(jié)構(gòu)而存在的(顽素?我信你個鬼,你個糟老頭子壞得很)烛占。

/*** When the intern method is invoked, if the pool already contains a

* string equal to this {@code String} object as determined by

* the {@link #equals(Object)} method, then the string from the pool is

* returned. Otherwise, this {@code String} object is added to the

* pool and a reference to this {@code String} object is returned.

**/publicnative String intern();

上面是源碼中的intern()方法的官方注釋說明胎挎,大概意思就是intern()方法用來返回常量池中的某字符串,如果常量池中已經(jīng)存在該字符串忆家,則直接返回常量池中該對象的引用犹菇。否則,在常量池中加入該對象芽卿,然后返回引用揭芍。然后我們可以從方法簽名上看出intern()方法是一個native方法。

下面通過幾個例子來詳細了解下intern()方法的用法卸例。

第一個例子

String str1 = newString("1");

System.out.println(str1 == str1.intern()); // falseSystem.out.println(str1 == "1"); // false

在上面的例子中称杨,intern()方法返回的是常量池中的引用肌毅,而str1保存的是堆中對象的引用,因此兩個打印語句的結(jié)果都是false姑原。

第二個例子

String str2 = newString("2") + newString("3");

System.out.println(str2 == str2.intern()); // trueSystem.out.println(str2 == "23"); // true

在上面的例子中悬而,str2保存的是堆中一個String對象的引用,這和JVM對【+】的優(yōu)化有關(guān)锭汛。實際上笨奠,在給str2賦值的第一條語句中,創(chuàng)建了3個對象唤殴,分別是在字符串常量池中創(chuàng)建的2和3艰躺、還有在堆中創(chuàng)建的字符串對象23。因為字符串常量池中不存在字符串對象23眨八,所以這里要特別注意:intern()方法在將堆中存在的字符串對象加入常量池的時候采取了一種截然不同的處理方案——不是在常量池中建立字面量,而是直接將該String對象自身的引用復(fù)制到常量池中左电,即常量池中保存的是堆中已存在的字符串對象的引用廉侧。根據(jù)前面的說法,這時候調(diào)用intern()方法篓足,就會在字符串常量池中復(fù)制出一個對堆中已存在的字符串常量的引用段誊,然后返回對字符串常量池中這個對堆中已存在的字符串常量池的引用的引用(就是那么繞,你來咬我呀)栈拖。這樣连舍,在調(diào)用intern()方法結(jié)束之后,返回結(jié)果的就是對堆中該String對象的引用涩哟,這時候使用【==】去比較索赏,返回的結(jié)果就是true了。同樣的贴彼,常量池中的字面量23也不是真正意義的字面量23了潜腻,它真正的身份是堆中的那個String對象23。這樣的話器仗,使用【==】去比較字面量23和str2融涣,結(jié)果也就是true了。

第三個例子

String str4 = "45";

String str3 = newString("4") + newString("5");

System.out.println(str3 == str3.intern()); // falseSystem.out.println(str3 == "45"); // false

這個例子乍然看起來好像比前面的例子還要復(fù)雜精钮,實際上卻和上面的第一個例子是一樣的威鹿,最難理解的反而是第二個例子。

所以這里就不多說了轨香,而至于為什么還要舉這個例子忽你,我相信聰明的你一下子就明白了。

String對象的不可變性

先來看String對象的一段源碼弹沽。

publicfinalclassString??? implementsjava.io.Serializable, Comparable<String>, CharSequence {???

/** The value is used for character storage. */

????privatefinalchar value[];???

????/** Cache the hash code for the string */

????privateint hash; // Default to 0

????/** use serialVersionUID from JDK 1.0.2 for interoperability */

????privatestaticfinallong serialVersionUID = -6849794470754667710L;

}

從類簽名上來看檀夹,String類用了final修飾符筋粗,這就意味著這個類是不能被繼承的,這是決定String對象不可變特性的第一點炸渡。從類中的數(shù)組char[] value來看娜亿,這個類成員變量被private和final修飾符修飾,這就意味著其數(shù)值一旦被初始化之后就不能再被更改了蚌堵,這是決定String對象不可變特性的第二點买决。

Java開發(fā)者為什么要將String對象設(shè)置為不可變的,主要可以從以下三個方面去考慮:

1.安全性吼畏。假設(shè)String對象是可變的督赤,那么String對象將可能被惡意修改。

2.唯一性泻蚊。這個做法可以保證hash屬性值不會頻繁變更躲舌,也就確保了唯一性,使得類似HashMap的容器才能實現(xiàn)相應(yīng)的key-value緩存功能性雄。

3.功能性没卸。可以實現(xiàn)字符串常量池(究竟是先有設(shè)計秒旋,還是先有實現(xiàn)呢)约计。

String對象的優(yōu)化

字符串是常用的Java類型之一,所以對字符串的操作是避免不了的迁筛。而在對字符串的操作過程中煤蚌,如果使用不當(dāng)?shù)脑挘阅芸赡軙刑觳畹貏e细卧,所以有一些地方是要注意一下的尉桩。

拼接字符串的性能優(yōu)化

字符串的拼接是對字符串的操作中最頻繁的一個使用。由于我們都知道了String對象的不可變性酒甸,所以我們在開發(fā)過程中要盡量減少使用【+】進行字符串拼接操作魄健。這是因為使用【+】進行字符串拼接,會在得到最終想要的結(jié)果前產(chǎn)生很多無用的對象插勤。

String str = 'i';

str = str + ' ';

str = str + 'like';

str = str + ' ';

str = str + 'yanggb';

str = str + '.';

System.out.println(str); // i like yanggb.

事實上沽瘦,如果我們使用的是比較智能的IDE編寫代碼的話,編譯器是會提示將代碼優(yōu)化成使用StringBuilder或者StringBuffer對象來優(yōu)化字符串的拼接性能的农尖,因為StringBuilder和StringBuffer都是可變對象析恋,也就避免了過程中產(chǎn)生無用的對象了。而這兩種替代方案的區(qū)別是盛卡,在需要線程安全的情況下助隧,選用StringBuffer對象,這個對象是支持線程安全的;而在不需要線程安全的情況下并村,選用StringBuilder對象巍实,因為StringBuilder對象的性能在這種場景下,要比StringBuffer對象或String對象要好得多哩牍。

使用intern()方法優(yōu)化內(nèi)存占用

前面吐槽了intern()方法在實際開發(fā)中沒什么用棚潦,這里又來說使用intern()方法來優(yōu)化內(nèi)存占用了,這人真的是膝昆,嘿嘿丸边,真香。關(guān)于方法的使用就不說了荚孵,上面有詳盡的用法說明妹窖,這里來說說具體的應(yīng)用場景好了。有一位Twitter的工程師在Qcon全球軟件開發(fā)大會上分享了一個他們對String對象優(yōu)化的案例收叶,他們利用了這個String.intern()方法將以前需要20G內(nèi)存存儲優(yōu)化到只需要幾百兆內(nèi)存骄呼。具體就是,使用intern()方法將原本需要創(chuàng)建到堆內(nèi)存中的String對象都放到常量池中判没,因為常量池的不重復(fù)特性(存在則返回引用)谒麦,也就避免了大量的重復(fù)String對象造成的內(nèi)存浪費問題。

什么哆致,要我給intern()方法道歉?不可能患膛。String.intern()方法雖好摊阀,但是也是需要結(jié)合場景來使用的,并不能夠亂用踪蹬。因為實際上胞此,常量池的實現(xiàn)是類似于一個HashTable的實現(xiàn)方式,而HashTable存儲的數(shù)據(jù)越大跃捣,遍歷的時間復(fù)雜度就會增加漱牵。這就意味著,如果數(shù)據(jù)過大的話疚漆,整個字符串常量池的負擔(dān)就會大大增加酣胀,有可能性能不會得到提升卻反而有所下降。

字符串分割的性能優(yōu)化

字符串的分割是字符串操作的常用操作之一娶聘,對于字符串的分割闻镶,大部分人使用的都是split()方法,split()方法在大部分場景下接收的參數(shù)都是正則表達式丸升,這種分割方式本身沒有什么問題铆农,但是由于正則表達式的性能是非常不穩(wěn)定的,使用不恰當(dāng)?shù)脑捒赡軙鸹厮輪栴}并導(dǎo)致CPU的占用居高不下狡耻。在以下兩種情況下split()方法不會使用正則表達式:

1.傳入的參數(shù)長度為1墩剖,且不包含“.$|()[{^?*+\”regex元字符的情況下猴凹,不會使用正則表達式。

2.傳入的參數(shù)長度為2岭皂,第一個字符是反斜杠郊霎,并且第二個字符不是ASCII數(shù)字或ASCII字母的情況下,不會使用正則表達式蒲障。

所以我們在字符串分割時歹篓,應(yīng)該慎重使用split()方法,而首先考慮使用String.indexOf()方法來進行字符串分割揉阎,在String.indexOf()無法滿足分割要求的時候再使用Split()方法庄撮。而在使用split()方法分割字符串時,需要格外注意回溯問題毙籽。

總結(jié)

雖然說在不了解String對象的情況下也能使用String對象進行開發(fā)洞斯,但是了解String對象可以幫助我們寫出更好的代碼。

"只希望在故事的最后坑赡,我還是我烙如,你也還是你。"

本篇文章來自PHP中文網(wǎng)的java學(xué)習(xí)教程欄目:https://www.php.cn/java/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末毅否,一起剝皮案震驚了整個濱河市亚铁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌螟加,老刑警劉巖徘溢,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異捆探,居然都是意外死亡然爆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門黍图,熙熙樓的掌柜王于貴愁眉苦臉地迎上來曾雕,“玉大人,你說我怎么就攤上這事助被∑收牛” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵揩环,是天一觀的道長修械。 經(jīng)常有香客問我,道長检盼,這世上最難降的妖魔是什么肯污? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上蹦渣,老公的妹妹穿的比我還像新娘哄芜。我一直安慰自己,他們只是感情好柬唯,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布认臊。 她就那樣靜靜地躺著,像睡著了一般锄奢。 火紅的嫁衣襯著肌膚如雪失晴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天拘央,我揣著相機與錄音涂屁,去河邊找鬼。 笑死灰伟,一個胖子當(dāng)著我的面吹牛拆又,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播栏账,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼帖族,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了挡爵?” 一聲冷哼從身側(cè)響起竖般,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茶鹃,沒想到半個月后捻激,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡前计,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了垃杖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片男杈。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖调俘,靈堂內(nèi)的尸體忽然破棺而出伶棒,到底是詐尸還是另有隱情,我是刑警寧澤彩库,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布肤无,位于F島的核電站,受9級特大地震影響骇钦,放射性物質(zhì)發(fā)生泄漏宛渐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望窥翩。 院中可真熱鬧业岁,春花似錦、人聲如沸寇蚊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仗岸。三九已至允耿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扒怖,已是汗流浹背较锡。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留姚垃,地道東北人念链。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像积糯,于是被迫代替她去往敵國和親掂墓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容

  • String 是Java編程中的引用類型看成,不屬于基本類型君编,默認值為null,在Java中是用來創(chuàng)建于操作字符串川慌。源...
    小杰的快樂時光閱讀 539評論 0 1
  • 從網(wǎng)上復(fù)制的吃嘿,看別人的比較全面,自己搬過來梦重,方便以后查找兑燥。原鏈接:https://www.cnblogs.com/...
    lxtyp閱讀 1,345評論 0 9
  • String對象的實現(xiàn) String對象是Java中使用最頻繁的對象之一蚓胸,所以 Java 公司也在不斷的對Stri...
    職涯寶閱讀 427評論 0 0
  • 浮生々閱讀 227評論 0 4
  • 窗外沛膳,依舊是一個銀白的世界扔枫,一個人在屋子里,聽雪細細的下著锹安,烹雪煮茶短荐,別有一番滋味倚舀。這個冬天,因了干燥少雪搓侄,一直咳...
    櫻花樹下_cdbb閱讀 258評論 0 0