String的原理與用法總結
1.字串的概念
字串:java中的字串就是存在常量池(方法區中)並以Unicode編碼的字串集合。
1.1 java中的字串使用Unicode編碼
C中的字串使用ASCII碼,用一個位元組表示一個字元。但一個位元組無法表示全世界那麼多種字元,例如表示漢字就需要用2個字元。
java使用Unicode編碼,使用兩個位元組表示一個字元,無論是字母還是漢字,用java處理就更加方便,跨平臺性好,比C的缺點就是消耗更多的記憶體。
1.2 java中字串存改在常量池中。
java的記憶體分為堆,棧,方法區(包括常量池)。 java中字串存改在常量池中。
方法區中主要存在類結構,靜態變數。方法區又包含常量池,常量池儲存字串常量。
變數:記憶體地址不變,記憶體值可以修改
常量:記憶體值不能改變,只能通過更改引用值來指向另一塊記憶體。java的String類沒有set方法。(事實上可以通過反射修改記憶體)
2.String類的概念
String類是用於字串相關操作的一個類。
類包括成員變數和方法。
(1)String類有一個特殊的成員變數,儲存著常量池中某個字串的記憶體地址,也可以理解為一個指標。
(2)String類有一些方法,如indexOf(),charAt()。String類沒有對字串進行修改的方法。
雖然String類沒有修改字串的方法,但保留字串地址的成員變數是可以修改的,也就是說String類的物件可以指向另外的字串。
3.String類例項化物件的方法
String類例項化物件有兩個方法
3.1 String str= "abc" 建立方式
建立物件的過程
1 首先在常量池中查詢是否存在內容為"abc"字串物件
2 如果不存在則在常量池中建立"abc",並讓str引用該物件
3 如果存在則直接讓str引用該物件
至於"abc"是怎麼儲存,儲存在哪?常量池屬於類資訊的一部分,而類資訊反映到JVM記憶體模型中是對應存在於JVM記憶體模型的方法區,也就是說這個類資訊中的常
3.2 String str= new String("abc")建立方式
建立物件的過程
1 首先在堆中(不是常量池)建立一個指定的物件,並讓str引用指向該物件。
2 在字串常量池中檢視,是否存在內容為"abc"字串物件
3 若存在,則將new出來的字串物件與字串常量池中的物件聯絡起來(即讓那個特殊的成員變數value的指標指向它)
4 若不存在,則在字串常量池中建立一個內容為"abc"的字串物件,並將堆中的物件與之聯絡起來。(有可能此時常量池中的"abc"已經被回收,所以要先建立一個內容
為"abc"的字串物件)
3.3 示例
(1)
String str1 = new String("abc");
String str2 = new String("abc");
String str3 = "abc";
String str4 = "abc";
String str5 = "ab" + "c";
System.out.println(str1 == str2);//false.在堆中建立了兩個不同的例項。雖然例項都指向常量池中的同一字串(成員變數value的地址相同),但例項的地址並不相同。
System.out.println(str3 == str4);//true. JVM建立了兩個引用str3和str4,但只建立了一個物件,而且兩個引用都指向了這個物件。
System.out.println(str1.intern() == str4.intern());//true
System.out.println(str1.intern() == str3);//true
//一個初始為空的字串池,它由類 String 私有地維護。
//當呼叫 intern 方法時,如果池已經包含一個等於此 String 物件的字串(用 equals(Object) 方法確定),則返回池中的字串。否則,將此 String 物件新增到池中,並返回此
//String 物件的引用。
//它遵循以下規則:對於任意兩個字串 s 和 t ,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true 。
System.out.println(str3 == str5);//true
//是因為String str2 = "ab" + "c"會查詢常量池中時候存在內容為"abc"字串物件,如果存在則直接讓str2引用該物件。
//顯然String str1 = "abc"的時候,上面說了,會在常量池中建立"abc"物件,所以str1引用該物件,str2也引用該物件,所以str1==str2
(2)
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
System.out.println(str1 + "," + str2); //bcd, abc
System.out.println(str1==str2); //false
這就是說,賦值的變化導致了類物件引用的變化,str1指向了另外一個新物件!而str2仍舊指向原來的物件。上例中,當我們將str1的值改為"bcd"時,JVM發現在 常量池中沒有存放該值的地址,便開闢了這個地址,並建立了一個新的物件,其字串的值指向這個地址。 事 實上,String類被設計成為不可改變(immutable)的類。如果你要改變其值,可以,但JVM在執行時根據新值悄悄建立了一個新物件,然後將這 個物件的地址返回給原來類的引用。這個建立過程雖說是完全自動進行的,但它畢竟佔用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。
(3)
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
String str3 = str1;
System.out.println(str3); //bcd
String str4 = "bcd";
System.out.println(str1 == str4); //true str3 這個物件的引用直接指向str1所指向的物件(注意,str3並沒有建立新物件)。當str1改完其值後,再建立一個String的引用str4,並指向 因str1修改值而建立的新的物件。可以發現,這回str4也沒有建立新的物件,從而再次實現棧中資料的共享。
(4)
我們再接著看以下的程式碼。
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2); //false 建立了兩個引用。建立了兩個物件。兩個引用分別指向不同的兩個物件。
String str1 = "abc"; String str2 = new String("abc"); System.out.println(str1==str2); //false 建立了兩個引用。建立了兩個物件。兩個引用分別指向不同的兩個物件。 以上兩段程式碼說明,只要是用new()來新建物件的,都會在堆中建立,而且其字串是單獨存值的,即使與棧中的資料相同,也不會與棧中的資料共享。
(5)
String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str6 = "a" + "b";
String str4 = str1 + str2;
String str5 = new String("ab");
System.out.println(str5.equals(str3));
System.out.println(str5 == str3);
System.out.println(str5.intern() == str3);
System.out.println(str5.intern() == str4);// false
結果解釋:
程式碼中的字串拼接符號 + ,會被編譯器過載為StringBuilder的append()方法以提高效能,對於String str4 = str1 + str2; 具體實現大致是這樣
StringBuilder temp = new StringBuilder(); temp.append(str1); temp.append(str2); String str4 = temp.toString();
字面量的字串會在編譯器優化,"a" + "b" 編譯期會直接優化成"ab", 前面已經str3 = "ab";所以引用str6的值在編譯期就已經確定了指向"ab";
str1 + str1的結果無法在編譯期確定(如果你把str1、str1定義為final型別,結果都是true了)
intern的用法:
返回字串物件的規範化表示形式。
一個初始時為空的字串池,它由類 String 私有地維護。
當呼叫 intern 方法時,如果池已經包含一個等於此 String 物件的字串(該物件由 equals(Object) 方法確定),則返回池中的字串。否則,將此 String 物件新增到池中,並且返回此 String 物件的引用。
它遵循對於任何兩個字串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true。
所有字面值字串和字串賦值常量表達式都是內部的。
返回:
一個字串,內容與此字串相同,但它保證來自字串池中。
4. 資料型別包裝類的值不可修改。
不僅僅是String類的值不可修改,所有的資料型別包裝類都不能更改其內部的值。
5. 結論與建議:
(1) 我們在使用諸如String str = "abc";的格式定義類時,總是想當然地認為,我們建立了String類的物件str。擔心陷阱!物件可能並沒有被建立!唯一可以肯定的是,指向 String類的引用被建立了。至於這個引用到底是否指向了一個新的物件,必須根據上下文來考慮,除非你通過new()方法來顯要地建立一個新的物件。因 此,更為準確的說法是,我們建立了一個指向String類的物件的引用變數str,這個物件引用變數指向了某個值為"abc"的String類。清醒地認 識到這一點對排除程式中難以發現的bug是很有幫助的。
(2)使用String str = "abc";的方式,可以在一定程度上提高程式的執行速度,因為JVM會自動根據棧中資料的實際情況來決定是否有必要建立新物件。而對於String str = new String("abc");的程式碼,則一概在堆中建立新物件,而不管其字串值是否相等,是否有必要建立新物件,從而加重了程式的負擔。這個思想應該是 享元模式的思想,但JDK的內部在這裡實現是否應用了這個模式,不得而知。
(3)當比較包裝類裡面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個物件時,用==。
(4)由於String類的immutable性質,當String變數需要經常變換其值時,應該考慮使用StringBuffer類,以提高程式效率
6.這裡有幾個問題(進階版):
(1) String a1 = new String("abc") 在執行時涉及幾個String例項?
兩個,一個是字串字面量"xyz"所對應的、駐留(intern)在一個全域性共享的字串常量池中的例項,另一個是通過new String(String)建立並初始化的、內容與"xyz"相同的例項
(2) String a1 = new String("abc") 涉及使用者宣告的幾個String型別的變數?
一個,就是String s。
(3)String s = null 涉及使用者宣告的幾個String型別的變數?
一個。Java裡變數就是變數,引用型別的變數只是對某個物件例項或者null的引用,不是例項本身。宣告變數的個數跟建立例項的個數沒有必然關係
(4)
- String s1 = "a";
- String s2 = s1.concat("");
- String s3 = null;
- new String(s1);
這段程式碼會涉及3個String型別的變數,
1、s1,指向下面String例項的1
2、s2,指向與s1相同
3、s3,值為null,不指向任何例項
以及3個String例項,
1、"a"字面量對應的駐留的字串常量的String例項
2、""字面量對應的駐留的字串常量的String例項
(String.concat()是個有趣的方法,當發現傳入的引數是空字串時會返回this,所以這裡不會額外建立新的String例項)
3、通過new String(String)建立的新String例項;沒有任何變數指向它。
(5)如下每執行一次建立了幾個例項:
- String s1 = new String("xyz");
- String s2 = new String("xyz");
每執行一次只會新建立2個String例項。
符合規範的JVM實現應該在類載入的過程中建立並駐留一個String例項作為常量來對應"xyz"字面量;具體是在類載入的resolve階段進行的。這個常量是全域性共享的,只在先前尚未有內容相同的字串駐留過的前提下才需要建立新的String例項。
等到真正執行原問題中的程式碼片段時,JVM需要執行的位元組碼類似這樣:
- 0: new #2; //class java/lang/String
- 3: dup
- 4: ldc #3; //String xyz
- 6: invokespecial #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V
- 9: astore_1
這之中出現過多少次new java/lang/String就是建立了多少個String物件。
這裡,ldc指令只是把先前在類載入過程中已經建立好的一個String物件("xyz")的一個引用壓到運算元棧頂而已,並不新建立String物件。
在Java語言裡,“new”表示式是負責建立例項的,其中會呼叫構造器去對例項做初始化;構造器自身的返回值型別是void,並不是“構造器返回了新建立的物件的引用”,而是new表示式的值是新建立的物件的引用。
對應的,在JVM裡,“new”位元組碼指令只負責把例項創建出來(包括分配空間、設定型別、所有欄位設定預設值等工作),並且把指向新建立物件的引用壓到運算元棧頂。此時該引用還不能直接使用,處於未初始化狀態(uninitialized);如果某方法a含有程式碼試圖通過未初始化狀態的引用來呼叫任何例項方法,那麼方法a會通不過JVM的位元組碼校驗,從而被JVM拒絕執行。
能對未初始化狀態的引用做的唯一一種事情就是通過它呼叫例項構造器,在Class檔案層面表現為特殊初始化方法“<init>”。實際呼叫的指令是invokespecial,而在實際呼叫前要把需要的引數按順序壓到運算元棧上。在上面的位元組碼例子中,壓引數的指令包括dup和ldc兩條,分別把隱藏引數(新建立的例項的引用,對於例項構造器來說就是“this”)與顯式宣告的第一個實際引數("xyz"常量的引用)壓到運算元棧上。
在構造器返回之後,新建立的例項的引用就可以正常使用了。
8.引用