您好,登錄后才能下訂單哦!
這篇文章主要介紹“Java之怎么理解String”,在日常操作中,相信很多人在Java之怎么理解String問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java之怎么理解String”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
“”
String s = new String("abc")
這段代碼創(chuàng)建了幾個對象呢?s=="abc"
這個判斷的結(jié)果是什么?s.substring(0,2).intern()=="ab"
這個的結(jié)果是什么呢?s.charAt(index) 真的能表示出所有對應(yīng)的字符嗎?
"abc"+"gbn"+s
直接的字符串拼接是否真的比使用StringBuilder的性能低?
很高興遇見你~
Java中的String對象特性,與c/c++語言是很不同的,重點在于其不可變性。那么為了服務(wù)字符串不可變性的設(shè)計,則衍生出非常多的相關(guān)問題:為什么要保持其不可變?底層如何存儲字符串?如何進(jìn)行字符串操作才擁有更好的性能?等等。此外,字符編碼的相關(guān)知識也是非常重要;畢竟,現(xiàn)在使用emoij是再正常不過的事情了。
文章的內(nèi)容圍繞著不可變 這個重點展開:
分析String對象的不可變性;
常量池的存儲原理以及intern方法的原理
字符串拼接的原理以及優(yōu)化
代碼單元與代碼點的區(qū)別
總結(jié)
那么,我們開始吧~
理解String的不可變性,我們可以簡單看幾行代碼:
String string = "abcd"; String string1 = string.replace("a","b"); System.out.println(string); System.out.println(string1); 輸出: abcd bbcd
string.replace("a","b")
這個方法把"abcd"
中的a
換成了b
。通過輸出可以發(fā)現(xiàn),原字符串string
并沒有發(fā)生任何改變,replace
方法構(gòu)造了一個新的字符串"bbcd"
并賦值給了string1
變量。這就是String的不可變性。
再舉個栗子:把"abcd"
的最后一個字符d
改成a
,在c/c++語言中,直接修改最后一個字符即可;而在java中,需要重新創(chuàng)建一個String對象:abca
,因為"abcd"
本身是不可變的,不能被修改。
“String對象值是不可變的,一切操作都不會改變String的值,而是通過構(gòu)造新的字符串來實現(xiàn)字符串操作。
”
很多時候很難理解,為什么Java要如此設(shè)計,這樣不是會導(dǎo)致性能的下降嗎?回顧一下我們?nèi)粘J褂肧tring的場景,更多的時候并沒有直接去修改一個string,而是使用一次,則被拋棄。但下次,很可能,又再一次使用到相同的String對象。例如日志打印:
Log.d("MainActivity",string);
前面的"MainActivity"
我們并不需要去更改他,但是卻會頻繁使用到這個字符串。Java把String設(shè)計為不可變,正是為了保持?jǐn)?shù)據(jù)的一致性,使得相同字面量的String引用同個對象。例如:
String s1 = "hello"; String s2 = "hello";
s1
與s2
引用的是同個String對象。如果String可變,那么就無法實現(xiàn)這個設(shè)計了。因此,我們可以重復(fù)利用我們創(chuàng)建過的String對象,而無需重新創(chuàng)建他。
“基于重復(fù)使用String的情況比更改String的場景更多的前提下,Java把String設(shè)計為不可變,保持?jǐn)?shù)據(jù)一致性,使得同個字面量的字符串可以引用同個String對象,重復(fù)利用已存在的String對象。
”
在《Java編程思想》一書中還提到另一個觀點。我們先看下面的代碼:
public String allCase(String s){ return string.toUpperCase(); }
allCase
方法把傳入的String對象全部變成大寫并返回修改后的字符串。而此時,調(diào)用者的期望是傳入的String對象僅僅作為提供信息的作用,而不希望被修改,那么String不可變的特性則非常符合這一點。
“使用String對象作為參數(shù)時,我們希望不要改變String對象本身,而String的不可變性符合了這一點。
”
由于String對象的不可變特性,在存儲上也與普通的對象不一樣。我們都知道對象創(chuàng)建在 堆 上,而String對象其實也一樣,不一樣的是,同時也存儲在 常量池 中。處于堆區(qū)中的String對象,在GC時有極大可能被回收;而常量池中的String對象則不會輕易被回收,那么則可以重復(fù)利用常量池中的String對象。也就是說, 常量池是String對象得以重復(fù)利用的根本原因 。
“常量池不輕易垃圾回收的特性,使得常量池中的String對象可以一直存在,重復(fù)被利用。
”
往常量池中創(chuàng)建String對象的方式有兩種: 顯式使用雙引號構(gòu)造字符串對象、使用String對象的intern()
方法 。這兩個方法不一定會在常量池中創(chuàng)建對象,如果常量池中已存在相同的對象,則會直接返回該對象的引用,重復(fù)利用String對象。其他創(chuàng)建String對象的方法都是在堆區(qū)中創(chuàng)建String對象。舉個栗子吧。
當(dāng)我們通過new String()
的方法或者調(diào)用String對象的實例方法,如string.substring()
方法,會在堆區(qū)中創(chuàng)建一個String對象。而當(dāng)我們使用雙引號創(chuàng)建一個字符串對象,如String s = "abc"
,或調(diào)用String對象的intern()
方法時,會在常量池中創(chuàng)建一個對象,如下圖所示:
image.png
還記得我們文章開頭的問題嗎?
String s = new String("abc")
,這句代碼創(chuàng)建了幾個對象?"abc"
在常量池中構(gòu)造了一個對象,new String()
方法在堆區(qū)中又創(chuàng)建了一個對象,所以一共是兩個。
s=="abc"
的結(jié)果是false。兩個不同的對象,一個位于堆中,一個位于常量池中。
s.substring(0,2).intern()=="ab"
intern方法在常量池中構(gòu)建了一個值為“ab"的String對象,"ab"語句不會再去構(gòu)建一個新的String對象,而是返回已經(jīng)存在的String對象。所以結(jié)果是true。
“只有顯式使用雙引號構(gòu)造字符串對象、使用String對象的
”intern()
方法 這兩種方法會在常量池中創(chuàng)建String對象,其他方法都是在堆區(qū)創(chuàng)建對象。每次在常量池創(chuàng)建String對象前都會檢查是否存在相同的String對象,如果是則會直接返回該對象的引用,而不會重新創(chuàng)建一個對象。
關(guān)于intern方法還有一個問題需要講一下,在不同jdk版本所執(zhí)行的具體邏輯是不同的。在jdk6以前,方法區(qū)是存放在永生代內(nèi)存區(qū)域中,與堆區(qū)是分割開的,那么當(dāng)往常量池中創(chuàng)建對象時,就需要進(jìn)行深拷貝,也就是把一個對象完整地復(fù)制一遍并創(chuàng)建新的對象,如下圖:
image.png
觀察這個代碼:
String s = new String(new char[]{'a'}); s.intern(); System.out.println(s=="a");
在jdk6以前創(chuàng)建的是兩個不同的對象,輸出為false;而jdk7以后常量池中并不會創(chuàng)建新的對象,引用的是同個對象,所以輸出是true。
“jdk6之前使用intern創(chuàng)建對象使用的深拷貝,而在jdk7之后使用的是淺拷貝,得以重復(fù)利用堆區(qū)中的String對象。
”
通過上面的分析,String真正重復(fù)利用字符串是在使用雙引號直接創(chuàng)建字符串時。使用intern方法雖然可以返回常量池中的字符串引用,但是本身已經(jīng)需要堆區(qū)中的一個String對象。因而我們可以得出結(jié)論:
“盡量使用雙引號顯式構(gòu)建字符串;如果一個字符串需要頻繁被重復(fù)利用,可以調(diào)用intern方法將他存放到常量池中。
”
字符串操作最多的莫過于字符串拼接了,由于String對象的不可變性,如果每次拼接都需要創(chuàng)建新的字符串對象就太影響性能了。因此,官方推出了兩個類: StringBuffer、StringBuilder 。這兩個類可以在不創(chuàng)建新的String對象的前提下拼裝字符串、修改字符串。如下代碼:
StringBuilder stringBuilder = new StringBuilder("abc"); stringBuilder.append("p") .append(new char[]{'q'}) .deleteCharAt(2) .insert(2,"abc"); String s = stringBuilder.toString();
拼接、插入、刪除都可以很快速地完成。因此,使用StringBuilder進(jìn)行修改、拼接等操作來初始化字符串是更加高效率的做法。StringBuffer和StringBuilder的接口一致,但StringBuffer對操作方法都加上了synchronize關(guān)鍵字,保證線程安全的同時,也付出了對應(yīng)的性能代價。單線程環(huán)境下更加建議使用StringBuilder。
“拼接、修改等操作來初始化字符串時使用StringBuilder和StringBuffer可以提高性能;單線程環(huán)境下使用StringBuilder更加合適。
”
一般情況下,我們會使用+
來連接字符串。+
在java經(jīng)過了運算符重載,可以用來拼接字符串。編譯器也對+
進(jìn)行了一系列的優(yōu)化。觀察下面的代碼:
String s1 = "ab"+"cd"+"fg"; String s2 = "hello"+s1; Object object = new Object(); String s3 = s2 + object;
對于s1字符串而言,編譯器會把"ab"+"cd"+"fg"
直接優(yōu)化成"abcdefg"
,與String s1 = "abcdefg";
是等價的。這種優(yōu)化也就減少了拼接時產(chǎn)生的消耗。甚至比使用StringBuilder更加高效。
s2的拼接編譯器會自動創(chuàng)建一個StringBuilder來構(gòu)建字符串。也就相當(dāng)于以下代碼:
StringBuilder sb = new StringBuilder(); sb.append("hello"); sb.append(s1); String s2 = sb.toString();
那么這是不是意味著我們可以不需要顯式使用StringBuilder了,反正編譯器都會幫助我們優(yōu)化?當(dāng)然不是,觀察下邊的代碼:
String s = "a";for(int i=0;i<=100;i++){ s+=i; }
這里有100次循環(huán),則會創(chuàng)建100個StringBuilder對象,這顯然是一個非常錯誤的做法。這時候就需要我們來顯示創(chuàng)建StringBuilder對象了:
StringBuilder sb = new StringBuilder("a");for(int i=0;i<=100;i++){ sb.append(i); } String s = sb.toString();
只需要構(gòu)建一個StringBuilder對象,性能就極大地提高了。
String s3 = s2 + object;
字符串拼接也是支持直接拼接一個普通的對象,這個時候會調(diào)用該對象的toString
方法返回一個字符串來進(jìn)行拼接。toString
方法是Object類的方法,若子類沒有重寫,則會調(diào)用Object類的toString方法,該方法默認(rèn)輸出類名+引用地址。這看起來沒有什么問題,但是有一個大坑:切記不要在toString方法中直接使用+
拼接自身 。如下代碼
@Overridepublic String toString() { return this+"abc"; }
這里直接拼接this會調(diào)用this的toString方法,從而造成了無限遞歸。
“Java對+拼接字符串進(jìn)行了優(yōu)化:
可以直接拼接普通對象
字面量直接拼接會合成一個字面量
普通拼接會使用StringBuilder來進(jìn)行優(yōu)化
但同時也有注意這些優(yōu)化是有限度的,我們需要在合適的場景選擇合適的拼接方式來提高性能。
”
在Java中,一般情況下,一個char對象可以存儲一個字符,一個char的大小是16位。但隨著計算機(jī)的發(fā)展,字符集也在不斷地發(fā)展,16位的存儲大小已經(jīng)不夠用了,因此拓展了使用兩個char,也就是32位來存儲一些特殊的字符,如emoij。一個16位稱為一個 代碼單元 ,一個字符稱為 代碼點 ,一個代碼點可能占用一個代碼單元,也可能是兩個。
在一個字符串中,當(dāng)我們調(diào)用String.length()
方法時,返回的是代碼單元的數(shù)目, String.charAt()
返回也是對應(yīng)下標(biāo)的代碼單元。這在正常情況下并沒有什么問題。而如果允許輸入特殊字符時,這就有大問題了。要獲得真正的代碼點數(shù)目,可以調(diào)用 String .codePointCount
方法;要獲得對應(yīng)的代碼點,可調(diào)用 String.codePointAt
方法。以此來兼容拓展的字符集。
“一個字符為一個代碼點,一個char稱為一個代碼單元。一個代碼點可能占據(jù)一個或兩個代碼單元。若允許輸入特殊字符,則必須使用代碼點為單位來操作字符串。
”
到此,關(guān)于“Java之怎么理解String”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。