這裏來對Java中的String對象作一個稍微深刻的瞭解。java
Java對象實現的演進程序員
String對象是Java中使用最頻繁的對象之一,因此Java開發者們也在不斷地對String對象的實現進行優化,以便提高String對象的性能。正則表達式
Java6以及以前版本中String對象的屬性數組
在Java6以及以前版本中,String對象是對char數組進行了封裝實現的對象,其主要有4個成員成員變量,分別是char數組、偏移量offset、字符數量count和哈希值hash。String對象是經過offset和count兩個屬性來定位char[]數組,獲取字符串。這樣作能夠高效、快速地共享數組對象,同時節省內存空間,可是這種方式卻可能會致使內存泄漏的發生。緩存
Java七、8版本中String對象的屬性安全
從Java7版本開始,Java對String類作了一些改變,具體是String類再也不有offset和count兩個變量了。這樣作的好處是String對象佔用的內存稍微少了點,同時String.substring()方法也再也不共享char[]了,從而解決了使用該方法可能致使的內存泄漏問題。性能優化
Java9以及以後版本中String對象的屬性函數
從Java9版本開始,Java將char[]數組改成了byte[]數組。咱們都知道,char是兩個字節的,若是用來存一個字節的狀況下就會形成內存空間的浪費。而爲了節約這一個字節的空間,Java開發者就改爲了一個使用一個字節的byte來存儲字符串。性能
另外,在Java9中,String對象維護了一個新的屬性coder,這個屬性是編碼格式的標識,在計算字符串長度或者調用indexOf()方法的時候,會須要根據這個字段去判斷如何計算字符串長度。coder屬性默認有0和1兩個值,其中0表明Latin-1(單字節編碼),1則表示UTF-16編碼。優化
String對象的建立方式與在內存中的存放
在Java中,對於基本數據類型的變量和對對象的引用,保存在棧內存的局部變量表中;而經過new關鍵字和Constructor建立的對象,則是保存在堆內存中。而String對象的建立方式通常爲兩種,一種是字面量(字符串常量)的方式,一種則是構造函數(String())的方式,兩種方式在內存中的存放有所不一樣。
字面量(字符串常量)的建立方式
使用字面量的方式建立字符串時,JVM會在字符串常量池中先檢查是否存在該字面量,若是存在,則返回該字面量在內存中的引用地址;若是不存在,則在字符串常量池中建立該字面量並返回引用。使用這種方式建立的好處是避免了相同值的字符串在內存中被重複建立,節約了內存,同時這種寫法也會比較簡單易讀一些。
String str = "i like yanggb.";
字符串常量池
這裏要特別說明一下常量池。常量池是JVM爲了減小字符串對象的重複建立,特別維護了一個特殊的內存,這段內存被稱爲字符串常量池或者字符串字面量池。在JDK1.6以及以前的版本中,運行時常量池是在方法區中的。在JDK1.7以及以後版本的JVM,已經將運行時常量池從方法區中移了出來,在Java堆(Heap)中開闢了一塊區域用來存放運行時常量池。而從JDK1.8開始,JVM取消了Java方法區,取而代之的是位於直接內存的元空間(MetaSpace)。總結就是,目前的字符串常量池在堆中。
咱們所知道的幾個String對象的特色都來源於String常量池。
1.在常量池中會共享全部的String對象,所以String對象是不可被修改的,由於一旦被修改,就會致使全部引用此String對象的變量都隨之改變(引用改變),因此String對象是被設計爲不可修改的,後面會對這個不可變的特性作一個深刻的瞭解。
2.String對象拼接字符串的性能較差的說法也是來源於此,由於String對象不可變的特性,每次修改(這裏是拼接)都是返回一個新的字符串對象,而不是再原有的字符串對象上作修改,所以建立新的String對象會消耗較多的性能(開闢另外的內存空間)。
3.由於常量池中建立的String對象是共享的,所以使用雙引號聲明的String對象(字面量)會直接存儲在常量池中,若是該字面量在以前已存在,則是會直接引用已存在的String對象,這一點在上面已經描述過了,這裏再次說起,是爲了特別說明這一作法保證了在常量池中的每一個String對象都是惟一的,也就達到了節約內存的目的。
構造函數(String())的建立方式
使用構造函數的方式建立字符串時,JVM一樣會在字符串常量池中先檢查是否存在該字面量,只是檢查後的狀況會和使用字面量建立的方式有所不一樣。若是存在,則會在堆中另外建立一個String對象,而後在這個String對象的內部引用該字面量,最後返回該String對象在內存地址中的引用;若是不存在,則會先在字符串常量池中建立該字面量,而後再在堆中建立一個String對象,而後再在這個String對象的內部引用該字面量,最後返回該String對象的引用。
String str = new String("i like yanggb.");
這就意味着,只要使用這種方式,構造函數都會另行在堆內存中開闢空間,建立一個新的String對象。具體的理解是,在字符串常量池中不存在對應的字面量的狀況下,new String()會建立兩個對象,一個放入常量池中(字面量),一個放入堆內存中(字符串對象)。
String對象的比較
比較兩個String對象是否相等,一般是有【==】和【equals()】兩個方法。
在基本數據類型中,只可使用【==】,也就是比較他們的值是否相同;而對於對象(包括String)來講,【==】比較的是地址是否相同,【equals()】纔是比較他們內容是否相同;而equals()是Object都擁有的一個函數,自己就要求對內部值進行比較。
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
由於使用字面量方式建立的String對象和使用構造函數方式建立的String對象的內存地址是不一樣的,可是其中的內容倒是相同的,也就致使了上面的結果。
String對象中的intern()方法
咱們都知道,String對象中有不少實用的方法。爲何其餘的方法都不說,這裏要特別說明這個intern()方法呢,由於其中的這個intern()方法最爲特殊。它的特殊性在於,這個方法在業務場景中幾乎用不上,它的存在就是在爲難程序員的,也能夠說是爲了幫助程序員瞭解JVM的內存結構而存在的(?我信你個鬼,你個糟老頭子壞得很)。
/** * 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()方法的官方註釋說明,大概意思就是intern()方法用來返回常量池中的某字符串,若是常量池中已經存在該字符串,則直接返回常量池中該對象的引用。不然,在常量池中加入該對象,而後返回引用。而後咱們能夠從方法簽名上看出intern()方法是一個native方法。
下面經過幾個例子來詳細瞭解下intern()方法的用法。
第一個例子
String str1 = new String("1"); System.out.println(str1 == str1.intern()); // false System.out.println(str1 == "1"); // false
在上面的例子中,intern()方法返回的是常量池中的引用,而str1保存的是堆中對象的引用,所以兩個打印語句的結果都是false。
第二個例子
String str2 = new String("2") + new String("3"); System.out.println(str2 == str2.intern()); // true System.out.println(str2 == "23"); // true
在上面的例子中,str2保存的是堆中一個String對象的引用,這和JVM對【+】的優化有關。實際上,在給str2賦值的第一條語句中,建立了3個對象,分別是在字符串常量池中建立的2和三、還有在堆中建立的字符串對象23。由於字符串常量池中不存在字符串對象23,因此這裏要特別注意:intern()方法在將堆中存在的字符串對象加入常量池的時候採起了一種大相徑庭的處理方案——不是在常量池中創建字面量,而是直接將該String對象自身的引用複製到常量池中,即常量池中保存的是堆中已存在的字符串對象的引用。根據前面的說法,這時候調用intern()方法,就會在字符串常量池中複製出一個對堆中已存在的字符串常量的引用,而後返回對字符串常量池中這個對堆中已存在的字符串常量池的引用的引用(就是那麼繞,你來咬我呀)。這樣,在調用intern()方法結束以後,返回結果的就是對堆中該String對象的引用,這時候使用【==】去比較,返回的結果就是true了。一樣的,常量池中的字面量23也不是真正意義的字面量23了,它真正的身份是堆中的那個String對象23。這樣的話,使用【==】去比較字面量23和str2,結果也就是true了。
第三個例子
String str4 = "45"; String str3 = new String("4") + new String("5"); System.out.println(str3 == str3.intern()); // false System.out.println(str3 == "45"); // false
這個例子乍然看起來好像比前面的例子還要複雜,實際上卻和上面的第一個例子是同樣的,最難理解的反而是第二個例子。
因此這裏就很少說了,而至於爲何還要舉這個例子,我相信聰明的你一會兒就明白了(我有醫保,你來打我呀)。
String對象的不可變性
先來看String對象的一段源碼。
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; }
從類簽名上來看,String類用了final修飾符,這就意味着這個類是不能被繼承的,這是決定String對象不可變特性的第一點。從類中的數組char[] value來看,這個類成員變量被private和final修飾符修飾,這就意味着其數值一旦被初始化以後就不能再被更改了,這是決定String對象不可變特性的第二點。
Java開發者爲何要將String對象設置爲不可變的,主要能夠從如下三個方面去考慮:
1.安全性。假設String對象是可變的,那麼String對象將可能被惡意修改。
2.惟一性。這個作法能夠保證hash屬性值不會頻繁變動,也就確保了惟一性,使得相似HashMap的容器才能實現相應的key-value緩存功能。
3.功能性。能夠實現字符串常量池(到底是先有設計,仍是先有實現呢)。
String對象的優化
字符串是經常使用的Java類型之一,因此對字符串的操做是避免不了的。而在對字符串的操做過程當中,若是使用不當的話,性能可能會有天差地別,因此有一些地方是要注意一下的。
拼接字符串的性能優化
字符串的拼接是對字符串的操做中最頻繁的一個使用。因爲咱們都知道了String對象的不可變性,因此咱們在開發過程當中要儘可能減小使用【+】進行字符串拼接操做。這是由於使用【+】進行字符串拼接,會在獲得最終想要的結果前產生不少無用的對象。
String str = 'i'; str = str + ' '; str = str + 'like'; str = str + ' '; str = str + 'yanggb'; str = str + '.'; System.out.println(str); // i like yanggb.
事實上,若是咱們使用的是比較智能的IDE編寫代碼的話,編譯器是會提示將代碼優化成使用StringBuilder或者StringBuffer對象來優化字符串的拼接性能的,由於StringBuilder和StringBuffer都是可變對象,也就避免了過程當中產生無用的對象了。而這兩種替代方案的區別是,在須要線程安全的狀況下,選用StringBuffer對象,這個對象是支持線程安全的;而在不須要線程安全的狀況下,選用StringBuilder對象,由於StringBuilder對象的性能在這種場景下,要比StringBuffer對象或String對象要好得多。
使用intern()方法優化內存佔用
前面吐槽了intern()方法在實際開發中沒什麼用,這裏又來講使用intern()方法來優化內存佔用了,這人真的是,嘿嘿,真香。關於方法的使用就不說了,上面有詳盡的用法說明,這裏來講說具體的應用場景好了。有一位Twitter的工程師在Qcon全球軟件開發大會上分享了一個他們對String對象優化的案例,他們利用了這個String.intern()方法將之前須要20G內存存儲優化到只須要幾百兆內存。具體就是,使用intern()方法將本來須要建立到堆內存中的String對象都放到常量池中,由於常量池的不重複特性(存在則返回引用),也就避免了大量的重複String對象形成的內存浪費問題。
什麼,要我給intern()方法道歉?不可能。String.intern()方法雖好,可是也是須要結合場景來使用的,並不可以亂用。由於實際上,常量池的實現是相似於一個HashTable的實現方式,而HashTable存儲的數據越大,遍歷的時間複雜度就會增長。這就意味着,若是數據過大的話,整個字符串常量池的負擔就會大大增長,有可能性能不會獲得提高卻反而有所降低。
字符串分割的性能優化
字符串的分割是字符串操做的經常使用操做之一,對於字符串的分割,大部分人使用的都是split()方法,split()方法在大部分場景下接收的參數都是正則表達式,這種分割方式自己沒有什麼問題,可是因爲正則表達式的性能是很是不穩定的,使用不恰當的話可能會引發回溯問題並致使CPU的佔用居高不下。在如下兩種狀況下split()方法不會使用正則表達式:
1.傳入的參數長度爲1,且不包含「.$|()[{^?*+\」regex元字符的狀況下,不會使用正則表達式。
2.傳入的參數長度爲2,第一個字符是反斜槓,而且第二個字符不是ASCII數字或ASCII字母的狀況下,不會使用正則表達式。
因此咱們在字符串分割時,應該慎重使用split()方法,而首先考慮使用String.indexOf()方法來進行字符串分割,在String.indexOf()沒法知足分割要求的時候再使用Split()方法。而在使用split()方法分割字符串時,須要格外注意回溯問題。
總結
雖說在不瞭解String對象的狀況下也能使用String對象進行開發,可是瞭解String對象能夠幫助咱們寫出更好的代碼。
"只但願在故事的最後,我仍是我,你也仍是你。"