String是Java中最經常使用的類,是不可變的(Immutable), 那麼String是如何實現Immutable呢,String爲何要設計成不可變呢?html
關於String,收集一波基礎,來源標明最後,不肯定是否權威, 但願有問題能夠獲得糾正。java
-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m
很難直接從百度出的中文資料中獲得確切的答案,由於大多以訛傳訛,未經驗證。這裏且作測試,先記住,由於很不情願啃官方文檔。面試
首先,要有字符串常量池的概念。而後知道String是怎麼和常量池打交道的。這裏的武器就是intern()
,看一下javadoc:express
/** * 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();
即常量池存在,返回常量池中的那個對象,常量池不存在,則放入常量池,並返回自己。由此推斷兩個公式:緩存
str.intern() == str //證實返回this自己,證實常量池不存在。 str.intern() != str //證實返回常量池中已存在的對象,不等於新建的對象。
面試題雖然被不少牛人說low(請別再拿「String s = new String("xyz");建立了多少個String實例」來面試了吧),但確實常常出現new String以及幾個對象之類的問題。而這個問題主要是考察String的內存模型,連帶能夠引出對Java中對象的內存模型的理解。安全
經過判斷上述兩個公式,咱們能夠知道對象到底是新建的,仍是來自常量池,如此就能夠坦然面對誰等於誰的問題。多線程
0xab
表示"ab"這個對象的地址JDK提供一個可視化內存查看工具jvisualvm
。Mac因爲安裝Java後已經設置了環境變量,因此打開命令行,直接輸入jvisualvm
, 便可打開。Windows下應該是在bin目錄下找到對應的exe文件,雙擊打開。oracle
在Java VisualVM中可使用OQL來查找對象。具體能夠查看Oracle博客。百度出來的結果都是摘抄的[深刻理解Java虛擬機]這本書附錄裏的內容。但我表示用來使用行不通。一些用法不同。簡單的概括一些用的語法。app
查詢一個內容爲RyanMiao
的字符串:工具
select {instance:s} from java.lang.String s where s.toString() == "RyanMiao"
查詢前綴爲Ryan
的字符串:
select {instance:s} from java.lang.String s where s.toString().substring(0,4) =="Ryan"
遍歷
filter( sort( map(heap.objects("java.lang.String"), function(heapString){ if( ! counts[heapString.toString()]){ counts[heapString.toString()] = 1; } else { counts[heapString.toString()] = counts[heapString.toString()] + 1; } return { string:heapString.toString(), count:counts[heapString.toString()]}; }), 'lhs.count < rhs.count'), function(countObject) { if( countObject.string ){ alreadyReturned[countObject.string] = true; return true; } else { return false; } } );
沒找到匹配前綴的作法,這裏使用最笨的遍歷
filter( heap.objects("java.lang.String"), function(str){ if(str != "Ryan" && str !="Miao" && str != "RyanMiao"){ return false; } return true; } );
=
建立字符串經過=
號建立對象,運行時只有一個對象存在。
/** * @author Ryan Miao * 等號賦值,注意字面量的存在 */ @Test public void testNewStr() throws InterruptedException { //str.intern(): 若常量池存在,返回常量池中的對象;若常量池不存在,放入常量池,並返回this。 //=號賦值,若常量池存在,直接返回常量池中的對象0xs1,若是常量池不存在,則放入常量池,常量池中的對象也是0xs1 String s1 = "RyanMiao";//0xs1 Assert.assertTrue(s1.intern() == s1);//0xs1 == 0xs1 > true Thread.sleep(1000*60*60); }
經過Java自帶的工具Java VisualVM來查詢內存中的String實例,能夠看出s1只有一個對象。操做方法以下。
爲了動態查看內存,選擇休眠1h,run testNewStr()
,而後打開jvisualvm, 能夠看到幾個vm列表,找到咱們的vm,右鍵heamp dump.
而後,選擇右側的OQL,在查詢內容編輯框裏輸入:
select {instance:s} from java.lang.String s where s.toString() == "RyanMiao"
能夠發現,只有一個對象。
new
建立字符串經過new建立對象時,參數RyanMiao
做爲字面量會生成一個對象,並存入字符創常量池。然後,new的時候又將建立另外一個String對象,因此,最好不要採用這種方式使用String, 否則就是雙倍消耗內存。
/** * @author Ryan Miao * * 暴露的字面量(literal)也會生成對象,放入Metaspace */ @Test public void testNew(){ //new賦值,直接堆中建立0xs2, 常量池中All literal strings and string-valued constant expressions are interned, // "RyanMiao"自己就是一個字符串,並放入常量池,故intern()返回0xab String s2 = new String("RyanMiao"); Assert.assertFalse(s2.intern() == s2);//0xRyanMiao == 0xs2 > false }
當字符創常量池不存在此對象的的時候,返回自己。
/** * @author Ryan Miao * 上慄中,因爲字面量(literal)會生成對象,並放入常量池,所以能夠直接從常量池中取出(前提是此行代碼運行以前沒有其餘代碼運行,常量池是乾淨的) * * 本次,測試非暴露字面量的str */ @Test public void testConcat(){ //沒有任何字面量爲"RyanMiao"暴露給編譯器,因此常量池沒有建立"RyanMiao",因此,intern返回this String s3 = new StringBuilder("Ryan").append("Miao").toString(); Assert.assertTrue(s3.intern() == s3); }
在Java Visual VM中,查詢以"Ryan"開頭的變量:
select {instance:s} from java.lang.String s where s.toString().substring(0,4) =="Ryan"
但,根據以上幾個例子,能夠明顯看出來,字符串字面量(literal)都是對象,因而上慄中應該有三個對象:Ryan
,Miao
,RyanMiao
。驗證以下:
此時的內存模型:
/** * @author Ryan Miao * 上慄中,只要不暴露咱們最終的字符串,常量池基本不會存在,則每次新建(new)的時候,都會放入常量池,intern並返回自己。即常量池的對象即新建的對象自己。 * * 本次,測試某些常量池已存在的字符串 */ @Test public void testExist(){ //爲毛常量池存在java這個單詞 //s4 == 0xs4, intern發現常量池存在,返回0xexistjava String s4 = new StringBuilder("ja").append("va").toString(); Assert.assertFalse(s4.intern() == s4); //0xexistjava == 0xs4 > false //int也一開始就存在於常量池中了, intern返回0xexistint String s5 = new StringBuilder().append("in").append("t").toString(); Assert.assertFalse(s5.intern()==s5); // 0xexistint == 0xs5 > false //因爲字面量"abc"加載時,已放入常量池,故s6 intern返回0xexistabc, 而s6是新建的0xs6 String a = "abc"; String s6 = new StringBuilder().append("ab").append("c").toString(); Assert.assertFalse(s6.intern() == s6); //0xexistabc == 0xs6 > false }
驗證以下:
使用命令行工具javap -c TestString
能夠反編譯class,看到指令執行的過程。
% javap -c TestString Warning: Binary file TestString contains com.test.java.string.TestString Compiled from "TestString.java" public class com.test.java.string.TestString { public com.test.java.string.TestString(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public void testNewStr() throws java.lang.InterruptedException; Code: 0: ldc #2 // String RyanMiao 2: astore_1 3: aload_1 4: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 7: aload_1 8: if_acmpne 15 11: iconst_1 12: goto 16 15: iconst_0 16: invokestatic #4 // Method org/junit/Assert.assertTrue:(Z)V 19: return public void testNew() throws java.lang.InterruptedException; Code: 0: new #5 // class java/lang/String 3: dup 4: ldc #2 // String RyanMiao 6: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: astore_1 10: aload_1 11: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 14: aload_1 15: if_acmpne 22 18: iconst_1 19: goto 23 22: iconst_0 23: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V 26: return public void testConcat() throws java.lang.InterruptedException; Code: 0: new #8 // class java/lang/StringBuilder 3: dup 4: ldc #9 // String Ryan 6: invokespecial #10 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 9: ldc #11 // String Miao 11: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 17: astore_1 18: aload_1 19: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 22: aload_1 23: if_acmpne 30 26: iconst_1 27: goto 31 30: iconst_0 31: invokestatic #4 // Method org/junit/Assert.assertTrue:(Z)V 34: return public void testExist() throws java.lang.InterruptedException; Code: 0: new #8 // class java/lang/StringBuilder 3: dup 4: ldc #14 // String ja 6: invokespecial #10 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V 9: ldc #15 // String va 11: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 14: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 17: astore_1 18: aload_1 19: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 22: aload_1 23: if_acmpne 30 26: iconst_1 27: goto 31 30: iconst_0 31: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V 34: new #8 // class java/lang/StringBuilder 37: dup 38: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V 41: ldc #17 // String in 43: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 46: ldc #18 // String t 48: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 51: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 54: astore_2 55: aload_2 56: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 59: aload_2 60: if_acmpne 67 63: iconst_1 64: goto 68 67: iconst_0 68: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V 71: ldc #19 // String abc 73: astore_3 74: new #8 // class java/lang/StringBuilder 77: dup 78: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V 81: ldc #20 // String ab 83: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 86: ldc #21 // String c 88: invokevirtual #12 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 91: invokevirtual #13 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 94: astore 4 96: aload 4 98: invokevirtual #3 // Method java/lang/String.intern:()Ljava/lang/String; 101: aload 4 103: if_acmpne 110 106: iconst_1 107: goto 111 110: iconst_0 111: invokestatic #7 // Method org/junit/Assert.assertFalse:(Z)V 114: ldc2_w #22 // long 3600000l 117: invokestatic #24 // Method java/lang/Thread.sleep:(J)V 120: return }
我覺得使用了StringBuilder能夠減小性能損耗啊,然而,編譯後的文件直接說no,直接給替換成拼接了:
Immutable是指String的對象實例生成後就不能夠改變。相反,加入一個user類,你能夠修改name,那麼就不叫作Immutable。因此,String的內部屬性必須是不可修改的。
String的內部很簡單,有兩個私有成員變量:
/** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0
然後並無對外提供能夠修改這兩個屬性的方法,沒有set,沒有build。
String有不少public方法,要想維護這麼多方法下的不可變須要付出代價。每次都將建立新的String對象。好比,這裏講一個頗有迷惑性的concat方法:
public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); }
從方法名上看,是拼接字符串。這樣下意識覺得是原對象修改了內容,因此對於str2 = str.concat("abc")
,會認爲是str2==str
。而後熟記String不可變定律的你確定會反對。確實不是原對象,確實new
了新String。一樣的道理,在其餘String的public方法裏,都將new一個新的String。所以就保證了原對象的不可變。說到這裏,下面的結果是什麼?
String str2 = str.concat(""); Assert.assertFalse(str2 == str);
按照String不可變的特性來理解,這裏str2應該是生成的新對象,那麼確定不等於str.因此是對的,是false。面試考這種題目也是醉了,爲了考驗你們對String API的熟悉程度嗎?看源碼才知道,當拼接的內容爲空的時候直接返回原對象。所以,str2==str是true。
因爲String被聲明式final的,則咱們不能夠繼承String,所以就不能經過繼承來複寫一些關於hashcode和value的方法。
一下內容來自http://www.kogonuso.com/2015/... 發現百度的中文版本基本也是此文的翻譯版。
String是不可變的。由於String會被String pool緩存。由於緩存String字面量要在多個線程之間共享,一個客戶端的行爲會影響其餘全部的客戶端,因此會產生風險。若是其中一個客戶端修改了內容"Test"爲「TEST」, 其餘客戶端也會獲得這個結果,但顯然並想要這個結果。由於緩存字符串對性能來講相當重要,所以爲了移除這種風險,String被設計成Immutable。
HashMap在Java裏過重要了,而它的key一般是String類型的。若是String是mutable,那麼修改屬性後,其hashcode也將改變。這樣致使在HashMap中找不到原來的value。
string的subString方法以下:
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }
若是String是可變的,即修改String的內容後,地址不變。那麼當多個線程同時修改的時候,value的length是不肯定的,形成不安全因素,沒法獲得正確的截取結果。而爲了保證順序正確,須要加synchronzied
,但這會獲得不可思議的性能問題。
這和上條中HashMap的須要同樣,不可變的好處就是hashcode不會變,能夠緩存而不用計算。
The absolutely most important reason that String is immutable is that it is used by the class loading mechanism, and thus have profound and fundamental security aspects. Had String been mutable, a request to load "java.io.Writer" could have been changed to load "mil.vogoon.DiskErasingWriter"
String會在加載class的時候須要,若是String可變,那麼可能會修改加載中的類。
總之,安全性和String字符串常量池緩存是String被設計成不可變的主要緣由。