Java String 字符串類細節探祕

一. 字符串基本知識要點java

  字符串類型String是Java中最經常使用的引用類型。咱們在使用Java字符串的時候,一般會採用兩種初始化的方式:1. String str = "Hello World"; 2. String str = new String("Hello World"); 這兩種方式均可以將變量初始化爲java字符串類型,經過第一種方式建立的字符串又被稱爲字符串常量。須要注意的是,Java中的String類是一個final類,str指向的字符串對象存儲於堆中,而str自己則是存儲在棧中的一個引用罷了。字符串對象一旦被初始化,則不容許再次被修改。從以下String的定義中咱們能夠驗證以上所述:數據庫

1 public final class String implements java.io.Serializable, Comparable<String>, CharSequence{
2     
3     /** The value is used for character storage. */
4     private final char value[];
5     
6     /** Cache the hash code for the string */
7     private int hash; // Default to 0
8     
9 }

從代碼中咱們發現,String前有final修飾,表示是final類;而其中存儲的字符數組value[],也是由final修飾,代表一旦被賦值,則不容許再次修改。數組

  那麼,使用如上兩種字符串初始化的方式有什麼不一樣呢?咱們能夠經過以下代碼體會:安全

public class EqualTest {
	public static void main(String[] args) {
		String s1 = "Hello";
		String s2 = new String("Hello");
		System.out.println(s1 == s2);
		System.out.println(s1.equals(s2));
	}
}

  程序輸出結果爲false和true。從== 和equals的區別上,咱們通常這樣來總結:==比較的是兩個對象的引用,對象必須如出一轍;equals則比較的是對象的內容,字符串內容一致便返回true。這說明,兩種初始化的方式所構造的字符串對象,內容是一致的(能夠理解爲values數組一致),可是倒是兩個不一樣的對象,分別存儲在內存的不一樣位置。其實,這兩種初始化方式的最大不一樣在於,s1被初始化在字符串常量池中,而s2則存儲在堆中。那麼,什麼是字符串常量池呢?併發

  字符串的分配,和其餘的對象分配同樣,耗費高昂的時間與空間代價。JVM爲了提升性能和減小內存開銷,在實例化字符串常量的時候進行了一些優化。爲了減小在JVM中建立的字符串的數量,字符串類維護了一個字符串池,每當代碼建立字符串常量時,JVM會首先檢查字符串常量池。若是字符串已經存在池中,就返回池中的實例引用。若是字符串不在池中,就會實例化一個字符串常量並放到池中。Java可以進行這樣的優化是由於字符串是不可變的final類型,共享的時候不用擔憂數據衝突(讀寫不衝突,由於不能寫,至關於數據庫中的S鎖,即共享鎖)。在常量池中,任何字符串至多維護一個對象。字符串常量老是指向常量池中的一個對象。經過new操做符建立的字符串對象不指向池中的任何對象,可是能夠經過使用字符串的intern()方法來指向其中的某一個。java.lang.String.intern()返回一個池字符串,就是一個在全局常量池中有了一個入口。若是該字符串之前沒有在全局常量池中,那麼它就會被添加到裏面。app

  Java String類中有不少基本的方法。主要分紅如下兩個部分:性能

  1)和value[]相關的方法:學習

  • int length(); //返回String長度,即value[]數組長度;
  • char charAt(int index); //返回指定位置字符;
  • int indexOf(int ch, int fromIndex); //從fromIndex位置開始,查找ch字符在字符串中首次出現的位置;
  • char[] toCharArray();   //將字符串轉換成一個新的字符數組

  2)和其餘字符串相關的方法:優化

  • int indexOf(String str, int fromIndex); //從fromIndex位置開始,查找str字符串在字符串中首次出現的位置;
  • int lastIndexOf(String str, int fromIndex); //從fromIndex位置開始,反向查找str字符串在字符串中首次出現的位置;
  • boolean contains(String str); //contains內部實現也是調用的indexOf,找不到則返回-1
  • boolean startsWith(String str); //判斷字符串是否以str開頭
  • boolean endsWith(String str); //判斷字符串是否以str結尾
  • String replace(CharSequence target, CharSequence replacement);  //使用replacement替換target
  • String substring(int beginIndex,  int endIndex);  //字符串截取,不傳第二個參數則表示直接截取到字符串末尾
  • String[] split(String regex);  // 以regex做爲分割點進行字符串分割

   另一個值得注意的細節是,String是不可變字符串對象,StringBuilder和StringBuffer是可變字符串對象(其內部的字符數組長度可變),StringBuffer線程安全,StringBuilder非線程安全。關於String的append操做,會在下面結合具體的例子進行解釋。ui

二. 幾個關於String的程序分析

2.1 intern的程序示例

  參看以下程序:

public class StringTest1 {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String s1 = "hello world";
		String s2 = new String("hello world");
		String s3 = s2.intern();
		System.out.println(s1 == s2);
		System.out.println(s1 == s3); 
	}
}

  程序的輸出是false,true。關於==和equals的區別在上面已作了詳細的解釋,因爲s1是分配在字符串常量池中,s2則存儲在堆中,所以兩個對象並非同一個對象,==操做返回false。而intern在JDK 1.7及如下,都是返回一個池字符串,該池字符串和原來的String對象的內容一致。若池中無該常量則添加,如有,則直接返回該常量的引用。所以,s1和s3是一個對象。說白了,在JVM的字符串常量池中,對於每個字符串,只有一個共享的對象。

2.2 經過字節碼進行深刻分析

  當狀況變得複雜的時候,參看以下程序:

public class StringTest2 {
	public static void main(String[] args){
		String baseStr = "base";
		final String baseFinalStr = "base";
		//extend
		String s1 = "baseext";
		String s2 = "base" + "ext";
		String s3 = baseStr + "ext";
		String s4 = baseFinalStr + "ext";
		String s5 = new String("baseext").intern();
		System.out.println(s1 == s2);
		System.out.println(s1 == s3);
		System.out.println(s1 == s4);
		System.out.println(s1 == s5);
	}
}

  這段程序乍一看很是複雜,裏面有final String(final是限制在String對象的引用上,即該引用不能再更改所指向的String對象,String對象自己即是final類型的),還有字符串常量,以及字符串對象,和各個對象之間的「+」操做(「+」操做在下面的程序中詳細解釋)。那麼咱們不由會問,在「+」操做的過程當中,JVM究竟是如何進行對象轉換和操做呢?要想搞清楚這個問題,咱們須要深刻Bytecode一探究竟。使用javap -v XXX.class命令,能夠打印出字節碼文件中的符號表和指令等信息,該段程序的字節碼輸出以下:

  這裏的constant pool指的是JVM內存結構中的運行時常量池,是方法區的一部分(參見周志明 《深刻理解Java虛擬機》),咱們上文提到的字符串常量池只是constant pool的一部分,除此以外,它還主要用來存儲編譯期生成的各類字面量和符號引用。javap -v的輸出主要分爲constant pool和方法體指令兩部分,而指令中的操做數則是常量池中的序號。爲了方便接下來的描述,咱們先對經常使用的JVM字節碼指令作一下說明:

LDC        將int, float或String型常量值從常量池中推送至棧頂;
ASTORE_<N>    Store reference into local variable,將棧頂的引用賦值給第N個局部變量;
ALOAD       將指定的引用類型本地變量推送至棧頂
INVOKE VIRTUAL  調用實例方法
INVOKE SPECIAL  調用超類構造方法等初始化方法
INVOKE STATIC   調用靜態方法
NEW         建立一個對象,並將其引用值壓入棧頂
DUP         複製棧頂數值並將複製值壓入棧頂

  以上指令是須要仔細理解的。使用javap -v進行字節碼的查看和理解可能比較困難,由於你要將 #序號 和 constant pool中的字面量不斷照應已方便理解。Eclipse中提供了Bytecode Outline的插件能夠很方便的查看和理解bytecode。插件的安裝請自行百度,這裏再也不贅述。這裏貼出本段代碼的outline:

 // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0  //
    LINENUMBER 5 L0
    LDC "base"  //將"base"從常量池推送至棧頂
    ASTORE 1    //賦值給baseStr變量
   L1
    LINENUMBER 6 L1
    LDC "base"
    ASTORE 2    //賦值給baseFinalStr變量
   L2
    LINENUMBER 8 L2
    LDC "baseext"
    ASTORE 3    
   L3
    LINENUMBER 9 L3
    LDC "baseext"  //注意,這裏直接將"baseext"賦值給了s2,而沒有進行"+"操做!!!
    ASTORE 4
   L4
    LINENUMBER 10 L4
    NEW java/lang/StringBuilder  //建立StringBuilder對象
    DUP
    ALOAD 1 //將baseStr推送至棧頂
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String; //獲取baseStr的value
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V  //將建立的StringBuilder對象初始化爲上一步得到的value
    LDC "ext"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;  //調用StringBuilder對象的append實例方法
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;  //調用StringBuilder對象的toString實例方法
    ASTORE 5  //將toString的結果賦值給s3
   L5
    LINENUMBER 11 L5
    LDC "baseext"  //s4也是直接賦值
    ASTORE 6
   L6
    LINENUMBER 12 L6
    NEW java/lang/String
    DUP
    LDC "baseext"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/String.intern ()Ljava/lang/String; //調用intern()方法
    ASTORE 7
//===================================如下爲 輸出部分,能夠忽略========================================= .......
  ....... L19 LINENUMBER 17 L19 RETURN L20 //相似於符號表,對應於local variable和變量編號 LOCALVARIABLE args [Ljava/lang/String; L0 L20 0 LOCALVARIABLE baseStr Ljava/lang/String; L1 L20 1 LOCALVARIABLE baseFinalStr Ljava/lang/String; L2 L20 2 LOCALVARIABLE s1 Ljava/lang/String; L3 L20 3 LOCALVARIABLE s2 Ljava/lang/String; L4 L20 4 LOCALVARIABLE s3 Ljava/lang/String; L5 L20 5 LOCALVARIABLE s4 Ljava/lang/String; L6 L20 6 LOCALVARIABLE s5 Ljava/lang/String; L7 L20 7 MAXSTACK = 3 MAXLOCALS = 8

  若是想深刻理解,請逐行理解以上字節碼程序。根據程序的分析,咱們不可貴出輸出結果:true false true true。

  s1和s2,s4,s5都是指向字符串常量池中的同一個字符串常量。s2和s4中的「+」並無起任何做用。String中使用 + 字符串鏈接符進行字符串鏈接時,鏈接操做最開始時若是都是字符串常量,編譯後將盡量多的直接將字符串常量鏈接起來,造成新的字符串常量參與後續鏈接。而s3中的第一個操做數是String對象類型,所以會首先以最左邊的字符串爲參數建立StringBuilder對象,而後依次對右邊進行append操做,最後將StringBuilder對象經過toString()方法轉換成String對象。

  這裏要注意的一點是,對於final字段修飾的字符串常量,編譯期直接進行了常量替換。若是final修飾的不是字符串常量,而是字符串對象,如final String a = new String("baseStr"); 則和沒有final修飾的狀況是同樣的,一樣須要用StringBuilder進行append並toString才能夠。

  咱們再經過一個程序來更深刻的理解字符串常量和「+」操做符。程序以下:

public class AppendTest {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		String a = "aa";
		String b = "bb";
		String c = "xx" + "yy " + a + "zz" + "mm" + b;
		System.out.println(c);
	}
}

  程序輸出天然不用贅述,咱們經過一樣的方法查看Bytecode的outline,輸出以下:

// access flags 0x21
public class com/yelbosh/java/str/AppendTest {

  // compiled from: AppendTest.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/yelbosh/java/str/AppendTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 6 L0
    LDC "aa"
    ASTORE 1
   L1
    LINENUMBER 7 L1
    LDC "bb"
    ASTORE 2
   L2
    LINENUMBER 8 L2
    NEW java/lang/StringBuilder
    DUP
    LDC "xxyy " //直接load的是字符串常量「xxyy 」
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; //以後都是在調用StringBuilder對象的append實例方法
    LDC "zz"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "mm"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3
   L3
    LINENUMBER 9 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 3
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 10 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE a Ljava/lang/String; L1 L5 1
    LOCALVARIABLE b Ljava/lang/String; L2 L5 2
    LOCALVARIABLE c Ljava/lang/String; L3 L5 3
    MAXSTACK = 3
    MAXLOCALS = 4
}

  經過這個程序,更印證了咱們如上的結論。

  經過這個深刻分析,咱們在寫代碼的時候,也要注意使用StringBuilder對象。若是直接在for循環中使用「+」操做符進行字符串對象(常量無所謂)的拼接,那麼實際上在每次循環的時候,都要建立StringBuilder,而後append,再toString出來,所以性能是十分低下的。這個時候,就須要在循環外聲明StringBuilder對象,而後在循環內調用append方法進行拼接。另外要注意的是,StringBuilder是線程不安全的,若是涉及到多個線程同時對StringBuilder的append操做,請使用synchronized或lock確保併發訪問的安全性,或者轉而使用線程安全的StringBuffer。

 

總結:Java String是很是靈活的一個對象,可是隻要把細節搞清楚,問題仍是很簡單的。在實際編碼的過程當中,必定要考慮字符串操做的性能和線程安全問題,這樣才能更好的運用字符串完成本身的業務邏輯。但願這篇博文能對您的學習有些幫助,若是錯誤,請不吝賜教。

相關文章
相關標籤/搜索