一. 字符串基本知識要點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[]相關的方法:學習
2)和其餘字符串相關的方法:優化
另一個值得注意的細節是,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是很是靈活的一個對象,可是隻要把細節搞清楚,問題仍是很簡單的。在實際編碼的過程當中,必定要考慮字符串操做的性能和線程安全問題,這樣才能更好的運用字符串完成本身的業務邏輯。但願這篇博文能對您的學習有些幫助,若是錯誤,請不吝賜教。