「」
String s = new String("abc")
這段代碼建立了幾個對象呢?s=="abc"
這個判斷的結果是什麼?s.substring(0,2).intern()=="ab"
這個的結果是什麼呢?htmls.charAt(index) 真的能表示出全部對應的字符嗎?java
"abc"+"gbn"+s
直接的字符串拼接是否真的比使用StringBuilder的性能低?c++
前言
很高興碰見你~git
Java中的String對象特性,與c/c++語言是很不一樣的,重點在於其不可變性。那麼爲了服務字符串不可變性的設計,則衍生出很是多的相關問題:爲何要保持其不可變?底層如何存儲字符串?如何進行字符串操做才擁有更好的性能?等等。此外,字符編碼的相關知識也是很是重要;畢竟,如今使用emoij是再正常不過的事情了。面試
文章的內容圍繞着不可變 這個重點展開:正則表達式
-
分析String對象的不可變性;編程
-
常量池的存儲原理以及intern方法的原理api
-
字符串拼接的原理以及優化安全
-
代碼單元與代碼點的區別網絡
-
總結
那麼,咱們開始吧~
不可變性
理解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
。經過輸出能夠發現,原字符串string
並無發生任何改變,replace
方法構造了一個新的字符串"bbcd"
並賦值給了string1
變量。這就是String的不可變性。
再舉個栗子:把"abcd"
的最後一個字符d
改爲a
,在c/c++語言中,直接修改最後一個字符便可;而在java中,須要從新建立一個String對象:abca
,由於"abcd"
自己是不可變的,不能被修改。
「String對象值是不可變的,一切操做都不會改變String的值,而是經過構造新的字符串來實現字符串操做。
」
不少時候很難理解,爲何Java要如此設計,這樣不是會致使性能的降低嗎?回顧一下咱們平常使用String的場景,更多的時候並無直接去修改一個string,而是使用一次,則被拋棄。但下次,極可能,又再一次使用到相同的String對象。例如日誌打印:
Log.d("MainActivity",string);
前面的"MainActivity"
咱們並不須要去更改他,可是卻會頻繁使用到這個字符串。Java把String設計爲不可變,正是爲了保持數據的一致性,使得相同字面量的String引用同個對象。例如:
String s1 = "hello"; String s2 = "hello";
s1
與s2
引用的是同個String對象。若是String可變,那麼就沒法實現這個設計了。所以,咱們能夠重複利用咱們建立過的String對象,而無需從新建立他。
「基於重複使用String的狀況比更改String的場景更多的前提下,Java把String設計爲不可變,保持數據一致性,使得同個字面量的字符串能夠引用同個String對象,重複利用已存在的String對象。
」
在《Java編程思想》一書中還提到另外一個觀點。咱們先看下面的代碼:
public String allCase(String s){ return string.toUpperCase(); }
allCase
方法把傳入的String對象所有變成大寫並返回修改後的字符串。而此時,調用者的指望是傳入的String對象僅僅做爲提供信息的做用,而不但願被修改,那麼String不可變的特性則很是符合這一點。
「使用String對象做爲參數時,咱們但願不要改變String對象自己,而String的不可變性符合了這一點。
」
存儲原理
因爲String對象的不可變特性,在存儲上也與普通的對象不同。咱們都知道對象建立在 堆 上,而String對象其實也同樣,不同的是,同時也存儲在 常量池 中。處於堆區中的String對象,在GC時有極大可能被回收;而常量池中的String對象則不會輕易被回收,那麼則能夠重複利用常量池中的String對象。也就是說, 常量池是String對象得以重複利用的根本緣由 。
「常量池不輕易垃圾回收的特性,使得常量池中的String對象能夠一直存在,重複被利用。
」
往常量池中建立String對象的方式有兩種: 顯式使用雙引號構造字符串對象、使用String對象的intern()
方法 。這兩個方法不必定會在常量池中建立對象,若是常量池中已存在相同的對象,則會直接返回該對象的引用,重複利用String對象。其餘建立String對象的方法都是在堆區中建立String對象。舉個栗子吧。
當咱們經過new String()
的方法或者調用String對象的實例方法,如string.substring()
方法,會在堆區中建立一個String對象。而當咱們使用雙引號建立一個字符串對象,如String s = "abc"
,或調用String對象的intern()
方法時,會在常量池中建立一個對象,以下圖所示:
image.png
還記得咱們文章開頭的問題嗎?
-
String s = new String("abc")
,這句代碼建立了幾個對象?"abc"
在常量池中構造了一個對象,new String()
方法在堆區中又建立了一個對象,因此一共是兩個。 -
s=="abc"
的結果是false。兩個不一樣的對象,一個位於堆中,一個位於常量池中。 -
s.substring(0,2).intern()=="ab"
intern方法在常量池中構建了一個值爲「ab"的String對象,"ab"語句不會再去構建一個新的String對象,而是返回已經存在的String對象。因此結果是true。
「只有顯式使用雙引號構造字符串對象、使用String對象的
」intern()
方法 這兩種方法會在常量池中建立String對象,其餘方法都是在堆區建立對象。每次在常量池建立String對象前都會檢查是否存在相同的String對象,若是是則會直接返回該對象的引用,而不會從新建立一個對象。
關於intern方法還有一個問題須要講一下,在不一樣jdk版本所執行的具體邏輯是不一樣的。在jdk6之前,方法區是存放在永生代內存區域中,與堆區是分割開的,那麼當往常量池中建立對象時,就須要進行深拷貝,也就是把一個對象完整地複製一遍並建立新的對象,以下圖:
image.png
永生代有一個很嚴重的缺點:容易發生OOM 。永生代是有內存上限的,且很小,當程序大量調用intern方法時很容易就發生OOM。在JDK7時將常量池遷移出了永生代,改在堆區中實現,jdk8之後使用了本地空間實現。jdk7之後常量池的實現使得在常量池中建立對象能夠進行淺拷貝,也就是不須要把整個對象複製過去,而只須要複製對象的引用便可,避免重複建立對象,以下圖:
image.png
觀察這個代碼:
String s = new String(new char[]{'a'}); s.intern(); System.out.println(s=="a");
在jdk6之前建立的是兩個不一樣的對象,輸出爲false;而jdk7之後常量池中並不會建立新的對象,引用的是同個對象,因此輸出是true。
「jdk6以前使用intern建立對象使用的深拷貝,而在jdk7以後使用的是淺拷貝,得以重複利用堆區中的String對象。
」
經過上面的分析,String真正重複利用字符串是在使用雙引號直接建立字符串時。使用intern方法雖然能夠返回常量池中的字符串引用,可是自己已經須要堆區中的一個String對象。於是咱們能夠得出結論:
「儘可能使用雙引號顯式構建字符串;若是一個字符串須要頻繁被重複利用,能夠調用intern方法將他存放到常量池中。
」
字符串拼接
字符串操做最多的莫過於字符串拼接了,因爲String對象的不可變性,若是每次拼接都須要建立新的字符串對象就太影響性能了。所以,官方推出了兩個類: StringBuffer、StringBuilder 。這兩個類能夠在不建立新的String對象的前提下拼裝字符串、修改字符串。以下代碼:
StringBuilder stringBuilder = new StringBuilder("abc"); stringBuilder.append("p") .append(new char[]{'q'}) .deleteCharAt(2) .insert(2,"abc"); String s = stringBuilder.toString();
拼接、插入、刪除均可以很快速地完成。所以,使用StringBuilder進行修改、拼接等操做來初始化字符串是更加高效率的作法。StringBuffer和StringBuilder的接口一致,但StringBuffer對操做方法都加上了synchronize關鍵字,保證線程安全的同時,也付出了對應的性能代價。單線程環境下更加建議使用StringBuilder。
「拼接、修改等操做來初始化字符串時使用StringBuilder和StringBuffer能夠提升性能;單線程環境下使用StringBuilder更加合適。
」
通常狀況下,咱們會使用+
來鏈接字符串。+
在java通過了運算符重載,能夠用來拼接字符串。編譯器也對+
進行了一系列的優化。觀察下面的代碼:
String s1 = "ab"+"cd"+"fg"; String s2 = "hello"+s1; Object object = new Object(); String s3 = s2 + object;
-
對於s1字符串而言,編譯器會把
"ab"+"cd"+"fg"
直接優化成"abcdefg"
,與String s1 = "abcdefg";
是等價的。這種優化也就減小了拼接時產生的消耗。甚至比使用StringBuilder更加高效。 -
s2的拼接編譯器會自動建立一個StringBuilder來構建字符串。也就至關於如下代碼:
StringBuilder sb = new StringBuilder(); sb.append("hello"); sb.append(s1); String s2 = sb.toString();
那麼這是否是意味着咱們能夠不須要顯式使用StringBuilder了,反正編譯器都會幫助咱們優化?固然不是,觀察下邊的代碼:
String s = "a"; for(int i=0;i<=100;i++){ s+=i; }
這裏有100次循環,則會建立100個StringBuilder對象,這顯然是一個很是錯誤的作法。這時候就須要咱們來顯示建立StringBuilder對象了:
StringBuilder sb = new StringBuilder("a"); for(int i=0;i<=100;i++){ sb.append(i); } String s = sb.toString();
只須要構建一個StringBuilder對象,性能就極大地提升了。
-
String s3 = s2 + object;
字符串拼接也是支持直接拼接一個普通的對象,這個時候會調用該對象的toString
方法返回一個字符串來進行拼接。toString
方法是Object類的方法,若子類沒有重寫,則會調用Object類的toString方法,該方法默認輸出類名+引用地址。這看起來沒有什麼問題,可是有一個大坑:切記不要在toString方法中直接使用+
拼接自身 。以下代碼@Override public String toString() { return this+"abc"; }
這裏直接拼接this會調用this的toString方法,從而形成了無限遞歸。
「Java對+拼接字符串進行了優化:
能夠直接拼接普通對象
字面量直接拼接會合成一個字面量
普通拼接會使用StringBuilder來進行優化
但同時也有注意這些優化是有限度的,咱們須要在合適的場景選擇合適的拼接方式來提升性能。
」
編碼問題
在Java中,通常狀況下,一個char對象能夠存儲一個字符,一個char的大小是16位。但隨着計算機的發展,字符集也在不斷地發展,16位的存儲大小已經不夠用了,所以拓展了使用兩個char,也就是32位來存儲一些特殊的字符,如emoij。一個16位稱爲一個 代碼單元 ,一個字符稱爲 代碼點 ,一個代碼點可能佔用一個代碼單元,也多是兩個。
在一個字符串中,當咱們調用String.length()
方法時,返回的是代碼單元的數目, String.charAt()
返回也是對應下標的代碼單元。這在正常狀況下並無什麼問題。而若是容許輸入特殊字符時,這就有大問題了。要得到真正的代碼點數目,能夠調用 String .codePointCount
方法;要得到對應的代碼點,可調用 String.codePointAt
方法。以此來兼容拓展的字符集。
「一個字符爲一個代碼點,一個char稱爲一個代碼單元。一個代碼點可能佔據一個或兩個代碼單元。若容許輸入特殊字符,則必須使用代碼點爲單位來操做字符串。
」
總結
到此,關於String的一些重點問題就分析完畢了,文章開頭的問題讀者應該也都知道答案了。這些是面試常考題,也是String的重點。除此以外,關於正則表達式、輸入與輸出、經常使用api等等也是String相關很重要的內容,有興趣的讀者可自行學習。
但願文章對你有幫助。
參考資料
-
《Java編程思想》 java工程師皆知的神書,詳細講解了如何更好運用java來編程,感覺編程思想。
-
《Java核心技術卷一》 入門書籍,主要講解如何使用String的api以及一些注意的點。
-
《深刻理解JVM》對於理解方法區以及常量池有很是大的幫助。
-
深刻解析String#intern美團技術團隊的一篇分析String.intern方法的文章。
-
感謝網絡其餘博客的貢獻。
「全文到此,原創不易,以爲有幫助能夠點贊收藏評論轉發。 筆者才疏學淺,有任何想法歡迎評論區交流指正。 如需轉載請評論區或私信交流。
另外歡迎光臨筆者的我的博客:傳送門
」