您好,登錄后才能下訂單哦!
這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)?lái)有關(guān)怎么在java中使用string對(duì)象,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
Java對(duì)象實(shí)現(xiàn)的演進(jìn)
String對(duì)象是Java中使用最頻繁的對(duì)象之一,所以Java開發(fā)者們也在不斷地對(duì)String對(duì)象的實(shí)現(xiàn)進(jìn)行優(yōu)化,以便提升String對(duì)象的性能。
Java6以及之前版本中String對(duì)象的屬性
在Java6以及之前版本中,String對(duì)象是對(duì)char數(shù)組進(jìn)行了封裝實(shí)現(xiàn)的對(duì)象,其主要有4個(gè)成員成員變量,分別是char數(shù)組、偏移量offset、字符數(shù)量count和哈希值hash。String對(duì)象是通過offset和count兩個(gè)屬性來(lái)定位char[]數(shù)組,獲取字符串。這樣做可以高效、快速地共享數(shù)組對(duì)象,同時(shí)節(jié)省內(nèi)存空間,但是這種方式卻可能會(huì)導(dǎo)致內(nèi)存泄漏的發(fā)生。
Java7、8版本中String對(duì)象的屬性
從Java7版本開始,Java對(duì)String類做了一些改變,具體是String類不再有offset和count兩個(gè)變量了。這樣做的好處是String對(duì)象占用的內(nèi)存稍微少了點(diǎn),同時(shí)String.substring()方法也不再共享char[]了,從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄漏問題。
Java9以及之后版本中String對(duì)象的屬性
從Java9版本開始,Java將char[]數(shù)組改為了byte[]數(shù)組。我們都知道,char是兩個(gè)字節(jié)的,如果用來(lái)存一個(gè)字節(jié)的情況下就會(huì)造成內(nèi)存空間的浪費(fèi)。而為了節(jié)約這一個(gè)字節(jié)的空間,Java開發(fā)者就改成了一個(gè)使用一個(gè)字節(jié)的byte來(lái)存儲(chǔ)字符串。
另外,在Java9中,String對(duì)象維護(hù)了一個(gè)新的屬性coder,這個(gè)屬性是編碼格式的標(biāo)識(shí),在計(jì)算字符串長(zhǎng)度或者調(diào)用indexOf()方法的時(shí)候,會(huì)需要根據(jù)這個(gè)字段去判斷如何計(jì)算字符串長(zhǎng)度。coder屬性默認(rèn)有0和1兩個(gè)值,其中0代表Latin-1(單字節(jié)編碼),1則表示UTF-16編碼。
String對(duì)象的創(chuàng)建方式與在內(nèi)存中的存放
在Java中,對(duì)于基本數(shù)據(jù)類型的變量和對(duì)對(duì)象的引用,保存在棧內(nèi)存的局部變量表中;而通過new關(guān)鍵字和Constructor創(chuàng)建的對(duì)象,則是保存在堆內(nèi)存中。而String對(duì)象的創(chuàng)建方式一般為兩種,一種是字面量(字符串常量)的方式,一種則是構(gòu)造函數(shù)(String())的方式,兩種方式在內(nèi)存中的存放有所不同。
字面量(字符串常量)的創(chuàng)建方式
使用字面量的方式創(chuàng)建字符串時(shí),JVM會(huì)在字符串常量池中先檢查是否存在該字面量,如果存在,則返回該字面量在內(nèi)存中的引用地址;如果不存在,則在字符串常量池中創(chuàng)建該字面量并返回引用。使用這種方式創(chuàng)建的好處是避免了相同值的字符串在內(nèi)存中被重復(fù)創(chuàng)建,節(jié)約了內(nèi)存,同時(shí)這種寫法也會(huì)比較簡(jiǎn)單易讀一些。
String str = "i like yanggb.";
字符串常量池
這里要特別說(shuō)明一下常量池。常量池是JVM為了減少字符串對(duì)象的重復(fù)創(chuàng)建,特別維護(hù)了一個(gè)特殊的內(nèi)存,這段內(nèi)存被稱為字符串常量池或者字符串字面量池。在JDK1.6以及之前的版本中,運(yùn)行時(shí)常量池是在方法區(qū)中的。在JDK1.7以及之后版本的JVM,已經(jīng)將運(yùn)行時(shí)常量池從方法區(qū)中移了出來(lái),在Java堆(Heap)中開辟了一塊區(qū)域用來(lái)存放運(yùn)行時(shí)常量池。而從JDK1.8開始,JVM取消了Java方法區(qū),取而代之的是位于直接內(nèi)存的元空間(MetaSpace)??偨Y(jié)就是,目前的字符串常量池在堆中。
我們所知道的幾個(gè)String對(duì)象的特點(diǎn)都來(lái)源于String常量池。
1.在常量池中會(huì)共享所有的String對(duì)象,因此String對(duì)象是不可被修改的,因?yàn)橐坏┍恍薷模蜁?huì)導(dǎo)致所有引用此String對(duì)象的變量都隨之改變(引用改變),所以String對(duì)象是被設(shè)計(jì)為不可修改的,后面會(huì)對(duì)這個(gè)不可變的特性做一個(gè)深入的了解。
2.String對(duì)象拼接字符串的性能較差的說(shuō)法也是來(lái)源于此,因?yàn)镾tring對(duì)象不可變的特性,每次修改(這里是拼接)都是返回一個(gè)新的字符串對(duì)象,而不是再原有的字符串對(duì)象上做修改,因此創(chuàng)建新的String對(duì)象會(huì)消耗較多的性能(開辟另外的內(nèi)存空間)。
3.因?yàn)槌A砍刂袆?chuàng)建的String對(duì)象是共享的,因此使用雙引號(hào)聲明的String對(duì)象(字面量)會(huì)直接存儲(chǔ)在常量池中,如果該字面量在之前已存在,則是會(huì)直接引用已存在的String對(duì)象,這一點(diǎn)在上面已經(jīng)描述過了,這里再次提及,是為了特別說(shuō)明這一做法保證了在常量池中的每個(gè)String對(duì)象都是唯一的,也就達(dá)到了節(jié)約內(nèi)存的目的。
構(gòu)造函數(shù)(String())的創(chuàng)建方式
使用構(gòu)造函數(shù)的方式創(chuàng)建字符串時(shí),JVM同樣會(huì)在字符串常量池中先檢查是否存在該字面量,只是檢查后的情況會(huì)和使用字面量創(chuàng)建的方式有所不同。如果存在,則會(huì)在堆中另外創(chuàng)建一個(gè)String對(duì)象,然后在這個(gè)String對(duì)象的內(nèi)部引用該字面量,最后返回該String對(duì)象在內(nèi)存地址中的引用;如果不存在,則會(huì)先在字符串常量池中創(chuàng)建該字面量,然后再在堆中創(chuàng)建一個(gè)String對(duì)象,然后再在這個(gè)String對(duì)象的內(nèi)部引用該字面量,最后返回該String對(duì)象的引用。
String str = new String("i like yanggb.");
這就意味著,只要使用這種方式,構(gòu)造函數(shù)都會(huì)另行在堆內(nèi)存中開辟空間,創(chuàng)建一個(gè)新的String對(duì)象。具體的理解是,在字符串常量池中不存在對(duì)應(yīng)的字面量的情況下,new String()會(huì)創(chuàng)建兩個(gè)對(duì)象,一個(gè)放入常量池中(字面量),一個(gè)放入堆內(nèi)存中(字符串對(duì)象)。
String對(duì)象的比較
比較兩個(gè)String對(duì)象是否相等,通常是有【==】和【equals()】?jī)蓚€(gè)方法。
在基本數(shù)據(jù)類型中,只可以使用【==】,也就是比較他們的值是否相同;而對(duì)于對(duì)象(包括String)來(lái)說(shuō),【==】比較的是地址是否相同,【equals()】才是比較他們內(nèi)容是否相同;而equals()是Object都擁有的一個(gè)函數(shù),本身就要求對(duì)內(nèi)部值進(jìn)行比較。
String str = "i like yanggb."; String str1 = new String("i like yanggb."); System.out.println(str == str1); // false System.out.println(str.equals(str1)); // true
因?yàn)槭褂米置媪糠绞絼?chuàng)建的String對(duì)象和使用構(gòu)造函數(shù)方式創(chuàng)建的String對(duì)象的內(nèi)存地址是不同的,但是其中的內(nèi)容卻是相同的,也就導(dǎo)致了上面的結(jié)果。
String對(duì)象中的intern()方法
我們都知道,String對(duì)象中有很多實(shí)用的方法。為什么其他的方法都不說(shuō),這里要特別說(shuō)明這個(gè)intern()方法呢,因?yàn)槠渲械倪@個(gè)intern()方法最為特殊。它的特殊性在于,這個(gè)方法在業(yè)務(wù)場(chǎng)景中幾乎用不上,它的存在就是在為難程序員的,也可以說(shuō)是為了幫助程序員了解JVM的內(nèi)存結(jié)構(gòu)而存在的(?我信你個(gè)鬼,你個(gè)糟老頭子壞得很)。
/** * 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. **/ public native String intern();
上面是源碼中的intern()方法的官方注釋說(shuō)明,大概意思就是intern()方法用來(lái)返回常量池中的某字符串,如果常量池中已經(jīng)存在該字符串,則直接返回常量池中該對(duì)象的引用。否則,在常量池中加入該對(duì)象,然后返回引用。然后我們可以從方法簽名上看出intern()方法是一個(gè)native方法。
下面通過幾個(gè)例子來(lái)詳細(xì)了解下intern()方法的用法。
第一個(gè)例子
String str1 = new String("1"); System.out.println(str1 == str1.intern()); // false System.out.println(str1 == "1"); // false
在上面的例子中,intern()方法返回的是常量池中的引用,而str1保存的是堆中對(duì)象的引用,因此兩個(gè)打印語(yǔ)句的結(jié)果都是false。
第二個(gè)例子
String str2 = new String("2") + new String("3"); System.out.println(str2 == str2.intern()); // true System.out.println(str2 == "23"); // true
在上面的例子中,str2保存的是堆中一個(gè)String對(duì)象的引用,這和JVM對(duì)【+】的優(yōu)化有關(guān)。實(shí)際上,在給str2賦值的第一條語(yǔ)句中,創(chuàng)建了3個(gè)對(duì)象,分別是在字符串常量池中創(chuàng)建的2和3、還有在堆中創(chuàng)建的字符串對(duì)象23。因?yàn)樽址A砍刂胁淮嬖谧址畬?duì)象23,所以這里要特別注意:intern()方法在將堆中存在的字符串對(duì)象加入常量池的時(shí)候采取了一種截然不同的處理方案——不是在常量池中建立字面量,而是直接將該String對(duì)象自身的引用復(fù)制到常量池中,即常量池中保存的是堆中已存在的字符串對(duì)象的引用。根據(jù)前面的說(shuō)法,這時(shí)候調(diào)用intern()方法,就會(huì)在字符串常量池中復(fù)制出一個(gè)對(duì)堆中已存在的字符串常量的引用,然后返回對(duì)字符串常量池中這個(gè)對(duì)堆中已存在的字符串常量池的引用的引用(就是那么繞,你來(lái)咬我呀)。這樣,在調(diào)用intern()方法結(jié)束之后,返回結(jié)果的就是對(duì)堆中該String對(duì)象的引用,這時(shí)候使用【==】去比較,返回的結(jié)果就是true了。同樣的,常量池中的字面量23也不是真正意義的字面量23了,它真正的身份是堆中的那個(gè)String對(duì)象23。這樣的話,使用【==】去比較字面量23和str2,結(jié)果也就是true了。
第三個(gè)例子
String str4 = "45"; String str3 = new String("4") + new String("5"); System.out.println(str3 == str3.intern()); // false System.out.println(str3 == "45"); // false
這個(gè)例子乍然看起來(lái)好像比前面的例子還要復(fù)雜,實(shí)際上卻和上面的第一個(gè)例子是一樣的,最難理解的反而是第二個(gè)例子。
所以這里就不多說(shuō)了,而至于為什么還要舉這個(gè)例子,我相信聰明的你一下子就明白了(我有醫(yī)保,你來(lái)打我呀)。
String對(duì)象的不可變性
先來(lái)看String對(duì)象的一段源碼。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; }
從類簽名上來(lái)看,String類用了final修飾符,這就意味著這個(gè)類是不能被繼承的,這是決定String對(duì)象不可變特性的第一點(diǎn)。從類中的數(shù)組char[] value來(lái)看,這個(gè)類成員變量被private和final修飾符修飾,這就意味著其數(shù)值一旦被初始化之后就不能再被更改了,這是決定String對(duì)象不可變特性的第二點(diǎn)。
Java開發(fā)者為什么要將String對(duì)象設(shè)置為不可變的,主要可以從以下三個(gè)方面去考慮:
1.安全性。假設(shè)String對(duì)象是可變的,那么String對(duì)象將可能被惡意修改。
2.唯一性。這個(gè)做法可以保證hash屬性值不會(huì)頻繁變更,也就確保了唯一性,使得類似HashMap的容器才能實(shí)現(xiàn)相應(yīng)的key-value緩存功能。
3.功能性。可以實(shí)現(xiàn)字符串常量池(究竟是先有設(shè)計(jì),還是先有實(shí)現(xiàn)呢)。
String對(duì)象的優(yōu)化
字符串是常用的Java類型之一,所以對(duì)字符串的操作是避免不了的。而在對(duì)字符串的操作過程中,如果使用不當(dāng)?shù)脑挘阅芸赡軙?huì)有天差地別,所以有一些地方是要注意一下的。
拼接字符串的性能優(yōu)化
字符串的拼接是對(duì)字符串的操作中最頻繁的一個(gè)使用。由于我們都知道了String對(duì)象的不可變性,所以我們?cè)陂_發(fā)過程中要盡量減少使用【+】進(jìn)行字符串拼接操作。這是因?yàn)槭褂谩?】進(jìn)行字符串拼接,會(huì)在得到最終想要的結(jié)果前產(chǎn)生很多無(wú)用的對(duì)象。
String str = 'i'; str = str + ' '; str = str + 'like'; str = str + ' '; str = str + 'yanggb'; str = str + '.'; System.out.println(str); // i like yanggb.
事實(shí)上,如果我們使用的是比較智能的IDE編寫代碼的話,編譯器是會(huì)提示將代碼優(yōu)化成使用StringBuilder或者StringBuffer對(duì)象來(lái)優(yōu)化字符串的拼接性能的,因?yàn)镾tringBuilder和StringBuffer都是可變對(duì)象,也就避免了過程中產(chǎn)生無(wú)用的對(duì)象了。而這兩種替代方案的區(qū)別是,在需要線程安全的情況下,選用StringBuffer對(duì)象,這個(gè)對(duì)象是支持線程安全的;而在不需要線程安全的情況下,選用StringBuilder對(duì)象,因?yàn)镾tringBuilder對(duì)象的性能在這種場(chǎng)景下,要比StringBuffer對(duì)象或String對(duì)象要好得多。
使用intern()方法優(yōu)化內(nèi)存占用
前面吐槽了intern()方法在實(shí)際開發(fā)中沒什么用,這里又來(lái)說(shuō)使用intern()方法來(lái)優(yōu)化內(nèi)存占用了,這人真的是,嘿嘿,真香。關(guān)于方法的使用就不說(shuō)了,上面有詳盡的用法說(shuō)明,這里來(lái)說(shuō)說(shuō)具體的應(yīng)用場(chǎng)景好了。有一位Twitter的工程師在Qcon全球軟件開發(fā)大會(huì)上分享了一個(gè)他們對(duì)String對(duì)象優(yōu)化的案例,他們利用了這個(gè)String.intern()方法將以前需要20G內(nèi)存存儲(chǔ)優(yōu)化到只需要幾百兆內(nèi)存。具體就是,使用intern()方法將原本需要?jiǎng)?chuàng)建到堆內(nèi)存中的String對(duì)象都放到常量池中,因?yàn)槌A砍氐牟恢貜?fù)特性(存在則返回引用),也就避免了大量的重復(fù)String對(duì)象造成的內(nèi)存浪費(fèi)問題。
什么,要我給intern()方法道歉?不可能。String.intern()方法雖好,但是也是需要結(jié)合場(chǎng)景來(lái)使用的,并不能夠亂用。因?yàn)閷?shí)際上,常量池的實(shí)現(xiàn)是類似于一個(gè)HashTable的實(shí)現(xiàn)方式,而HashTable存儲(chǔ)的數(shù)據(jù)越大,遍歷的時(shí)間復(fù)雜度就會(huì)增加。這就意味著,如果數(shù)據(jù)過大的話,整個(gè)字符串常量池的負(fù)擔(dān)就會(huì)大大增加,有可能性能不會(huì)得到提升卻反而有所下降。
字符串分割的性能優(yōu)化
字符串的分割是字符串操作的常用操作之一,對(duì)于字符串的分割,大部分人使用的都是split()方法,split()方法在大部分場(chǎng)景下接收的參數(shù)都是正則表達(dá)式,這種分割方式本身沒有什么問題,但是由于正則表達(dá)式的性能是非常不穩(wěn)定的,使用不恰當(dāng)?shù)脑捒赡軙?huì)引起回溯問題并導(dǎo)致CPU的占用居高不下。在以下兩種情況下split()方法不會(huì)使用正則表達(dá)式:
1.傳入的參數(shù)長(zhǎng)度為1,且不包含“.$|()[{^?*+\”regex元字符的情況下,不會(huì)使用正則表達(dá)式。
2.傳入的參數(shù)長(zhǎng)度為2,第一個(gè)字符是反斜杠,并且第二個(gè)字符不是ASCII數(shù)字或ASCII字母的情況下,不會(huì)使用正則表達(dá)式。
所以我們?cè)谧址指顣r(shí),應(yīng)該慎重使用split()方法,而首先考慮使用String.indexOf()方法來(lái)進(jìn)行字符串分割,在String.indexOf()無(wú)法滿足分割要求的時(shí)候再使用Split()方法。而在使用split()方法分割字符串時(shí),需要格外注意回溯問題。
上述就是小編為大家分享的怎么在java中使用string對(duì)象了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。