[Java]String之尋根問底,string問底
轉載請註明出處:http://blog.csdn.net/tang9140/article/details/43982887
-
-
- 引言
- 一 Immutable特性
- 二 串連符號本質
- 三 相等判斷兩種方式equals說明
- 補充知識點
- 附反射修改String對象代碼
引言
在java編程中,幾乎每天都會跟String打交道,因此,深入理解String及其用法十分有必要。下面分三方面來詳細說明下String相關的特點及用法
- Immutable(不可變)特性
- 串連符號+的本質
- 相等判斷兩種方式(==/equals)說明
一、 Immutable特性
Java設計人員為了方便大家對字串的各種操作,抽象出String類,該類封裝了對字串的尋找、拼接、替換、截取等一系列操作。查看java.lang.String的源碼,首先就能看到如下描述:
The String class represents character strings. All string literals in Java programs, such as “abc”, are implemented as instances of this class.
Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared.
大意是:String類代表著字元序列。Java語言中所有的字串字面量,如“abc”,都實現為String類的執行個體。
String對象都是常量,其值在建立之後就不能改變。字串緩衝區支援可變的字元序列。由於String對象的不可變性,他們可以被共用。
String的immutable體現在兩個方面:
String類被final關鍵字修飾,意味著該類不能被繼承。由於String類不能有子類,保證了String類的靜態和執行個體方法不可能被繼承修改,保證了安全性。
String類內部的私人成員變數,如value(char[]),offset(int),count(int)都被final關鍵字修飾。當String對象建立後,offset跟count欄位的值不可改變(因為是基礎資料型別 (Elementary Data Type)int),value變數不能再指向其它的字元數組(因為其是參考型別變數)
看到這裡,可能有人會說:雖然value屬性不能指向其它的字元數組,但其指向的字元數組內容還是可以改變,如果能夠改變其內容,那不意味著String對象是可變的了。
上面的說法沒錯,但是String類本身沒有提供修改字元數組的方法,除非你用非常規手段(例如反射)去改變私人屬性的值(後面會附上代碼實現)。雖然從代碼上完全是可以改變建立後String對象的各屬性的值(即使屬性被private final修飾),但畢竟是採用反射這種非常規手段。按照正常使用方式,我們是不能改變String對象的值,所以還是認為String對象是不可變的。
注意:這裡說String不變性,是指String對象在建立後其值不能改變。對於參考型別的變數,是可以指向不同的String對象來改變其所代表的值。如
String s = "abc"; s = "def";
參考型別變數s指向值為‘abc’的String對象,然後s又指向了值為‘def’的String對象。雖然s所代表的字串確實改變了,但是對於String對象abc和def並沒有改變,僅僅是s指向了不同的String對象而已。
關於String設計為immutable,至少有兩方面的好處:
由於String對象的不變性,在用+號進行字串串連時,可能會造成效率低下,下面詳細說明下串連符號+的本質是什嗎?底層是如何進行字串拼接的?什麼情況下用+號進行字串串連效率較低?
二、 串連符號+本質
要瞭解+號的本質,先從java編譯說起。眾所周知,java代碼在運行前都需要先編譯成Class檔案(關於Class檔案的結構,由於篇幅有限,這裡不作詳細說明)。在Class檔案中,有一部分是叫屬性工作表集合,其中包括Code屬性,簡單說,Code屬性包含的就是方法體裡面的代碼經過編譯後對應的位元組碼指令。因此,我們可以直接查看Class檔案中的位元組碼指令來瞭解+的本質。範例程式碼如下
public class StringTest { public static void main(String[] args) { String s = "Hello"; s = s + " world!"; }}
由於我們不熟悉Class檔案結構,而且位元組碼非常不容易看懂,在這裡不直接查看編譯產生的StringTest.class檔案的內容,而是通過jad工具反編譯位元組碼查看結果。該工具為http://download.csdn.net/detail/tang9140/8426571(本人的csdn資源,打下廣告非喜勿噴)。在cmd下執行jad命令jad -o -a -sjava StringTest.class成功執行上述命令後,會發現StringTest.class檔案所在目錄下會多出源檔案StringTest.java,內容如下:
public class StringTest{ public StringTest() { // 0 0:aload_0 // 1 1:invokespecial #8 <Method void Object()> // 2 4:return } public static void main(String args[]) { String s = "Hello"; // 0 0:ldc1 #16 <String "Hello"> // 1 2:astore_1 s = (new StringBuilder(String.valueOf(s))).append(" world!").toString(); // 2 3:new #18 <Class StringBuilder> // 3 6:dup // 4 7:aload_1 // 5 8:invokestatic #20 <Method String String.valueOf(Object)> // 6 11:invokespecial #26 <Method void StringBuilder(String)> // 7 14:ldc1 #29 <String " world!"> // 8 16:invokevirtual #31 <Method StringBuilder StringBuilder.append(String)> // 9 19:invokevirtual #35 <Method String StringBuilder.toString()> // 10 22:astore_1 // 11 23:return }}
上述反編譯出的原始碼包含了注釋行,代表與原始碼相對應的位元組碼指令。很顯然,原始碼中並沒有字串串連符+,也就是說,+號在經過編譯後,已經被替換成StringBuilder的append方法調用(實現上在jdk1.5版本之前,+號在編譯器編譯後是替換為StringBuffer的append方法調用)。所謂的+號連接字串,本質上是通過new StringBuilder對象後調用其append方法進行字串拼接。
java通過在編譯階段重載字串操作符+,在方便對字串的操作同時也帶來了一定的副作用,比如由於程式員不清楚+號的本質而編寫出效率低下的代碼,請看如下代碼:
public String concat(){ String result = ""; for (int i = 0; i < 1000; i++) { result += i; } return result; }
在for迴圈體內,出現+號的地方,編譯後都會被替換為如下調用:
result = (new StringBuilder(String.valueOf(result))).append(i).toString();
顯然,每次迴圈都需要在構造StringBuilder對象時對result中的字元數組進行拷貝,而在調用toString方法時,又要拷貝StringBuilder中的字元數組來構建String對象。相當於每次for迴圈,要進行兩次對象建立及兩次字元數組拷貝,因而程式效率低下。更高效的代碼如下:
public String concat(){ StringBuilder result = new StringBuilder(); for (int i = 0; i < 1000; i++) { result.append(i); } return result.toString(); }
至此,我相信大家已經知道了+號的本質,以及如何避免低效的使用+號。下面我們就來深入瞭解下String對象相等判斷的兩種方式(經常出現在java面試題中)。
三、 相等判斷兩種方式(==/equals)說明
- ==:當兩個運算元是基礎資料型別 (Elementary Data Type)時,比較值是否相等;當兩個運算元是參考型別時,比較是否指向同一個對象。
- equals方法用來比較兩個對象的內容是否相等。
由於String是參考型別,當用==判斷時,比較的是兩個String變數是否指向同一個String對象;當用equals方法判斷時,才會比較兩個String對象的內容是否相等。在實際項目中進行字串比較時,基本是比較兩個String對象的內容是否相等,因此建議大家全部使用equals方法進行比較。
用==進行字串比較,經常出現在面試題中,而不是項目代碼中,對於實際工作考核的意義不大,只是作為大家對String瞭解程度的一個考核。下面列舉一些面試題如下:
題1
String a = "a1"; String b = "a" + 1; System.out.println(a == b);
答案:true
說明:當兩個字面量進行串連時,實際上在java編譯器的編譯期,已經進行了字面量的拼接。也就是說編譯產生的Class檔案中並不存在String b = “a” + 1對應的位元組碼指令,已經被最佳化為String b = “a1”對應的位元組碼指令。我想這步最佳化大家應該很能理解,在編譯期間能夠確定結果並進行計算,就能有效減少Class檔案中的位元組碼指令,即減少了程式運行時需要執行的指令,提高了程式效率(大家可以用上述jad命令反編譯Class檔案進行驗證)。同理,對於基礎資料型別 (Elementary Data Type)字面量的算術操作,也在編譯期間進行了計算,例如int day = 24 * 60 * 60,編譯後被替換成代碼int day = 0x15180。由於在編譯期間已經進行了拼接,這樣局部變數a和b都指向了常量池中的’a1’對象,因此a == b輸出為true。
題2
String hw = "Hello world!"; String h = "Hello"; h = h + " world!"; System.out.println(h == hw);
答案:false
說明:通過之前關於字串串連符+分析,我們知道h = h + " world!"經過編譯後會被替換成h = (new StringBuilder(String.valueOf(h))).append(" world!").toString()。查看下StringBuilder的toString方法,可以看到該方法實際就是return new String(value, 0, count),也就是h將指向java堆上的對象,而hw是指向常量池中的對象。雖然h和hw的內容相同,但由於指向不同的String對象,所以輸出為false。
題3
public static final String h2 = "Hello"; public static final String h4 = getH(); private static String getH() { return "Hello"; } public static void main(String[] args) { String hw = "Hello world!"; final String h1 = "Hello"; final String h3 = getH(); String hw1 = h1 + " world!"; String hw2 = h2 + " world!"; String hw3 = h3 + " world!"; String hw4 = h4 + " world!"; System.out.println(hw == hw1); System.out.println(hw == hw2); System.out.println(hw == hw3); System.out.println(hw == hw4); }
答案:true,true,false,false
說明:局部變數h1被final修飾,意味著h1是常量,同時h1被直接賦值為字串字面量”Hello”,這樣java編譯器在編譯期就能確定h1的值,從而將h1出現的地方直接替換成字面量”Hello”(類似c/c++用define定義的常量),再聯絡之前關於字面量會在編譯期直接拼接說明,因此代碼String hw1 = h1 + " world!"編譯後最佳化為String hw1 = "Hello world!",hw、hw1都指向了常量池中的String對象,輸出為true。同理h2是靜態常量,且是直接字面量賦值方式,h2出現的地方也會在編譯後直接被字面量”Hello”替換,最終,hw2也是指向常量池中的String對象,輸出為true。
局部變數h3也被final修飾,為常量,但是其是通過方法調用進行賦值的,編譯期無法確定其具體值(此時代碼都沒執行,是無法通過靜態分析得到方法的傳回值的,即使方法體中只是簡單的返回字串常量,如上述例子),再聯絡之前關於+的本質分析,因此String hw3 = h3 + " world!"編譯後為String hw3 = (new StringBuilder(String.valueOf(h3))).append(" world!").toString(),hw3將指向java堆上的String對象,hw == hw3輸出為false。同理,hw4也指向java堆上的String對象,hw == hw4輸出為false。
補充知識點
關於String類型變數的賦值,有兩種方式:
- 其一、直接字面量賦值,即
String str = “abc”;
- 其二、new方式賦值,即
String str = new String(“abc”);
方式一中,變數str直接指向字串常量池1中字面量為”abc”的String對象,即指向常量池中的String對象。
方式二中,變數str通過new建構函式String(String original)賦值,即指向java堆中的String對象。該建構函式接收String型別參數,而實參”abc”指向常量池中的String對象。
上面兩種給String類型變數賦值的方式,除了它們指向不同的String對象外,其它並沒有什麼區別。從程式效率的角度看,推薦使用方式一給String類型變數賦值,因為方式二多了一次java堆的String對象分配。
前面說過,字串字面量直接被看作String類的一個執行個體,實際是其在編譯期就存放在Class檔案的常量池中,當Class檔案被jvm載入時,其就進入到方法區的運行時常量池中。如果想在運行期間將新的常量加入常量池中,可調用String的intern()方法。
當調用 intern方法時,如果常量池已經包含一個等於此String對象的字串(用equals(Object)方法確定),則返回常池中的字串。否則,將此String 對象添加到池中,並返回此String對象的引用。
附反射修改String對象代碼:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { String name = "angel"; String name1 = "angel"; Field strField = String.class.getDeclaredField("value"); strField.setAccessible(true); char[] data = (char[])strField.get(name); data[4] = 'r'; System.out.println(name); System.out.println(name1); System.out.println(name == name1); strField = String.class.getDeclaredField("count"); strField.setAccessible(true); strField.setInt(name, 10); int i = (Integer)strField.get(name); System.out.println(i); System.out.println(name.length()); }