1. 程式人生 > 資訊 >中興 Axon 40 屏下攝像頭真機亮相:邊框極窄,真全面屏

中興 Axon 40 屏下攝像頭真機亮相:邊框極窄,真全面屏

前言

很高興遇見你~

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

文章的內容圍繞著不可變這個重點展開:

分析String物件的不可變性;常量池的儲存原理以及intern方法的原理字串拼接的原理以及優化程式碼單元與程式碼點的區別總結那麼,我們開始吧~

不可變性

理解String的不可變性,我們可以簡單看幾行程式碼:

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程式設計思想》一書中還提到另一個觀點。我們先看下面的程式碼:

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()方法時,會在常量池中建立一個物件,如下圖所示:

還記得我們文章開頭的問題嗎?

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以前,方法區是存放在永生代記憶體區域中,與堆區是分割開的,那麼當往常量池中建立物件時,就需要進行深拷貝,也就是把一個物件完整地複製一遍並建立新的物件,如下圖:

永生代有一個很嚴重的缺點:容易發生OOM。永生代是有記憶體上限的,且很小,當程式大量呼叫intern方法時很容易就發生OOM。在JDK7時將常量池遷移出了永生代,改在堆區中實現,jdk8以後使用了本地空間實現。jdk7以後常量池的實現使得在常量池中建立物件可以進行淺拷貝,也就是不需要把整個物件複製過去,而只需要複製物件的引用即可,避免重複建立物件,如下圖:

觀察這個程式碼:

在jdk6以前建立的是兩個不同的物件,輸出為false;而jdk7以後常量池中並不會建立新的物件,引用的是同個物件,所以輸出是true。

jdk6之前使用intern建立物件使用的深拷貝,而在jdk7之後使用的是淺拷貝,得以重複利用堆區中的String物件。

通過上面的分析,String真正重複利用字串是在使用雙引號直接建立字串時。使用intern方法雖然可以返回常量池中的字串引用,但是本身已經需要堆區中的一個String物件。因而我們可以得出結論:

儘量使用雙引號顯式構建字串;如果一個字串需要頻繁被重複利用,可以呼叫intern方法將他存放到常量池中。

字串拼接

字串操作最多的莫過於字串拼接了,由於String物件的不可變性,如果每次拼接都需要建立新的字串物件就太影響效能了。因此,官方推出了兩個類: StringBuffer、StringBuilder 。這兩個類可以在不建立新的String物件的前提下拼裝字串、修改字串。如下程式碼:

拼接、插入、刪除都可以很快速地完成。因此,使用StringBuilder進行修改、拼接等操作來初始化字串是更加高效率的做法。StringBuffer和StringBuilder的介面一致,但StringBuffer對操作方法都加上了synchronize關鍵字,保證執行緒安全的同時,也付出了對應的效能代價。單執行緒環境下更加建議使用StringBuilder。

拼接、修改等操作來初始化字串時使用StringBuilder和StringBuffer可以提高效能;單執行緒環境下使用StringBuilder更加合適。

一般情況下,我們會使用+來連線字串。+在java經過了運算子過載,可以用來拼接字串。編譯器也對+進行了一系列的優化。觀察下面的程式碼:

對於s1字串而言,編譯器會把"ab"+"cd"+"fg"直接優化成"abcdefg" ,與String s1 = "abcdefg"; 是等價的。這種優化也就減少了拼接時產生的消耗。甚至比使用StringBuilder更加高效。s2的拼接編譯器會自動建立一個StringBuilder來構建字串。也就相當於以下程式碼:

那麼這是不是意味著我們可以不需要顯式使用StringBuilder了,反正編譯器都會幫助我們優化?當然不是,觀察下邊的程式碼:

這裡有100次迴圈,則會建立100個StringBuilder物件,這顯然是一個非常錯誤的做法。這時候就需要我們來顯示建立StringBuilder物件了:

只需要構建一個StringBuilder物件,效能就極大地提高了。

String s3 = s2 + object; 字串拼接也是支援直接拼接一個普通的物件,這個時候會呼叫該物件的toString方法返回一個字串來進行拼接。toString方法是Object類的方法,若子類沒有重寫,則會呼叫Object類的toString方法,該方法預設輸出類名+引用地址。這看起來沒有什麼問題,但是有一個大坑:切記不要在toString方法中直接使用+拼接自身 。如下程式碼

這裡直接拼接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相關很重要的內容,有興趣的讀者可自行學習。