本人學習筆記,內容來自於閱讀源碼和其餘博客,水平有限,若有錯誤,煩請指正。java
詳情參考:git
Java 7 源碼學習系列(一)——Stringgithub
String 是Java中很是基礎和重要的類,Stirng是典型的Immutable類,即不可變類。(若是一個對象它被構造後其,狀態不能改變,則這個對象被認爲是不可變的(immutable ))。String聲明爲final class,全部屬性也是final,這同時也意味着String是沒法繼承的。緩存
Java語言提供了對字符串鏈接運算符的特別支持(+),+ 號也能夠將其餘類型轉成字符串,經過對象的toString方法實現。因爲String是不可變的,因此String在進行拼接、裁剪等字符串操做時,都會產生新的String對象。安全
Java中還提供了StringBuffer、StringBuilder類,來更好地解決String拼接而產生新對象的問題。網絡
進入java.lang.String下,能夠看到String類以下定義:app
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
ide
能夠清楚的看到,String類被聲明爲final類,且實現了Serializable、Comparable、CharSequence 接口。其中CharSequencetigon 接口中提供了length()、chatAt() 等方法。
private final char value[];(JDK 1.8) private final byte value[];(JDK 1.9)
value[]數組用於存儲String中的字符串內容。是一個被聲明成final的字符數組,在JDK1.9之後,value[]被聲明爲字節數組。由於是被final聲明的,因此String一旦被初始化以後,就容許再改變。
private int hash;
hash 緩存了字符串的hashCode值,默認爲0
private static final long serialVersionUID = -6849794470754667710L; private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
String實現了 Serializable 接口,因此支持序列化和反序列化。
Java的序列化機制是經過在運行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,若是相同就認爲是一致的,能夠進行反序列化,不然就會出現序列化版本不一致的異常(InvalidCastException)。
JDK1.9中新增了一個coder屬性:
private final byte coder;
此屬性爲用於編碼字節的編碼的標識符,分爲 LATIN1 與 UTF16,被虛擬機信任,不可變,不重寫。
String類中包含了許多的構造方法(去除廢棄的有13個),這裏介紹幾個經常使用的構造方法。
String() -- 空構造
public String() { this.value = "".value; }
能夠看到調用空構造時,會建立一個空字符對象。
String(String original) -- 使用字符串建立一個字符串對象
public String(String original) { this.value = original.value; this.hash = original.hash; }
與直接用""雙引號建立字符串不一樣的是,使用new String("")建立字符串時,每一個建立出來的對象都是存儲在堆上的新對象,而使用""雙引號建立出來的字符串從常量池中獲取。因此出現以下代碼中的狀況:
public class TestStringCons{ public static void main(String[] args){ String abc = "abc"; String abc2 = new String("abc"); String abc3 = new String("abc"); String abc4 = "abc"; System.out.println(abc == abc2); // false System.out.println(abc2 == abc3); // false System.out.println(abc == abc4); // true } }
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); } public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); }
傳入字符數組建立時,會用到Arrays.copyOf方法和Arrays.copyOfRange方法。這兩個方法是將原有的字符數組中的內容逐一的複製到String中的字符數組中。
public String(byte bytes[], int offset, int length, Charset charset) { if (charset == null) throw new NullPointerException("charset"); checkBounds(bytes, offset, length); this.value = StringCoding.decode(charset, bytes, offset, length); }
在Java中,String實例中保存有一個char[]字符數組,char[]字符數組是以unicode碼來存儲的,String 和 char 爲內存形式,byte是網絡傳輸或存儲的序列化形式。因此在不少傳輸和存儲的過程當中須要將byte[]數組和String進行相互轉化。因此,String提供了一系列重載的構造方法來將一個字符數組轉化成String,提到byte[]和String之間的相互轉換就不得不關注編碼問題。經過charset來解碼指定的byte數組,將其解碼成unicode的char[]數組,夠形成新的String。
這裏的bytes字節流是使用charset進行編碼的,想要將他轉換成unicode的char[]數組,而又保證不出現亂碼,那就要指定其解碼方式
若是咱們在使用byte[]構造String的時候,使用的是下面這四種構造方法(帶有charsetName或者charset參數)的一種的話,那麼就會使用StringCoding.decode方法進行解碼,使用的解碼的字符集就是咱們指定的charsetName或者charset。 咱們在使用byte[]構造String的時候,若是沒有指明解碼使用的字符集的話,那麼StringCoding的decode方法首先調用系統的默認編碼格式,若是沒有指定編碼格式則默認使用ISO-8859-1編碼格式進行編碼操做。主要體現代碼以下:
static char[] decode(byte[] ba, int off, int len) { String csn = Charset.defaultCharset().name(); try { // use charset name decode() variant which provides caching. return decode(csn, ba, off, len); } catch (UnsupportedEncodingException x) { warnUnsupportedCharset(csn); } try { return decode("ISO-8859-1", ba, off, len); } catch (UnsupportedEncodingException x) { // If this code is hit during VM initialization, MessageUtils is // the only way we will be able to get any kind of error message. MessageUtils.err("ISO-8859-1 charset not available: " * x.toString()); // If we can not find ISO-8859-1 (a required encoding) then things // are seriously wrong with the installation. System.exit(1); return null; } }
在JDK1.9中,這個構造方法和StringCoding.decode方法發生一些改變:
public String(byte bytes[], int offset, int length, Charset charset) { if (charset == null) throw new NullPointerException("charset"); checkBoundsOffCount(offset, length, bytes.length); StringCoding.Result ret = StringCoding.decode(charset, bytes, offset, length); this.value = ret.value; this.coder = ret.coder; }
其中StringCoding.decode在JDK1.9中再也不返回char[]數組,而返回的是 StringCodingde 靜態內部類 Result,再將Result中的value和coder賦給String。
public String(StringBuffer buffer) { synchronized(buffer) { this.value = Arrays.copyOf(buffer.getValue(), buffer.length()); } } public String(StringBuilder builder) { this.value = Arrays.copyOf(builder.getValue(), builder.length()); }
一個特殊的保護(protected)類型的構造方法
String中提供了一個protected修飾的構造器:
String(char[] value, boolean share) { // assert share : "unshared not supported"; this.value = value; }
該方法與String(char[] value)的區別是:第一,多了一個boolean類型的share參數。這個參數方法中並無用到,其實**加入這個boolean參數share是爲了和String(cahr[] value) 這個構造器區分開來。**第二,這個構造器將傳入的字符數組直接賦給了value 。而String(char[] value) 這個構造器將傳入的字符數組使用Arrays.copyOf()方法複製了一份。
使用該構造器的優勢:性能好。不須要複製數組;節約內存,由於共享同一個數組,因此不須要新建數組空間。
之因此這個構造方法被設置爲poretected,若是設置爲public,就有可能破壞String的不可變性。因此,從安全角度來看,這個構造器也是安全的。
String的一些方法也使用了這種"性能好、節約內存、安全的構造器",好比replace、concat、valueOf()以及JDK1.6的substring方法(實際上他們使用的是public String(char[], int, int)方法,原理和本方法相同,已經被本方法取代)。
substring 方法的做用就是提取某個字符串的子串。可是JDK6的substring 可能會致使內存泄露。先看一下JDK1.6 substring 的源碼:
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); //使用的是和父字符串同一個char數組value } // 沒有新差建立對象,仍然使用了原字符串對象 String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count; }
因爲返回回來的子字符串和原有的父字符串是同一個對象,就可能引起內存泄露:
String str = "abcdefghijklmnopqrst"; String sub = str.substring(1, 3) + ""; str = null;
上面代碼中,雖然str = nulln,可是sub依然引用了str所引用的對象,致使str 所指向的對象 "abcdefghijklmnopqrst" 沒法被回收,進而可能致使內存泄露。
爲了改正這個問題,JDK1.7 以後的 substring 方法進行了修改,下面是JDK1.7的 substring 方法源碼:
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException(endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); } public String(char value[], int offset, int count) { if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } if (count < 0) { throw new StringIndexOutOfBoundsException(count); } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); } public static char[] copyOfRange(char[] original, int from, int to) { int newLength = to - from; if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); char[] copy = new char[newLength]; //是建立了一個新的char數組 System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); return copy; }
能夠發現是去爲子字符串建立了一個新的char數組去存儲子字符串中的字符。這樣子字符串和父字符串也就沒有什麼必然的聯繫了,當父字符串的引用失效的時候,GC就會適時的回收父字符串佔用的內存空間。
Java是不支持運算符重載的,String 的 '+' 是 java 中惟一的一個重載運算符。先看下面一段代碼
public class TestA{ public static void main(String[] args){ String str1 = "Hello"; String str2 = str1 + "World"; } }
反編譯上面的代碼:
public class TestA{ public static void main(final String[] array) { new StringBuilder().append("Hello").append("World").toString(); } }
能夠看出,String 中對 '+' 的重載其實就是使用StringBuilder 和 toString() 方法進行處理。
1.int i = 5; 2.String i1 = "" + i; 3.String i2 = String.valueOf(i); 4.String i3 = Integer.toString(i);
第3行和第4行沒有什麼區別,由於String.valueOf(i) 也是調用了 Integer.toString()方法來實現的。
第2行代碼實際上是String i1 = (new StringBuilder()).append(i).toString()。首先建立了一個StringBuilder 對象,在講
intern() 方法有兩個做用:
首先看下面一段代碼:
String str1 = "Hello"; String str2 = new String("Hello"); String str3 = new String("Hello").intern(); System.out.println(str1 == str2); // false System.out.println(str1 == str3); // true
首先須要瞭解幾個關鍵詞:
運行時常量池 JVM 中有幾種常量池:
class文件中的常量池
主要用於存放字面量和符號引用,這部份內容會在類加載以後進入方法區與運行時常量池存放。
方法區中的運行時常量池
運行時常量池除了存放calss文件常量池的內容外,與class常量池不一樣的是,運行時茶涼吃具備動態性,在運行期也可能將新的常量放入池中。
JVM爲了減小JVM中建立的字符串數量,字符串類維護了一個常量池,主要用來存儲編譯期生成的各類字面量和符號引用。
字面量
如文本字符串、聲明爲final 的常量值等;
符號引用
1.類和接口的全限定名;2.字段名稱和描述符;3.方法名稱和描述符。
對於上面的代碼產生的結果,先分析 new String("Hello") 建立對象的過程
首先,編譯期間,符號引用 str1 和字面量 Hello 會被加入到class文件中的常量池中,在類加載以後(具體時間請參考:Java 中new String("字面量") 中 "字面量" 是什麼時候進入字符串常量池的?) 可是並非全部的字面量都會進入字符串常量池,若是字符串已經存在常量池中就不會再加載進來了。
到了運行時期,執行到 new String("Hello")時,會在Java堆中建立一個字符串對象,這個對象所對應的字符串字面量保存在常量池中,可是 符號引用 Str1 指向的是堆中新建立出來的地址。因此會有如下代碼成立:
Stirng s1 = new String("Hello"); Stirng s2 = new String("Hello"); System.out.println(s1 == s2); // false
由於s1,s2是堆上兩個不一樣對象的地址引用,因此s1 == s2 爲false。內存結構圖大體以下圖(草圖)所示:
在不一樣版本的JDK中,Java堆和字符串常量池之間的關係也是不一樣的,這裏爲了方便表述,就畫成兩個獨立的物理區域了。
因此能夠很清楚的看到在執行 new String("Hello") 一共建立了兩個對象,一個是s1所引用的堆空間中的對象,另外一個是在常量池中的對象。
JVM並無規定常量池中的對象必須在編譯期才能放入常量池,運行期也能夠放入常量池,String的intern方法就是利用了這個特色。
再來分析一下一開始的代碼:
String str1 = "Hello"; String str2 = new String("Hello"); String str3 = new String("Hello").intern(); System.out.println(str1 == str2); // false System.out.println(str1 == str3); // true
此時的內存結構應該是這樣的:
分析new String("Hello") 這段代碼,若是後面沒有執行intern()方法,那麼str2,str3都指向的是堆空間中的對象,也就是圖中綠色的那片區域。可是因爲是兩片不一樣的空間,地址不一樣,因此此時 str1 == str2 爲false,並且str2 == str3 也是false。
可是如今 str3 執行了 new String("Hello").intern(),intern()方法會將常量池中的引用返回給str3(由於這裏作了賦值),由於前面的str1 已經在常量池中建立了一個"Hello"字面量,全部str3 接受到的intern()返回的引用與str1 一致,因此str1 == str3 爲 true成立。
在新建字符串對象的時候,咱們通常使用下面兩種方法:
不管是上面兩種哪一種方法,建立字符串對象時都會先檢查常量池中是否有該字面量,沒有的話就會放入常量池。那麼這樣的話,intern()是否是就沒有用了呢?
在前面說 String 對 '+' 重載時說到,String 在使用 '+' 進行字符串拼接時,實質是建立了一個 StringBuilder 對象再調用toString() 方法,可是若是拼接了兩個字符串變量,這種拼接以後產生的新的字符串並不在常量池中。
String s1 = "Hello"; String s2 = "World"; String s3 = s1 + s2; String s4 = "Hello" + "World";
進行反編譯以後
String s1 = "Hello"; String s2 = "World"; String s3 = new StringBuilder().append("Hello").append("World").toString(); String s4 = "HelloWorld";
究其緣由,是由於常量池要保存的是已肯定的字面量值。也就是說,對於字符串的拼接,純字面量和字面量的拼接,會把拼接結果做爲常量保存到字符串池。
若是在字符串拼接中,有一個參數是非字面量,而是一個變量的話,整個拼接操做會被編譯成StringBuilder.append,這種狀況編譯器是沒法知道其肯定值的。只有在運行期才能肯定。
因此只有運行期才能肯定的字符串,就可使用intern()方法放入常量池,減小字符串的重複建立。
前面提到了new String("Hello"),也會把Hello放入常量池中,那new String("Hello").intern() 是否是就多餘了呢。其實否則,intern() 方法有兩個做用,一個是講字符串放入常量池,另外一個是將常量引用返回。
也就是這個值是有返回值的,返回的就是常量的引用。若是將下面的代碼
String str1 = "Hello"; String str2 = new String("Hello"); String str3 = new String("Hello").intern(); System.out.println(str1 == str2); // false System.out.println(str1 == str3); // true
修改成:
String str1 = "Hello"; String str2 = new String("Hello"); String str3 = new String("Hello"); // 使用新的變量接受返回的引用 String str4 = str3.intern(); System.out.println(str1 == str2); // false System.out.println(str1 == str3); // false(這裏true 再也不成立) System.out.println(str1 == str4); // true
不過這種寫法的確沒有什麼意義,可是對於理解intern()方法和常量池是頗有幫助的。