揭祕Java中的String

揭祕Java中的String

最近有人問我一個問題:String是什麼?

是什麼?腦子一片混亂。不可變?不是基本數據類型但又像基本數據類型?頓時很多模糊的理解湧入腦海,怎麼都理不清,究其原因,還是對String在JVM裏的實現原理不甚理解。爲了徹底搞清究竟什麼是String,今天特意看了很多相關的資料,終於有所感悟,感悟之餘,記錄於此,與大家共享。(部分內容抄摘自網上)

一、創建String

創建一個String對象,主要有兩種方式:

  1. String s=”Hello world!”;
  2. String s=new String(“Hello world”);

兩種方式雖然都實現了創建一個String對象的功能,但實現的原理卻大不相同。在討論這兩種方法的不同之前,我們先來了解一下JVM裏的常量池概念,對接下來的理解很有幫助。

相信大家都知道,Java程序在運行之前,編譯器首先要把源代碼編譯成字節碼文件(.class文件),然後JVM再解釋執行.class文件。其中,在.class文件中有一個非常重要的項—常量池,我們上面代碼中的”Hello world”字符串被編譯之後,就被存放在class常量池中的字符串常量表中。改用網上一段話:

在Java源代碼中的每一個字面值字符串,都會在編譯成class文件階段,形成標誌號 爲8(CONSTANT_String_info)的常量表 。 當JVM加載 class文件的時候,會爲對應的常量池建立一個內存數據結構,並存放在方法區中。同時JVM會自動爲CONSTANT_String_info常量表中 的字符串常量字面值 在堆中創建 新的String對象(intern字符串對象)。然後把CONSTANT_String_info常量表的入口地址轉變成這個堆中String對象的直接地址(常量池解析)。

源代碼中所有相同字面值的字符串常量只可能建立唯一一個intern字符串對象。另外,我們也可以調用String的intern()方法來使得一個常規字符串對象成爲intern字符串對象。

好了,下面我們來討論一下第一種方法,也是大家非常常用的方法:

String s=”Hello world!”

首先在編譯期,也就是在運行這段指令之前,JVM就已經爲”Hello world”在堆中創建了一個intern字符串,局部變量s存儲的是早已創建好的intern字符串的堆地址。也就是說,不管有幾條String s1=”Hello world”,堆中都只有1個值爲”Hello world”的字符串。

那麼第二種方法呢?

String s=new String(“Hello world”)

同樣在編譯期,JVM也爲”Hello world”在堆中創建了一個intern字符串,然後用這個intern字符串的值來初始化new出來的新String對象,局部變量s實際上存儲的是new出來的堆對象地址。 此時在JVM管理的堆中,有兩個相同字符串值的String對象:一個是intern字符串對象,一個是new新建的字符串對象。

最後,來段代碼做個更形象的比較:

01 //代碼1
02 String sa = "ab";
03 String sb = "cd";
04 String sab=sa+sb;
05 String s="abcd";
06 System.out.println(sab==s); // false
07 //代碼2
08 String sc="ab"+"cd";
09 String sd="abcd";
10 System.out.println(sc==sd); //true

代碼1中局部變量sa,sb存儲的是堆中兩個intern字符串對象的地址。而當執行sa+sb時,JVM首先會在堆中創建一個StringBuilder類,同時用sa指向的intern字符串對象完成初始化,然後調用append方法完成對sb所指向的intern字符串的合併操作,接着調用StringBuilder的toString()方法在堆中創建一個String對象,最後將剛生成的String對象的堆地址存放在局部變量sab中。而局部變量s存儲的是常量池中”abcd”所對應的intern字符串對象的地址。 sab與s地址當然不一樣了。這裏要注意了,代碼1的堆中實際上有五個字符串對象:三個intern字符串對象、一個String對象和一個StringBuilder對象。
代碼2中”ab”+”cd”會直接在編譯期就合併成常量”abcd”, 因此相同字面值常量”abcd”所對應的是同一個拘留字符串對象,自然地址也就相同。

二、String,StringBuffer和StringBuilder

  1. 通過查看源碼可以發現,String中的value[]是常量(final)數組,只能被賦值一次;而StringBuffer中的value[]就是一個很普通的數組,而且可以通過append()方法將新字符串加入value[]末尾。
  2. StringBuffer和StringBuilder的區別是StringBuffer是線程安全的,而後者不是。這是因爲在源代碼中StringBuffer的很多方法都被關鍵字synchronized 修飾了,而StringBuilder沒有。另外,由於String是不可變的,也就是隻讀,自然也是安全的了。
  3. 因爲String對象中的value[]是不能改變的,每一次合併後字符串值都需要創建一個新的String對象來存放。循環1000次自然需要創建1000個String對象和1000個StringBuilder對象,效率低就可想而知了;而StringBuffer/StringBuilder,只需要將自己的value[]數組不停的擴大來存放即可,循環過程中無需在堆中創建任何新的對象,效率自然就高了。

三、總結:

  1. 不停的創建對象是程序低效的一個重要原因。那麼相同的字符串值能否在堆中只創建一個String對象?可以!除了程序中的字符串常量會被JVM自動創建拘留字符串之外,調用String的intern()方法也能做到這一點。當調用intern()時,如果常量池中已經有了當前String的值,那麼返回這個常量指向intern對象的地址。如果沒有,則將String值加入常量池中,並創建一個新的intern字符串對象。
  2. String的種種行爲都來源於它的immutable性. 因爲它是不變的,沒有線程安全問題,可以無限共享,池化當然最節省時間空間; 也因爲它是不變的,用”+”導致N多的新對象生成,才生的效率問題.

四、知識補充:

equals 和 == 的區別

  1. equals 方法(是String類從它的超類Object中繼承的)被用來檢測兩個對象是否相等,即兩個對象的內容是否相等。
  2. ==用於比較引用和比較基本數據類型時具有不同的功能:比較基本數據類型,如果兩個值相同,則結果爲true ; 在比較引用時,如果引用指向內存中的同一對象,結果爲true

堆、棧與常量池:

  1. 棧:存放基本類型的變量數據和對象的引用,但對象本身不存放在棧中,而是存放在堆(new 出來的對象)或者常量池中(字符串常量對象存放在常量池中。)
  2. 堆:存放所有new出來的對象。
  3. 靜態域:存放靜態成員(static定義的)
  4. 常量池:存放字符串常量和基本類型常量(public static final)。

棧和常量池中的對象可以共享,堆中的對象不可以共享。棧中的數據大小和生命週期是可以確定的,當沒有引用指向數據時,這個數據就會消失。堆中的對象的由垃圾回收器負責回收,因此大小和生命週期不需要確定,具有很大的靈活性。
字符串:其對象的引用都是存儲在棧中的,如果是編譯期已經創建好(直接用雙引號定義的)的就存儲在常量池中,如果是運行期(new出來的)才能確定的就存儲在堆中。對於equals相等的字符串,在常量池中永遠只有一份,在堆中有多份。

原創文章,轉載請註明: 轉載自Ryan's note

本文鏈接地址: 揭祕Java中的String

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章