Java之String重點解析

  1. String s = new String("abc")這段代碼建立了幾個對象呢?s=="abc"這個判斷的結果是什麼?s.substring(0,2).intern()=="ab"這個的結果是什麼呢?html

  2. s.charAt(index) 真的能表示出全部對應的字符嗎?java

  3. "abc"+"gbn"+s直接的字符串拼接是否真的比使用StringBuilder的性能低?c++

前言

很高興碰見你~git

Java中的String對象特性,與c/c++語言是很不一樣的,重點在於其不可變性。那麼爲了服務字符串不可變性的設計,則衍生出很是多的相關問題:爲何要保持其不可變?底層如何存儲字符串?如何進行字符串操做才擁有更好的性能?等等。此外,字符編碼的相關知識也是很是重要;畢竟,如今使用emoij是再正常不過的事情了。面試

文章的內容圍繞着不可變 這個重點展開:正則表達式

  1. 分析String對象的不可變性;編程

  2. 常量池的存儲原理以及intern方法的原理api

  3. 字符串拼接的原理以及優化安全

  4. 代碼單元與代碼點的區別網絡

  5. 總結

那麼,咱們開始吧~

不可變性

理解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";

s1s2引用的是同個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

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

image.png

永生代有一個很嚴重的缺點:容易發生OOM 。永生代是有內存上限的,且很小,當程序大量調用intern方法時很容易就發生OOM。在JDK7時將常量池遷移出了永生代,改在堆區中實現,jdk8之後使用了本地空間實現。jdk7之後常量池的實現使得在常量池中建立對象能夠進行淺拷貝,也就是不須要把整個對象複製過去,而只須要複製對象的引用便可,避免重複建立對象,以下圖:

image.png

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方法的文章。

  • 感謝網絡其餘博客的貢獻。

全文到此,原創不易,以爲有幫助能夠點贊收藏評論轉發。 筆者才疏學淺,有任何想法歡迎評論區交流指正。 如需轉載請評論區或私信交流。

另外歡迎光臨筆者的我的博客:傳送門

相關文章
相關標籤/搜索