Java總結篇系列:Java String


String作為Java中最常用的引用類型,相對來說基本上都比較熟悉,無論在平時的編碼過程中還是在筆試面試中,String都很受到青睞,然而,在使用String過程中,又有較多需要注意的細節之處。

1.String是不可變類。

這句話其實大家都很熟悉了,那么具體什么是不可變類呢?一般認為:當對象一旦創建完成后,在正常情況下,對象的狀態不會因外界的改變而改變(對象的狀態是指對象的屬性,包括屬性的類型及屬性值)。

首先看一個基本的例子:

1 String s = "abc"; 2 System.out.println("s:" + s);  // 輸出s:abc
3 s = "def"; 4 System.out.println("s:" + s);  // 輸出s:def

此時,初看上去,輸出的結果變了,發現s的值發生了變化,那么這與上面的說法——String類是不可變類是否矛盾呢?答案是否定的,因為s只是指向堆內存中的引用,存儲的是對象在堆中的地址,而非對象本身,s本身存儲在棧內存中。

實際上,此時堆內存中依然存在着"abc"和"def"對象。對於"abc"對象本身而言,對象的狀態是沒有發生任何變化的。

那么為什么String類具有不可變性呢,顯然,既然不可變說明String類中肯定沒有提供對外可setters方法。接下來來具體看一下String類的定義。

下面是String類中主要屬性的定義(Java 1.7源碼):

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 }

與之前版本的Java String源碼相比,String類減少了int offset 和 int count的定義。這樣變化的結果主要體現在:

1.避免之前版本的String對象subString時可能引起的內存泄露問題;

2.新版本的subString時間復雜度將有O(1)變為O(n);

具體分析可見文章:

http://www.importnew.com/7656.html

http://www.importnew.com/14105.html

通過上面String類的定義,類名前面用了final class修飾,因此,String類不能被繼承。對於其屬性定義,可以看出,屬性value[]和hash都是被定義成private類型,且由於沒有提供對外的public setters方法,String類屬性不可被改變。

其中,需要重點關注屬性value[],其被final char修飾,因此字符型數組value只會被賦值一次就不可修改。其存儲內容正好是String中的單個字符內容。

 

2.String相關的 +

String中的 + 常用於字符串的連接。此處為了說明清楚這個問題,首先可以安裝Eclipse 查看字節碼插件ByteCode Outline。

在線安裝網址: http://zipeditor.sourceforge.net/update/ DisabledHelp >> install new software >> 輸入網址 >> 選擇 bytecode outline >> ... ... 安裝成功。

Window >> show view >> other >> java >> ByteCode即可在Eclipse下方面板欄中查看。

看下面一個簡單的例子:

 1 class D {  2 
 3     public static void main(String[] args) {  4 
 5         String a = "aa";  6         String b = "bb";  7         String c = "xx" + "yy " + a + "zz" + "mm" + b;  8  System.out.println(c);  9  } 10 }

編譯運行后,點擊ByteCode查看,主要字節碼部分如下:

 1 public static main([Ljava/lang/String;)V  2  L0  3     LINENUMBER 5 L0  4     LDC "aa"
 5     ASTORE 1
 6  L1  7     LINENUMBER 6 L1  8     LDC "bb"
 9     ASTORE 2
10  L2 11     LINENUMBER 7 L2 12     NEW java/lang/StringBuilder 13  DUP 14     LDC "xxyy "
15     INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V 16     ALOAD 1
17     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; 18     LDC "zz"
19     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; 20     LDC "mm"
21     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; 22     ALOAD 2
23     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; 24     INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; 25     ASTORE 3
26  L3 27     LINENUMBER 8 L3 28     GETSTATIC java/lang/System.out : Ljava/io/PrintStream; 29     ALOAD 3
30     INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V 31  L4 32     LINENUMBER 9 L4 33  RETURN 34  L5 35     LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
36     LOCALVARIABLE a Ljava/lang/String; L1 L5 1
37     LOCALVARIABLE b Ljava/lang/String; L2 L5 2
38     LOCALVARIABLE c Ljava/lang/String; L3 L5 3
39     MAXSTACK = 3
40     MAXLOCALS = 4
41 }

顯然,通過字節碼我們可以得出如下幾點結論:

1.String中使用 + 字符串連接符進行字符串連接時,連接操作最開始時如果都是字符串常量,編譯后將盡可能多的直接將字符串常量連接起來,形成新的字符串常量參與后續連接(通過反編譯工具jd-gui也可以方便的直接看出);

2.接下來的字符串連接是從左向右依次進行,對於不同的字符串,首先以最左邊的字符串為參數創建StringBuilder對象,然后依次對右邊進行append操作,最后將StringBuilder對象通過toString()方法轉換成String對象(注意:中間的多個字符串常量不會自動拼接)。

也就是說

String c = "xx" + "yy " + a + "zz" + "mm" + b; 實質上的實現過程是: String c = new StringBuilder("xxyy").append(a).append("zz").append("mm").append(b).toString();

由於得出結論:當使用+進行多個字符串連接時,實際上是產生了一個StringBuilder對象和一個String對象。

 

 3.String中的常用方法

1).與String類中value[]數組存儲直接相關的有:

int length(); // 返回String長度,亦即value[]數組長度;

char charAt(int index); // 返回指定位置字符;

int indexOf(int ch, int fromIndex); //從fromIndex位置開始,查找ch字符在字符串中首次出現的位置。fromIndex默認為0,ch直接傳入字符即可。如'C',區分大小寫,未查找到返回-1;

char[] toCharArray() ;   // 將字符串轉換成一個新的字符數組

2).與其他字符串即字串相關的方法有:

int indexOf(String str, int fromIndex) ;

與indexOf含義相反有lastIndexOf(..),反向索引。

boolean contains(String str); //實際上 contains內部實現也是調用的indexOf,然后將其結果與-1相比較。

boolean startsWith(String str); // 判斷字符串是否以str開頭

boolean endsWith(String str); //.....是否以str結尾

String replace(CharSequence target, CharSequence replacement) ;  // 替換

String substring(int beginIndex,  int endIndex);  //字符串截取,不傳第二個參數則表示直接截取到字符串末尾

String[] split(String regex);  // 字符串分割

 

4.String中的equals()與hashCode()

String類重寫了Object類的equlas方法,使得比較字符串內容是否相等可以直接使用equlas方法。關於equals以及相應的hashCode方法參見博文《Java總結篇系列:java.lang.Object 》

 

5.String字符串常量池

JVM為了提高性能和減少內存開銷,內部維護了一個字符串常量池,每當創建字符串常量時,JVM首先檢查字符串常量池,如果常量池中已經存在,則返回池中的字符串對象引用,否則創建該字符串對象並放入池中。

因此下述結果返回true。

1 String a = "abc"; 2 String b = "abc"; 3 System.out.print(a == b); //true

但與創建字符串常量方式不同的是,當使用new String(String str)方式等創建字符串對象時,不管字符串常量池中是否有與此相同內容的字符串,都會在堆內存中創建新的字符串對象。

因此,下面代碼片段有如下結果。

1 String a = "Hello"; 2 String b = new String("Hello"); 3 System.out.println(a == b);  //false
4 System.out.println(a.equals(b)); //true

即使字符串內容相同,字符串常量池中的字符串與通過new String(..)等方式創建的字符串對象之間沒有直接的關系,但是,可以通過字符串的intern()方法找到此種關聯。intern()方法返回字符串對象在字符串常量池中的對象引用,若字符串常量池中尚未有此字符串,則創建一新的字符串常量放置於池中。

於是,很據如上理解,很自然的,可以得到如下結果。

 1 String a = "Hello";  2 System.out.println(a == a.intern()); //true
 3 
 4 String b = new String("corn");  5 String c = b.intern();  6 
 7 System.out.println(b == c); //false
 8 
 9 String d = "corn"; 10 
11 System.out.println(c == d); //true

 

6.String/StringBuilder/StringBuffer區別

String是不可變字符串對象,StringBuilder和StringBuffer是可變字符串對象(其內部的字符數組長度可變),StringBuffer線程安全,StringBuilder非線程安全。

 

7.既然String是不可變字符串對象,如何才能改變讓其可變?

既然String對象中沒有對外提供可用的public setters等方法,因此只能通過Java中的反射機制實現。因此,前文中說到的String是不可變字符串對象只是針對“正常情況下”。而非必然。

 1 public static void stringReflection() throws Exception {  2 
 3     String s = "Hello World";  4 
 5     System.out.println("s = " + s); //Hello World  6 
 7     //獲取String類中的value字段
 8     Field valueField = String.class.getDeclaredField("value");  9 
10     //改變value屬性的訪問權限
11     valueField.setAccessible(true); 12 
13     char[] value = (char[]) valueField.get(s); 14 
15     //改變value所引用的數組中的第5個字符
16     value[5] = '_'; 17 
18     System.out.println("s = " + s); //Hello_World
19 }

由此可見Java中反射的強大之處。

 


免責聲明!

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



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