簡談Java String

前言

前陣子和同事在吃飯時聊起Java的String,以爲本身以前的筆記寫的略顯零散。故此又從新整理了一下。java

String在Java中算是一個有意思的類型,是final類型,所以不能夠繼承這個類、不能修改這個類。設計模式

兩個小問題

咱們先來看一段代碼:緩存

String s = "Hello";
s = s + " world!";

試問:這兩行代碼執行後,原始的 String 對象中的內容到底變了沒有?安全

答案是沒有。由於 String 被設計成不可變(immutable)類,因此它的全部對象都是不可變對象。在 這段代碼中,s 原先指向一個 String 對象,內容是 "Hello",而後咱們對 s 進行了+操做。這時,s 不指向原來那個對象了, 而指向了另外一個 String 對象,內容爲"Hello world!",原來那個對象還存在於內存之中,只 是 s 這個引用變量再也不指向它了。性能優化

經過上面的說明,咱們很容易導出另外一個結論,若是常常對字符串進行各類各樣的修改,或 者說,不可預見的修改,那麼使用 String 來表明字符串的話會引發很大的內存開銷。由於 String 對象創建以後不能再改變,因此對於每個不一樣的字符串,都須要一個 String 對象來 表示。這時,應該考慮使用 StringBuffer類,它容許修改,而不是每一個不一樣的字符串都要生 成一個新的對象。而且,這兩種類的對象轉換十分容易。app

同時,咱們還能夠知道,若是要使用內容相同的字符串,沒必要每次都 new 一個 String。例 如咱們要在構造器中對一個名叫 s 的 String 引用變量進行初始化,把它設置爲初始值,應當這樣作:性能

public class Demo {
   private String s;
   ...
   public Demo {
     s = "Initial Value";
      }
      ...
      //而非 s = new String("Initial Value");
}

前者每次都會調用構造器,生成新對象,性能低下且內存開銷大,而且沒有意義,由於 String 對象不可改變,因此對於內容相同的字符串,只要一個 String 對象來表示就能夠了。也就 說,屢次調用上面的構造器建立多個對象,他們的 String 類型屬性 s 都指向同一個對象。 上面的結論還基於這樣一個事實:對於字符串常量,若是內容相同,Java 認爲它們表明同 一個 String 對象。而用關鍵字 new 調用構造器,老是會建立一個新的對象,不管內容是否 相同。優化

再請你們看一段代碼:ui

String s = new String("xyz");

問題:建立了幾個 String Object?兩者之間有什麼區別?spa

一個或兩個

  • 」xyz」對應一個對象,這個對象放在字符串常量池,常量」xyz」無論出現多少遍,都是緩衝區中的那一個。New String 每寫一遍,就建立一個新的對象在堆上。
  • 若是之前就用過’xyz’,這句表明就不會 建立」xyz」本身了,直接從字符串常量池拿。

常量池

在Java中,其實有不少常量池相關的概念:

常量池表(constant_pool table)

  • Class文件中存儲全部常量(包括字符串)的table
  • 這是Class文件中的內容,還不是運行時的內容,不要理解它是個池子,其實就是Class文件中的字節碼指令

運行時常量池(Runtime Constant Pool)

  • JVM內存中方法區的一部分,這是運行時的內容
  • 這部份內容(絕大部分)是隨着JVM運行時候,從常量池轉化而來,每一個Class對應一個運行時常量池
  • 前面說的絕大部分是由於:除了 Class中常量池內容,還可能包括動態生成並加入這裏的內容

字符串常量池(String Pool)

  • 這部分也在方法區中,但與Runtime Constant Pool不是一個概念,String Pool是JVM實例全局共享的,全局只有一個
  • JVM規範要求進入這裏的String實例叫「被駐留的interned string」,各個JVM能夠有不一樣的實現,HotSpot是設置了一個哈希表StringTable來引用堆中的字符串實例,被引用就是被駐留。

相似這種常量池的思想即涉及到了一個設計模式——享元模式。顧名思義,共享元素。

也就是說:一個系統中若是有多處用到了相同的一個元素,那麼咱們應該只存儲一份此元素,而讓全部地方都引用這一個元素

不可變的String

那麼爲何要不可變呢?

  • 主要是爲了安全與效率。

安全

String被許多的Java類庫用來當作參數。例:

  • URL、IP
  • 文件路徑path
  • 反射機制所須要的String參數
  • 等等...

倘若String不是固定不變的,將會引發各類安全隱患。

效率

在前面提到過常量池的享元模式。這樣在拷貝或者建立內容相同對象時,就沒必要複製對象自己,而是隻要引用它便可。這樣的開銷比起copy object是天差地別的。另外,也就只有不可變對象才能使用常量池,由於能夠保證引用同一常量值的多個變量不產生相互影響。

一樣也是因爲String對象的不可變特性,因此String對象能夠自身緩存HashCode。Java中String對象的哈希碼被頻繁地使用, 好比在hashMap 等容器中。字符串不變性保證了hash碼的惟一性,所以能夠放心地進行緩存。這也是一種性能優化手段,意味着沒必要每次都去計算新的哈希碼:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

其餘

String 和 StringBuffer

JAVA 平臺提供了兩個類:String 和 StringBuffer,它們能夠儲存和操做字符串,即包含多個字符的字符數據。這個 String 類提供了數值不可改變的字符串。而這個 StringBuffer 類提供 的字符串進行修改。當你知道字符數據要改變的時候你就可使用 StringBuffer。典型地, 你可使用 StringBuffers 來動態構造字符數據。另外,String 實現了 equals 方法,new String(「abc」).equals(newString(「abc」)的結果爲true,而StringBuffer沒有實現equals方法, 因此,new StringBuffer(「abc」).equals(newStringBuffer(「abc」)的結果爲 false。

接着要舉一個具體的例子來講明,咱們要把1到100的全部數字拼起來,組成一個串。

StringBuffer sbf = new StringBuffer();
 for(int i=0;i<100;i++){
           sbf.append(i);
    }

上面的代碼效率很高,由於只建立了一個 StringBuffer 對象,而下面的代碼效率很低,由於 建立了101個對象。

String str = new String();
   for(int i=0;i<100;i++) {
             str = str + i;
}

在講二者區別時,應把循環的次數搞成10000,而後用 endTime-beginTime 來比較二者執 行的時間差別。

String 覆蓋了 equals 方法和 hashCode 方法,而 StringBuffer沒有覆蓋 equals 方法和 hashCode 方法,因此,將 StringBuffer對象存儲進 Java集合類中時會出現問題

StringBuilder與 StringBuffer

StringBuilder不是線程安全的,可是單線程中中的性能比StringBuffer高。

Demo Code

String對象建立方式

String str1 = "abcd";
  String str2 = new String("abcd");
  System.out.println(str1==str2);//false

這兩種不一樣的建立方法是有差異的:

  • 第一種方式是在常量池中拿對象
  • 第二種方式是直接在堆內存空間建立一個新的對象。只要使用new方法,便會建立新的對象

鏈接表達式 +

  1. 只有使用引號包含文本的方式建立的String對象之間使用「+」鏈接產生的新對象纔會被加入字符串池中。
  2. 對於全部包含new方式新建對象(包括null)的「+」鏈接表達式,它所產生的新對象都不會被加入字符串池中。
String str1 = "str";
String str2 = "ing";

String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false

String str5 = "string";
System.out.println(str3 == str5);//true

鏈接表達式demo1

public static final String str1 = "ab";
public static final String str2 = "cd";

public static void main(String[] args) {
    String s = str1 + str2;  // 將兩個常量用+鏈接對s進行初始化
    String t = "abcd";
    if (s == t) {
        System.out.println("s等於t,它們是同一個對象");
    } else {
        System.out.println("s不等於t,它們不是同一個對象");
    }
}
  • s等於t,它們是同一個對象
  • A和B都是常量,值是固定的,所以s的值也是固定的,它在類被編譯時就已經肯定了。也就是說:String s=A+B; 等同於:String s="ab"+"cd";

鏈接表達式demo2

public static final String str1;
public static final String str2;

static {
    str1 = "ab";
    str2 = "cd";
}

public static void main(String[] args) {
// 將兩個常量用+鏈接對s進行初始化
    String s = str1 + str2;
    String t = "abcd";
    if (s == t) {
        System.out.println("s等於t,它們是同一個對象");
    } else {
        System.out.println("s不等於t,它們不是同一個對象");
    }
}
  • s不等於t,它們不是同一個對象
  • A和B雖然被定義爲常量,可是它們都沒有立刻被賦值。在運算出s的值以前,他們什麼時候被賦值,以及被賦予什麼樣的值,都是個變數。所以A和B在被賦值以前,性質相似於一個變量。那麼s就不能在編譯期被肯定,而只能在運行時被建立了。

intern方法

運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,Java語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的就是String類的intern()方法。

String的intern()方法會查找在常量池中是否存在一份equal相等的字符串,若是有則返回該字符串的引用,若是沒有則添加本身的字符串進入常量池。

public static void main(String[] args) {    
   String s1 = new String("計算機");
   String s2 = s1.intern();
   String s3 = "計算機";
   System.out.println("s1 == s2? " + (s1 == s2));
   System.out.println("s3 == s2? " + (s3 == s2));
}
//s1 == s2? false
//s3 == s2? true

一個較爲豐富的demo

public class Test {
    public static void main(String[] args) {
        String hello = "Hello", lo = "lo";
        System.out.println((hello == "Hello") + " ");
        System.out.println((Other.hello == hello) + " ");
        System.out.println((other.Other.hello == hello) + " ");
        System.out.println((hello == ("Hel" + "lo")) + " ");
        System.out.println((hello == ("Hel" + lo)) + " ");
        System.out.println(hello == ("Hel" + lo).intern());
    }
}
class Other {
    static String hello = "Hello";
}
package other;

public class Other {
    public static String hello = "Hello";
}
//true true true true false true
  • 在同包同類下,引用自同一String對象
  • 在同包不一樣類下,引用自同一String對象
  • 在不一樣包不一樣類下,依然引用自同一String對象
  • 在編譯成.class時可以識別爲同一字符串的,自動優化成常量,引用自同一String對象
  • 在運行時建立的字符串具備獨立的內存地址,因此不引用自同一String對象
相關文章
相關標籤/搜索