在Java語言了中,全部相似「ABC」的字面值,都是String類的實例;String類位於java.lang包下,是Java語言的核心類,提供了字符串的比較、查找、截取、大小寫轉換等操做;Java語言爲「+」鏈接符(字符串鏈接符)以及對象轉換爲字符串提供了特殊的支持,字符串對象可使用「+」鏈接其餘對象。String類的部分源碼以下:java
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 ... }
從上面能夠看出
1)String類被final關鍵字修飾,意味着String類不能被繼承,而且它的成員方法都默認爲final方法;字符串一旦建立就不能再修改。
2)String類實現了Serializable、CharSequence、 Comparable接口。
3)String實例的值是經過字符數組實現字符串存儲的。面試
1. 「+」鏈接符
1.1 「+」鏈接符的實現原理
1.2 「+」鏈接符的效率
2. 字符串常量池
2.1 內存區域
2.2 存放的內容
3. intern方法
3.1 intern的用法
4. String、StringBuilder和StringBuffer
4.1 繼承結構
4.2 主要區別express
1. 「+」鏈接符
1.1 「+」鏈接符的實現原理
Java語言爲「+」鏈接符以及對象轉換爲字符串提供了特殊的支持,字符串對象可使用「+」鏈接其餘對象。其中字符串鏈接是經過 StringBuilder(或 StringBuffer)類及其append 方法實現的,對象轉換爲字符串是經過 toString 方法實現的,該方法由 Object 類定義,並可被 Java 中的全部類繼承。有關字符鏈接和轉換的更多信息,能夠參閱 Gosling、Joy 和 Steele 合著的 《The Java Language Specification》
咱們能夠經過反編譯驗證一下數組
/** * 測試代碼 */ public class Test { public static void main(String[] args) { int i = 10; String s = "abc"; System.out.println(s + i); } } /** * 反編譯後 */ public class Test { public static void main(String args[]) { //刪除了默認構造函數和字節碼 byte byte0 = 10; String s = "abc"; System.out.println((new StringBuilder()).append(s).append(byte0).toString()); } }
由上能夠看出,Java中使用」+」鏈接字符串對象時,會建立一個StringBuilder()對象,並調用append()方法將數據拼接,最後調用toString()方法返回拼接好的字符串。因爲append()方法的各類重載形式會調用String.valueOf方法,因此咱們能夠認爲:安全
//如下二者是等價的 s = i + "" s = String.valueOf(i); //如下二者也是等價的 s = "abc" + i; s = new StringBuilder("abc").append(i).toString();
1.2 「+」鏈接符的效率
使用「+」鏈接符時,JVM會隱式建立StringBuilder對象,這種方式在大部分狀況下並不會形成效率的損失,不過在進行大量循環拼接字符串時則須要注意。app
String s = "abc"; for (int i=0; i<10000; i++) { s += "abc"; } /** * 反編譯後 */ String s = "abc"; for(int i = 0; i < 1000; i++) { s = (new StringBuilder()).append(s).append("abc").toString(); }
這樣因爲大量StringBuilder建立在堆內存中,確定會形成效率的損失,因此在這種狀況下建議在循環體外建立一個StringBuilder對象調用append()方法手動拼接(如上面例子若是使用手動拼接運行時間將縮小到1/200左右)。dom
/** * 循環中使用StringBuilder代替「+」鏈接符 */ StringBuilder sb = new StringBuilder("abc"); for (int i = 0; i < 1000; i++) { sb.append("abc"); } sb.toString();
與此以外還有一種特殊狀況,也就是當」+」兩端均爲編譯期肯定的字符串常量時,編譯器會進行相應的優化,直接將兩個字符串常量拼接好,例如:jvm
System.out.println("Hello" + "World"); /** * 反編譯後 */ System.out.println("HelloWorld");
/** * 編譯期肯定 * 對於final修飾的變量,它在編譯時被解析爲常量值的一個本地拷貝存儲到本身的常量池中或嵌入到它的字節碼流中。 * 因此此時的"a" + s1和"a" + "b"效果是同樣的。故結果爲true。 */ String s0 = "ab"; final String s1 = "b"; String s2 = "a" + s1; System.out.println((s0 == s2)); //result = true
/** * 編譯期沒法肯定 * 這裏面雖然將s1用final修飾了,可是因爲其賦值是經過方法調用返回的,那麼它的值只能在運行期間肯定 * 所以s0和s2指向的不是同一個對象,故上面程序的結果爲false。 */ String s0 = "ab"; final String s1 = getS1(); String s2 = "a" + s1; System.out.println((s0 == s2)); //result = false public String getS1() { return "b"; }
綜上,「+」鏈接符對於直接相加的字符串常量效率很高,由於在編譯期間便肯定了它的值,也就是說形如」I」+」love」+」java」; 的字符串相加,在編譯期間便被優化成了」Ilovejava」。對於間接相加(即包含字符串引用,且編譯期沒法肯定值的),形如s1+s2+s3; 效率要比直接相加低,由於在編譯器不會對引用變量進行優化。函數
2. 字符串常量池
在Java的內存分配中,總共3種常量池,分別是Class常量池、運行時常量池、字符串常量池。
字符串的分配和其餘對象分配同樣,是須要消耗高昂的時間和空間的,並且字符串使用的很是多。JVM爲了提升性能和減小內存的開銷,在實例化字符串的時候進行了一些優化:使用字符串常量池。每當建立字符串常量時,JVM會首先檢查字符串常量池,若是該字符串已經存在常量池中,那麼就直接返回常量池中的實例引用。若是字符串不存在常量池中,就會實例化該字符串而且將其放到常量池中。因爲String字符串的不可變性,常量池中必定不存在兩個相同的字符串。性能
/** * 字符串常量池中的字符串只存在一份! * 運行結果爲true */ String s1 = "hello world!"; String s2 = "hello world!"; System.out.println(s1 == s2);
2.1 內存區域
在HotSpot VM中字符串常量池是經過一個StringTable類實現的,它是一個Hash表,默認值大小長度是1009;這個StringTable在每一個HotSpot VM的實例中只有一份,被全部的類共享;字符串常量由一個一個字符組成,放在了StringTable上。要注意的是,若是放進String Pool的String很是多,就會形成Hash衝突嚴重,從而致使鏈表會很長,而鏈表長了後直接會形成的影響就是當調用String.intern時性能會大幅降低(由於要一個一個找)。
在JDK6及以前版本,字符串常量池是放在Perm Gen區(也就是方法區)中的,StringTable的長度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,StringTable的長度能夠經過-XX:StringTableSize=66666參數指定。至於JDK7爲何把常量池移動到堆上實現,緣由多是因爲方法區的內存空間過小且不方便擴展,而堆的內存空間比較大且擴展方便。
2.2 存放的內容
在JDK6及以前版本中,String Pool裏放的都是字符串常量;在JDK7.0中,因爲String.intern()發生了改變,所以String Pool中也能夠存放放於堆內的字符串對象的引用。
/** * 運行結果爲true false */ String s1 = "AB"; String s2 = "AB"; String s3 = new String("AB"); System.out.println(s1 == s2); System.out.println(s1 == s3);
因爲常量池中不存在兩個相同的對象,因此s1和s2都是指向JVM字符串常量池中的」AB」對象。new關鍵字必定會產生一個對象,而且這個對象存儲在堆中。因此String s3 = new String(「AB」);產生了兩個對象:保存在棧中的s3和保存堆中的String對象。
當執行String s1 = 「AB」時,JVM首先會去字符串常量池中檢查是否存在」AB」對象,若是不存在,則在字符串常量池中建立」AB」對象,並將」AB」對象的地址返回給s1;若是存在,則不建立任何對象,直接將字符串常量池中」AB」對象的地址返回給s1。
3. intern方法
直接使用雙引號聲明出來的String對象會直接存儲在字符串常量池中,若是不是用雙引號聲明的String對象,可使用String提供的intern方法。intern 方法是一個native方法,intern方法會從字符串常量池中查詢當前字符串是否存在,若是存在,就直接返回當前字符串;若是不存在就會將當前字符串放入常量池中,以後再返回。
JDK1.7的改動:
1. 將String常量池 從 Perm 區移動到了 Java Heap區
2. String.intern() 方法時,若是存在堆中的對象,會直接保存對象的引用,而不會從新建立對象。
/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * 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. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern();
3.1 intern的用法
static final int MAX = 1000 * 10000; static final String[] arr = new String[MAX]; public static void main(String[] args) throws Exception { Integer[] DB_DATA = new Integer[10]; Random random = new Random(10 * 10000); for (int i = 0; i < DB_DATA.length; i++) { DB_DATA[i] = random.nextInt(); } long t = System.currentTimeMillis(); for (int i = 0; i < MAX; i++) { //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])); arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); } System.out.println((System.currentTimeMillis() - t) + "ms"); System.gc(); }
運行的參數是:-Xmx2g -Xms2g -Xmn1500M 上述代碼是一個演示代碼,其中有兩條語句不同,一條是未使用 intern,一條是使用 intern。結果以下圖
未使用intern,耗時826ms:
使用intern,耗時2160ms:
經過上述結果,咱們發現不使用 intern 的代碼生成了1000w 個字符串,佔用了大約640m 空間。 使用了 intern 的代碼生成了1345個字符串,佔用總空間 133k 左右。其實經過觀察程序中只是用到了10個字符串,因此準確計算後應該是正好相差100w 倍。雖然例子有些極端,但確實能準確反應出 intern 使用後產生的巨大空間節省。
細心的同窗會發現使用了 intern 方法後時間上有了一些增加。這是由於程序中每次都是用了 new String 後,而後又進行 intern 操做的耗時時間,這一點若是在內存空間充足的狀況下確實是沒法避免的,但咱們平時使用時,內存空間確定不是無限大的,不使用 intern 佔用空間致使 jvm 垃圾回收的時間是要遠遠大於這點時間的。 畢竟這裏使用了1000w次intern 纔多出來1秒鐘多的時間。
4. String、StringBuilder和StringBuffer
4.1 繼承結構
4.2 主要區別
1)String是不可變字符序列,StringBuilder和StringBuffer是可變字符序列。
2)執行速度StringBuilder > StringBuffer > String。
3)StringBuilder是非線程安全的,StringBuffer是線程安全的。
5. 總結
String類是咱們使用頻率最高的類之一,也是面試官常常考察的題目,下面是一個小測驗。
public static void main(String[] args) { String s1 = "AB"; String s2 = new String("AB"); String s3 = "A"; String s4 = "B"; String s5 = "A" + "B"; String s6 = s3 + s4; System.out.println(s1 == s2); System.out.println(s1 == s5); System.out.println(s1 == s6); System.out.println(s1 == s6.intern()); System.out.println(s2 == s2.intern()); }
解析:真正理解此題目須要清楚如下三點
1)直接使用雙引號聲明出來的String對象會直接存儲在常量池中;
2)String對象的intern方法會獲得字符串對象在常量池中對應的引用,若是常量池中沒有對應的字符串,則該字符串將被添加到常量池中,而後返回常量池中字符串的引用;
3) 字符串的+操做其本質是建立了StringBuilder對象進行append操做,而後將拼接後的StringBuilder對象用toString方法處理成String對象,這一點能夠用javap -c命令得到class文件對應的JVM字節碼指令就能夠看出來。