Java 由淺及深之 String 對象的創建及堆、棧的解釋


參考文章

 http://www.cnblogs.com/dolphin0520/p/3778589.html (探秘Java中String、StringBuilder以及StringBuffer)

 

 String str=new String("abc");

  緊接着這段代碼之后的往往是這個問題,那就是這行代碼究竟創建了幾個String對象呢?相信大家對這道題並不陌生,答案也是眾所周知的,2個。接下來我們就從這道題展開,一起回顧一下與創建String對象相關的一些JAVA知識。

  我們可以把上面這行代碼分成String str、=、"abc"和new String()四部分來看待。String str只是定義了一個名為str的String類型的變量,因此它並沒有創建對象;=是對變量str進行初始化,將某個對象的引用(或者叫句柄)賦值給它,顯然也沒有創建對象;現在只剩下new String("abc")了。那么,new String("abc")為什么又能被看成"abc"和new String()呢?我們來看一下被我們調用了的String的構造器:

  public String(String original) {

  //other code ...

  }

  大家都知道,我們常用的創建一個類的實例(對象)的方法有以下兩種:

  使用new創建對象

  調用Class類的newInstance方法,利用反射機制創建對象。

  我們正是使用new調用了String類的上面那個構造器方法創建了一個對象,並將它的引用賦值給了str變量。同時我們注意到,被調用的構造器方法接受的參數也是一個String對象,這個對象正是"abc"。由此我們又要引入另外一種創建String對象的方式的討論——引號內包含文本

  這種方式是String特有的,並且它與new的方式存在很大區別。

  String str="abc";

  毫無疑問,這行代碼創建了一個String對象。

  String a="abc";

  String b="abc";

  那這里呢?答案還是一個。

  String a="ab"+"cd";

  再看看這里呢?答案是三個。有點奇怪嗎?說到這里,我們就需要引入對字符串池相關知識的回顧了。

在JAVA虛擬機(JVM)中存在着一個字符串池,其中保存着很多String對象,並且可以被共享使用,因此它提高了效率。由於String類是final的,它的值一經創建就不可改變,因此我們不用擔心String對象共享而帶來程序的混亂。字符串池由String類維護,我們可以調用intern()方法來訪問字符串池。

  我們再回頭看看String a="abc";,這行代碼被執行的時候,JAVA虛擬機首先在字符串池中查找是否已經存在了值為"abc"的這么一個對象,它的判斷依據是String類equals(Object obj)方法的返回值。如果有,則不再創建新的對象,直接返回已存在對象的引用;如果沒有,則先創建這個對象,然后把它加入到字符串池中,再將它的引用返回。因此,我們不難理解前面三個例子中頭兩個例子為什么是這個答案了。

  對於第三個例子:

  String a="ab"+"cd";

  "ab"和"cd"分別創建了一個對象,它們經過“+”連接后又創建了一個對象"abcd",因此一共三個,並且它們都被保存在字符串池里了。


 

  現在問題又來了,是不是所有經過“+”連接后得到的字符串都會被添加到字符串池中呢?我們都知道“==”可以用來比較兩個變量,它有以下兩種情況:

  1) 如果比較的是兩個基本類型(char,byte,short,int,long,float,double,boolean),則是判斷它們的值是否相等。

  2) 如果表較的是兩個對象變量,則是判斷它們的引用是否指向同一個對象。

  下面我們就用“==”來做幾個測試。為了便於說明,我們把指向字符串池中已經存在的對象也視為該對象被加入了字符串池:

  

public class StringTest {

  public static void main(String[] args) {

  String a = "ab";// 創建了一個對象,並加入字符串池中

  System.out.println("String a = \"ab\";");

  String b = "cd";// 創建了一個對象,並加入字符串池中

  System.out.println("String b = \"cd\";");

  String c = "abcd";// 創建了一個對象,並加入字符串池中

  String d = "ab" + "cd";

  // 如果d和c指向了同一個對象,則說明d也被加入了字符串池

  if (d == c) {

  System.out.println("\"ab\"+\"cd\" 創建的對象 \"加入了\" 字符串池中");

  }

  // 如果d和c沒有指向了同一個對象,則說明d沒有被加入字符串池

  else {

  System.out.println("\"ab\"+\"cd\" 創建的對象 \"沒加入\" 字符串池中");

  }

  String e = a + "cd";

  // 如果e和c指向了同一個對象,則說明e也被加入了字符串池

  if (e == c) {

  System.out.println(" a  +\"cd\" 創建的對象 \"加入了\" 字符串池中");

  }
// 如果e和c沒有指向了同一個對象,則說明e沒有被加入字符串池

  else {

  System.out.println(" a  +\"cd\" 創建的對象 \"沒加入\" 字符串池中");

  }

  String f = "ab" + b;

  // 如果f和c指向了同一個對象,則說明f也被加入了字符串池

  if (f == c) {

  System.out.println("\"ab\"+ b   創建的對象 \"加入了\" 字符串池中");

  }

  // 如果f和c沒有指向了同一個對象,則說明f沒有被加入字符串池

  else {

  System.out.println("\"ab\"+ b   創建的對象 \"沒加入\" 字符串池中");

  }

  String g = a + b;

  // 如果g和c指向了同一個對象,則說明g也被加入了字符串池

  if (g == c) {

  System.out.println(" a  + b   創建的對象 \"加入了\" 字符串池中");

  }

  // 如果g和c沒有指向了同一個對象,則說明g沒有被加入字符串池

  else {

  System.out.println(" a  + b   創建的對象 \"沒加入\" 字符串池中");

  }

  }

  }

  運行結果如下:

  String a = "ab";

  String b = "cd";

  "ab"+"cd" 創建的對象 "加入了" 字符串池中

  a  +"cd" 創建的對象 "沒加入" 字符串池中

  "ab"+ b   創建的對象 "沒加入" 字符串池中

  a  + b   創建的對象 "沒加入" 字符串池中

  從上面的結果中我們不難看出,只有使用引號包含文本的方式創建的String對象之間使用“+”連接產生的新對象才會被加入字符串池中。對於所有包含new方式新建對象(包括null)的“+”連接表達式,它所產生的新對象都不會被加入字符串池中,對此我們不再贅述。因此我們提倡大家用引號包含文本的方式來創建String對象以提高效率,實際上這也是我們在編程中常采用的。


  

  接下來我們再來看看intern()方法,它的定義如下:

  public native String intern();

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

  我們來看這段代碼:

  

public class StringInternTest {

  public static void main(String[] args) {

  // 使用char數組來初始化a,避免在a被創建之前字符串池中已經存在了值為"abcd"的對象

  String a = new String(new char[] { 'a', 'b', 'c', 'd' });

  String b = a.intern();

  if (b == a) {

  System.out.println("b被加入了字符串池中,沒有新建對象");

  } else {

  System.out.println("b沒被加入字符串池中,新建了對象");

  }

  }

  }

  運行結果:

  b沒被加入字符串池中,新建了對象

 

  如果String類的intern()方法在沒有找到相同值的對象時,是把當前對象加入字符串池中,然后返回它的引用的話,那么b和a指向的就是同一個對象;否則b指向的對象就是JAVA虛擬機在字符串池中新建的,只是它的值與a相同罷了。上面這段代碼的運行結果恰恰印證了這一點。


 

 

  最后我們再來說說String對象在JAVA虛擬機(JVM)中的存儲,以及字符串池與堆(heap)和棧(stack)的關系。我們首先回顧一下堆和棧的區別:

  棧(stack):主要保存基本類型(或者叫內置類型)(char、byte、short、int、long、float、double、boolean)和引用變量,包裹基本數據類型的字面值引用變量類對象的引用變量,數據可以共享,速度僅次於寄存器(register),快於堆。

  基本類型(primitive types), 共有8種,即int, short, long, byte, float, double, boolean, char(注意,並沒有string的基本類型)。這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱為自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這里並沒有類的存在。如int a = 3; 這里的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由於大小可知,生存期可知(這些字面值固定定義在某個程序塊里面,程序塊退出 后,字段值就消失了),出於追求速度的原因,就存在於棧中。

  棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義: 
  int a = 3; 
  int b = 3; 
  編譯器先處理int a = 3;首先它會在棧中創建一個變量為a的引用,然后查找棧中是否有3這個值,如果沒找到,就將3存放進來,然后將a指向3。接着處理int b = 3;在創建完b的引用變量后,因為在棧中已經有3這個值,便將b直接指向3。這樣,就出現了a與b同時均指向3的情況。 

  這時,如果再令a=4;那么編譯器會重新搜索棧中是否有4值,如果沒有,則將4存放進來,並令a指向4;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。 

  特別注意的是,這種字面值的引用類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那么另一個 對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟着改變的情況。如上例,我們定義完a與 b的值后,再令a=4;那么,b不會等於4,還是等於3。在編譯器內部,遇到a=4;時,它就會重新搜索棧中是否有4的字面值,如果沒有,重新開辟地址存 放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。

 

  堆(heap):用於存儲對象。

  堆內存用來存放由new創建的對象和數組。  
   
  在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。  
   
  在堆中產生了一個數組或對象后,還可以在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量

對於String類型,有一張固定長度的CONSTANT_String_info表用來存儲文字字符串值(字符串常量池)(常量池技術,),該表只存儲文字字符串值,不存儲符號引用。,常量池會儲存在方法區(Method Area),而不是堆中,下面詳細說明:

拘留字符串對象
源代碼中所有相同字面值的字符串常量只可能建立唯一 一個拘留字符串對象。 實際上JVM是通過一個記錄了拘留字符串引用的內部數據結構來維持這一特性的。在Java程序中,可以調用String的intern()方法來使得一個常規字符串對象成為拘留字符串對象。
(1)String s=new String("Hello world"); 編譯成class文件后的指令(在 myeclipse中查看):
事實上,在運行這段指令之前,JVM就已經為"Hello world"在堆中創建了一個拘留字符串( 值得注意的是:如果源程序中還有一個"Hello world"字符串常量,那么他們都對應了同一個堆中的拘留字符串)。然后用這個拘留字符串的值來初始化堆中用new指令創建出來的新的String對象,局部變量s實際上存儲的是new出來的堆對象地址。
(2)String s="Hello world";
這跟(1)中創建指令有很大的不同,此時局部變量s存儲的是早已創建好的拘留字符串的堆地址。
java常量池技術  java中的常量池技術,是為了方便快捷地創建某些對象而出現的,當需要一個對象時,就可以從池中取一個出來(如果池中沒有則創建一個),則在需要重復創建相等變量時節省了很多時間。常量池其實也就是一個內存空間,常量池存在於方法區中。
String類也是java中用得多的類,同樣為了創建String對象的方便,也實現了常量池的技術

 

  總結:

  我們查看String類的源碼就會發現,它有一個value屬性,保存着String對象的值,類型是char[],這也正說明了字符串就是字符的序列。

  當執行String a="abc";時,JAVA虛擬機會在棧中創建三個char型的值'a'、'b'和'c',然后在堆中創建一個String對象,它的值(value)是剛才在棧中創建的三個char型值組成的數組{'a','b','c'},最后這個新創建的String對象會被添加到字符串池中。如果我們接着執行String b=new String("abc");代碼,由於"abc"已經被創建並保存於字符串池中,因此JAVA虛擬機只會在堆中新創建一個String對象,但是它的值(value)是共享前一行代碼執行時在棧中創建的三個char型值值'a'、'b'和'c'。

  所以一定要分清值,對象,引用: 值和引用都是存在棧中的,具有共享性,對象是存在堆中的。

  說到這里,我們對於篇首提出的String str=new String("abc")為什么是創建了兩個對象這個問題就已經相當明了了。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM