String 解析--建立對象存儲分析

常量池(Constant Pool):指的是在編譯期被肯定,並被保存在已編譯的.class文件中的一些數據。JVM虛擬機爲每一個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序集和,包括直接常量(String,Integer和 Floating point常量)和對其餘類型,字段和方法的符號引用。對於String常量,它的值是在常量池中的。而JVM中的常量池在內存當中是以表的形式存在的, 對於String類型,有一張固定長度的CONSTANT_String_info表用來存儲文字字符串值,注意:該表只存儲文字字符串值,不存儲符號引用。 

一、String s = "abc"; 
建立過程分析:在class文件被JVM裝載到內存中,JVM會建立一塊String Pool(String緩衝池)。當執行String s = 「abc」;時,JVM首先在String Pool中查看是否存在字符串對象「abc」(如何查看呢?用equals()方法判斷),若是已存在該對象,則不用建立新的字符串對象「abc」,而直接使用String Pool中已存在的對象「abc」,而後將引用s指向該對象;若是不存在該對象,則先在String Pool中建立一個新的字符串對象「abc」,而後將引用s指向String Pool中建立的新對象。 

注意:使用「字符串常量」引號建立的字符串對象時,在編譯期就已經肯定將該對象存儲到String Pool中了。所以,String s = 「abc」只會在編譯期,在String Pool中建立一個對象。  

例如: 
1 String s1 = "abc";    
2 String s2 = "abc";    
3 System.out.println(s1 == s2);//true    
 
結果說明:JVM建立了兩個引用str1和str2,但在String Pool中只建立了一個對象,並且兩個引用都指向了同一個對象。 
 
二、String s = new String("abc");
建立過程分析:當執行String s = new String(「abc」);時,JVM首先在String Pool中查看是否存在字符串對象「abc」,若是不存在該對象,則先在String Pool中建立一個新的字符串對象「abc」,而後執行new String(「abc」)構造方法,在Heap裏又建立一個新的字符串對象「abc」(new出來的對象都放在Heap裏面),並將引用s指向Heap中建立的新對象;若是已存在該對象,則不用建立新的字符串對象「abc」,而直接使用String Pool中已存在的對象「abc」, 而後執行new String(「abc」)構造方法,在Heap裏又建立一個新的字符串對象「abc」,並將引用s指向Heap中建立的新對象。 

注意:使用new String(「」)建立的字符串對象時,會在運行期建立新對象存儲到Heap中。所以,new String(「abc」)建立字符串對象時,會建立2個對象,編譯期在String Pool中建立一個,運行時Heap中建立一個。 

這裏使用了
  1. * Initializes a newly created String object so that it
         * represents the same sequence of characters as the argument; in other
         * words, the newly created string is a copy of the argument string. Unless 
         * an explicit copy of original is needed, use of this 
         * constructor is unnecessary since Strings are immutable.
  2. public String(String original)  
翻譯以下:這個構造方法來用來初始化新建立的String對象,使之具備與參數有相同的字符串順序流。換句話說,新建立的字符串對象只是一個參數的拷貝。由於String對象是不可改變的,因此除非有必要顯式地生成參數的副本,不然不必用此構造方法來構造String對象。
這個構造方法,做用:初始化一個新建立的String對象,使其表示一個與參數相同的字符序列;換句話說,新建立的字符串是該參數字符串的副本。 
例如: 
1 String s1 = new String("abc");  
2 String s2 = new String("abc");  
3 System.out.println(s1 == s2);//false  
 
結果說明:只要是用new()來新建對象的,都會在堆(Heap)中建立,並且其字符串是單獨存值的,即便與String Pool中的數據相同,也不會與String Pool中的數據共享。 

例程1: 

 1 String s1 = "abcdef";  
 2 String s2 = "abcdef";  
 3 String s3 = "abc"+"def";//編譯期自動優化爲String s3 = "abcdef";  
 4 System.out.println(s1 == s2);  
 5 System.out.println(s1 == s3);  
 6 System.out.println(s2 == s3);  
 7 
 8 運行結果: 
 9 true 
10 true 
11 true 
 
結果說明:字符串常量生成的字符串對象在String Pool中只有一個拷貝,且它是在編譯期就被肯定了,因此「s1 == s2」;「abc」和「def」都是字符串常量,當一個字符串由多個字符串常量鏈接而成時,它本身也確定是字符串常量(它在編譯期就被解析爲一個字符串對象了,即class文件中就已經存在「abcdef」),因此在字符串生成字符串對象時,s3也是String Pool中「abcdef」的一個引用。故JVM對於字符串常量的"+"號鏈接,在程序編譯期,JVM就將常量字符串的"+"鏈接優化爲鏈接後的值。 

例程2: 
 1 String s1 = "abc";  
 2 String s2 = "def";  
 3 String s3 = "abcdef";  
 4 String s4 = "abc"+"def";  
 5 String s5 = s1 + "def";  
 6 String s6 = "abc"+s2;  
 7 String s7 = s1 + s2;  
 8 System.out.println(s3 == s4);  
 9 System.out.println(s3 == s5);  
10 System.out.println(s3 == s6);  
11 System.out.println(s3 == s7);  
12 
13 運行結果以下: 
14 true 
15 false 
16 false 
17 false 

結果說明:JVM對於有字符串引用存在的字符串"+"鏈接中,而引用的值在程序編譯期是沒法肯定的,即s1 + 「def」沒法被編譯器優化,只有在程序運行期來動態分配並將鏈接後的新地址賦給s5。 

例程3: 
 1 final String s1 = "abc";  
 2 String s2 = "def";  
 3 String s3 = "abcdef";  
 4 String s4 = "abc"+"def";  
 5 String s5 = s1 + "def";  
 6 String s6 = "abc"+s2;  
 7 String s7 = s1 + s2;  
 8 System.out.println(s3 == s4);  
 9 System.out.println(s3 == s5);  
10 System.out.println(s3 == s6);  
11 System.out.println(s3 == s7);  
12 
13 運行結果以下: 
14 true 
15 true 
16 false 
17 false 

 


例程4: 
 1 final String s1 = "abc";  
 2 final String s2 = "def";  
 3 String s3 = "abcdef";  
 4 String s4 = "abc"+"def";  
 5 String s5 = s1 + "def";  
 6 String s6 = "abc"+s2;  
 7 String s7 = s1 + s2;  
 8 System.out.println(s3 == s4);  
 9 System.out.println(s3 == s5);  
10 System.out.println(s3 == s6);  
11 System.out.println(s3 == s7);  
12 
13 運行結果以下: 
14 true 
15 true 
16 true 
17 true 

 

結果說明:例程3和例程4與例程2的區別是,例程3在字符串s1前加了final修飾,例程4在字符串s1和s2前都加了final修飾。對於final修飾的變量,它在編譯時被解析爲常量值的一個本地拷貝存儲到本身的常量池中或嵌入到它的字節碼流中。因此此時的s1 + 「def」和"abc" + "def"效果是同樣的。接着後面兩個含引用的字符串鏈接,JVM會進行相同的處理。故上面程序後面三個的結果爲true。 

例程5: 
 1 public static void main(String args[]){  
 2     String s1 = "abc";  
 3     final String s2 = getDef();  
 4     String s3 = "abcdef";  
 5     String s4 = "abc"+s2;  
 6     String s5 = s1 + s2;  
 7     System.out.println(s3 == s4);  
 8     System.out.println(s3 == s5);  
 9 }  
10 private static String getDef(){  
11     return "def";  
12 }  
13 
14 程序運行結果以下: 
15 false 
16 false 

 

結果說明:JVM對於方法調用給字符串引用賦值的狀況,引用指向字符串的值在編譯期是沒法肯定的,只有在程序運行調用方法後,將方法的返回值「def」和「abc」動態鏈接並分配新地址賦值給s4,因此上述程序的結果都爲false。 

經過以上的例子可知: 
String s = "a" + "b" + "c";  

等價於:
String s = "abc";  

編譯期,直接優化,進行常量鏈接。 

對於: 
String a = "a";  
String b = "b";  
String c = "c";  
String s = a + b + c;  

就不等價於:
String s = "abc";  

最終結果等於: 
StringBuilder builder = new StringBuilder ();  
builder.append(a);  
builder.append(b);  
builder.append(c);  
String s = builder.toString();  

 

去看StringBuilder的toString()方法: 
  1. public String toString() {  
  2. // Create a copy, don't share the array  
  3. return new String(value, 0, count);  
  4. }  

能夠發現是經過new String(..)返回了一個String對象,也就是說在堆中建立了對象。這時候會不會在池中出現"abc"這個對象呢?(question還沒解決) 

生成String s的過程當中,編譯器使用sb執行的過程:建立一個StringBuffer對象,使用append()向此StringBuffer對象直接添加新的字符串(而不是每次製做一個新的副本)。 

對於String c = "c";String s = "a" + "b" + c;,編譯器將會先將"a" + "b"做爲編譯時常量,優化生成成字面常量"ab" ,而後生成一個StringBuilder對象,接着調用兩次 append()方法,即: 
String s = new Builder().append("ab").append(c) .toString(); 

對於String a = "a";String s = a + "b" + "c";,編譯器分析a爲引用變量,後面的"b" + "c"就不會做爲編譯時常量來運算了。至關於執行: 
String s = new Builder().append(a).append("b") .append("c") .toString(); 

對於String b = "b";String s = "a" + b + "c";],這種形式的就沒辦法優化了,直接生成StringBuilder對象,而後調用三次 append()方法,即: 
String s = new Builder().append("a").append(b) .append("c") .toString(); 

接着,咱們再看如下代碼: 
1 String str1 = "abc";//是字符串常量,它在編譯期被肯定,放在常量池中(共享內容值)  
2 //new String()建立的字符串不放入常量池中  
3 String str2 =new String("abc");//不是字符串常量,不在編譯期肯定(不共享內容值)  
4 String str1 = new String("abc");  
5 String str2 = "abc";  
6 System.out.println(str1==str2);  //false  

 

建立了兩個引用。建立了兩個對象。兩個引用分別指向不一樣的兩個對象。 
  1. String str1 = "abc";  
  2. String str2 = new String("abc");  
  3. System.out.println(str1==str2);  //false  

建立了兩個引用。建立了兩個對象。兩個引用分別指向不一樣的兩個對象。 

接下來咱們再來看看intern()方法,它的定義以下: 
  1. public native String intern();   

這是一個本地方法。在調用這個方法時,JAVA虛擬機首先檢查String Pool中是否已經存在與該對象值相等對象存在,若是有則返回字符串池中對象的引用;若是沒有,則先在String Pool中建立一個相同值的String對象,而後再將它的引用返回。 

例程6: 
 1 public class TestString{    
 2     public static void main(String args[]){    
 3         String s1 = new String("abc");//語句1    
 4         String s2 = "abc";//語句2    
 5         String s3 = new String("abc");//語句3    
 6     
 7         System.out.println(s1 == s2);//語句4    
 8         System.out.println(s1 == s3);//語句5    
 9         System.out.println(s2 == s3);//語句6    
10     
11         System.out.println(s1 == s1.intern());//語句7    
12         System.out.println(s2 == s2.intern());//語句8    
13         System.out.println(s1.intern() == s2.intern());//語句9    
14     
15         String hello = "hello";//語句10    
16         String hel = "hel";//語句11    
17         String lo = "lo";//語句12    
18     
19         System.out.println(hello == "hello");//語句13    
20         System.out.println(hello == "hel" + "lo");//語句14    
21         System.out.println(hello == "hel" + lo);//語句15    
22         System.out.println(hello == hel + lo);//語句16    
23     }    
24 }    
問題1:當執行完語句(1)時,在內存裏面生成幾個對象?它們是什麼?在什麼地方? 
--->當執行完語句(1)時,在內存裏面建立了兩個對象,它們的內容分別都是abc,分別在String Pool(常量池)和Heap(堆)裏。
其字符串的建立過程以下:首先在String Pool裏面查找查找是否有 "abc",若是有就直接使用,但這是本程序的第一條語句,故不存在一個對象"abc",因此要在String Pool中生成一個對象"abc",接下來,執行new String("abc")構造方法,new出來的對象都放在Heap裏面。在Heap裏又建立了一個"abc"的對象。這時內存裏就有兩個對象了,一個在String Pool 裏面,一個在Heap裏面。 

問題2:當執行完語句(2)時,在內存裏面一共有幾個對象?它們是什麼?在什麼地方? 
當執行完語句(2)時,在內存裏面一個對象也沒有建立。當咱們定義語句(2)的時候,若是咱們用字符串的常量值(字面值)給s2賦值的話,那麼首先JVM仍是從String Pool裏面去查找有沒有內容爲abc的這樣一個對象存在,咱們發現當咱們執行完語句(1)的時候,StringPool裏面已經存在了內容爲abc的對象,那麼就不會再在String Pool裏面去生成內容爲abc的字符串對象了。而是會使用已經存在String Pool裏面的內容爲abc的字符串對象,而且會將s2這個引用指向String Pool裏面的內容爲abc的字符串對象,s2存放的是String Pool裏面的內容爲abc的字符串對像的地址。也就是說當你使用String s2 = "abc",即便用字符串常量("abc")給定義的引用(str2)賦值的話,那麼它首先是在String Pool裏面去找有沒有內容爲abc的字符串對象存在,若是有的話,就不用建立新的對象,直接引用String Pool裏面已經存在的對象;若是沒有的話,就在 String Pool裏面去建立一個新的對象,接着將引用指向這個新建立的對象。因此,當執行完語句(2)時內存裏面一共有2個對象,它們的內容分別都是abc,在String Pool裏面一個內容abc的對象,在Heap裏面有一個內容爲abc的對象。 

問題3:當執行完語句(3)時,在內存裏面一共有幾個對象?它們是什麼?在什麼地方? 
當執行完語句(3)時,其執行過程是這樣的:它首先在String Pool裏面去查找有沒有內容爲abc的字符串對象存在,發現有這個對象存在,它就不去建立 一個新的對象。接着執行new...,只要在java裏面有關鍵字new存在,無論內容是否相同,都表示它將生成一個新的對象,new多少次,就生成多少個對象,並且新生成的對象都是在Heap裏面,因此它會在Heap裏面生成一個內容爲abc的對象,而且將它的地址賦給了引用s3,s3就指向剛在Heap裏面生成的內容爲abc的對象。因此,當執行完語句(3)時,內存裏面一共有3個對象,其中包含了在String Pool裏面一個內容爲abc的字符串對象和在Heap裏面包含了兩個內容爲abc的字符串對象。 

問題4:當執行完語句(4)(5)(6)後,它們的結果分別是什麼? 
在java裏面,對象用"=="永遠比較的是兩個對象的內存地址,換句話說,是比較"=="左右兩邊的兩個引用是否指向同一個對象。對於java裏面的8種原生數據類型來講,"=="比較的是它們的字面值是否是同樣的;對應用類型來講,比較的是它們的內存地址是否是同樣的。在語句(1)(2)(3)中,因爲s一、s二、s3指向不一樣的對象,它們的內存地址就不同,所以能夠說當執行完語句(4)(5)(6),它們返回的結果都是false。 

問題5:當執行完語句(7)(8)(9)後,它們的結果分別是什麼? 
首先,s1這個對象指向的是堆中第一次new...生成的對象,當調用 intern 方法時,若是String Pool已經包含一個等於此 String 對象的字符串(該對象由equals(Object)方法肯定),則返回指向String Pool中的字符串對象的引用。由於String Pool中有內容爲abc的對象,因此s1.intern()返回的是String Pool中的內容爲abc的字符串對象的內存地址,而s1倒是指向Heap上內容爲abc的字符串對象的引用。於是,兩個引用指向的對象不一樣,因此,s1 == s1.intern() 爲false,即語句(7)結果爲false。 
對於s2.intern(),它仍是會首先檢查String Pool中是否有內容爲abc的對象,發現有,則將String Pool中內容爲abc的對象的地址賦給s2.intern()方法的返回值。由於s2和s2.intern()方法的返回值指向的是同一個對象,因此,s2 == s2.intern()的結果爲true,,即語句(8)結果爲true。 
對於s1.intern(),它首先檢查String Pool中是否有內容爲abc的對象,發現有,則將String Pool中內容爲abc的對象的賦給s1.intern()方法的返回值。對於s2.intern(),首先檢查String Pool中是否有內容爲abc的對象,發現有,則將String Pool中內容爲abc的對象的地址賦給s2.intern()方法的返回值。由於二者返回的地址都指向同一個對象,因此,s1.intern() == s2.intern()的結果爲true,,便是語句(9)結果爲true。 
所以,當執行完語句(7)(8)(9)後,它們的結果分別是false、true、true。 

問題6:當執行完語句(13)(14) (15)(16)後,它們的結果分別是什麼? 
hello == "hello"引用hello指向的對象就是String Pool中的「hello」,即語句(13)的結果爲true。 
hello == "hel" + "lo"當加號兩邊都是常量值時,就會組成一個新的常量值"hello"在String Pool裏面,若是String Pool已經有相同內容的就不會再建立,則直接返回String Pool裏面的內容爲"hello"的字符串對象的內存地址,因此,hello == "hel" + "lo"結果爲true。 
hello =="hel" + lo 當加號兩邊有一個不是常量值,會在堆裏面建立一個新的"hello"對象,一個在String Pool中,一個在Heap中,故輸出false 。 
hel + lo 同上,輸出false。 
所以,當執行完語句(7)(8)(9)後,它們的結果分別是true、true、false、false。 

例程7: 
 1 String s1 = "abc";  
 2 String s2 = new String("abc");  
 3 String s3 = new String("abc");  
 4 s2.intern();//雖然執行了s2.intern(),但它的返回值沒有賦給s2  
 5 s3 = s3.intern();//把String Pool中「abc」的引用賦給s3  
 6 System.out.println(s1 == s2);  
 7 System.out.println(s1 == s2.intern());    
 8 System.out.println(s1 == s3);  
 9 
10 運行結果以下: 
11 false 
12 true 
13 true 

 

結果說明:由於s2.intern()只是執行了,而沒有把String Pool中的「abc」的地址賦給s2,因此s1仍是指向String Pool中的「abc」,s2則指向Heap中的「abc」,對於s1 == s2,返回值爲false;對於s1 == s2.intern(),其中s2.intern()的返回值爲指向String Pool中「abc」的地址,故s2.intern()與s1指向同一個對象,於是返回true;由於s3 = s3.intern()把String Pool中的「abc」的地址賦給了s3,此時s1和s3指向的是同一對象,即String Pool中的「abc「,於是返回true。 

例程8: 
 1 String s1 = "abc";  
 2 String s2 = new String("abc");  
 3 String s3 = "a" + new String("bc");  
 4 System.out.println(s1 == s2);  
 5 System.out.println(s1 == s3);  
 6 System.out.println(s2 == s3);  
 7 
 8 運行結果以下: 
 9 false 
10 false 
11 false 

 

結果說明: 
s1指向String Pool中的字符串對象「abc」,編譯期肯定; 
s2指向Heap中的字符串對象「abc」,運行期肯定; 
s3指向Heap中的另外一個字符串對象「abc」,運行期肯定。 
注意:String s3 = "a" + new String("bc");等價於 
String s3 = new StringBuilder().append("a").append(new String("bc")).toString(); 

思考:String s = "a" + new String("b") + "c";產生了那幾個對象? 
解:等價於String s = new StringBuilder().append("a").append(new String("b")).append("c").toString(),會在String Pool中產生"a"、"b"、"c"三個對象,在Heap中產生"b"、"abc"兩個個對象。一共5個字符串對象。 

例程9: 
1 String s1 = "Hello";    
2 s1 = "Java";    
3 String s2 = "Hello";    
4 String s3 = new String("Hello");    
5 System.out.println(s1 == s2);    
6 System.out.println(s2 == s3);    
7 運行結果以下: 
8 false 
9 false 
分析這段程序的執行過程:  首先在加載Java程序時,JVM會建立一片的內存空間(String Pool)專門存入string對象。  String s1 = "Hello",如今棧中建立一個字符串引用s1,而後JVM會在String Pool中查找是否存在"Hello",若是存在,則直接使用它,將其地址賦給s1,若是不存在(這時String Pool中顯然不存在"Hello"),則在String Pool中建立"Hello",並將其地址賦給s1。  s1 = "Java",JVM會在String Pool中查找是否存在"Java",若是存在,則直接使用它,將其地址賦給s1,若是不存在(這時String Pool中顯然不存在"Java"),則在String Pool中建立"Java",並將其地址賦給s1。而原來的字符串對象"Hello"仍然在String Pool中,沒有消失,由於String對象的值是不能被修改的。這裏只是改變了引用的值即引用指向的對象的地址,而沒有改變它所引用的對象。  String s2 = "Hello",JVM會在String Pool裏查看有沒有字符串"Hello",如有,則返回它的地址給s2,不然,建立新的String對象"Hello", 放到String Pool裏。這裏因爲"Hello"對象已經建立,並存在於String Pool中,於是不須要從新建立String對象"Hello"。此時s1指向String Pool中的"Java",s2指向String Pool中的"Hello",故s1 == s2的值爲false。  String s3=String("Hello"),JVM會在String Pool裏查看有沒有字符串"Hello",如有,直接執行new操做,若沒有,則先要在String Pool中建立"Hello",而後執行new操做,因爲遇到了new,還會在Heap上(不是String Pool裏)建立string對象"Hello",並將Heap上的"Hello"對象的地址賦給引用s3。因此s2 == s3將返回false,由於s2和s3不是引用同一個對象。 
相關文章
相關標籤/搜索