引言
本文將講解String的幾個性質。
一、String的不可變性
對於初學者來說,很容易誤認為String物件是可以改變的,特別是+連結時,物件似乎真的改變了。然而,String物件一經建立就不可以修改。接下來,我們一步步 分析String是怎麼維護其不可改變的性質;
1. 手段一:final類 和 final的私有成員
我們先看一下String的部分原始碼:
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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
}
複製程式碼
我們可以發現 String是一個final類,且3個成員都是私有的,這就意味著String是不能被繼承的,這就防止出現:程式設計師通過繼承重寫String類的方法的手段來使得String類是“可變的”的情況。
從原始碼發現,每個String物件維護著一個char陣列 —— 私有成員value。陣列value 是String的底層陣列,用於儲存字串的內容,而且是 private final ,但是陣列是引用型別,所以只能限制引用不改變而已,也就是說陣列元素的值是可以改變的,而且String 有一個可以傳入陣列的構造方法,那麼我們可不可以通過修改外部char陣列元素的方式來“修改”String 的內容呢?
我們來做一個實驗,如下:
public static void main(String[] args) {
char[] arr = new char[]{'a','b','c','d'};
String str = new String(arr);
arr[3]='e';
System.out.println("str= "+str);
System.out.println("arr[]= "+Arrays.toString(arr));
}
複製程式碼
執行結果
str= abcd arr[]= [a, b, c, e]
結果與我們所想不一樣。字串str使用陣列arr來構造一個物件,當陣列arr修改其元素值後,字串str並沒有跟著改變。那就看一下這個構造方法是怎麼處理的:
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
複製程式碼
原來 String在使用外部char陣列構造物件時,是重新複製了一份外部char陣列,從而不會讓外部char陣列的改變影響到String物件。
2. 手段二:改變即建立物件的方法
從上面的分析我們知道,我們是無法從外部修改String物件的,那麼可不可能使用String提供的方法,因為有不少方法看起來是可以改變String物件的,如replace()
、replaceAll()
、substring()
等。我們以substring()
為例,看一下原始碼:
public String substring(int beginIndex, int endIndex) {
//........
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
複製程式碼
從原始碼可以看出,如果不是切割整個字串的話,就會新建一個物件。也就是說,只要與原字串不相等,就會新建一個String物件。
擴充套件
基本型別的包裝類跟String很相似的,都是final類,都是不可改變的物件,以及維護著一個儲存內容的private final成員。如 Integer類:
public final class Integer extends Number implements Comparable<Integer> {
private final int value;
}
複製程式碼
二、String的+操作 與 字串常量池
我們先來看一個例子:
public class MyTest {
public static void main(String[] args) {
String s = "Love You";
String s2 = "Love"+" You";
String s3 = s2 + "";
String s4 = new String("Love You");
System.out.println("s == s2 "+(s==s2));
System.out.println("s == s3 "+(s==s3));
System.out.println("s == s4 "+(s==s4));
}
}
複製程式碼
執行結果:
s == s2 true s == s3 false s == s4 false
是不是對執行結果感覺很不解。別急,我們來慢慢理清楚。首先,我們要知道編譯器有個優點:在編譯期間會盡可能地優化程式碼,所以能由編譯器完成的計算,就不會等到執行時計算,如常量表示式的計算就是在編譯期間完成的。所以,s2 的結果其實在編譯期間就已經計算出來了,與 s 的值是一樣,所以兩者相等,即都屬於字面常量,在類載入時建立並維護在字串常量池中。但 s3 的表示式中含有變數 s2 ,只能是執行時才能執行計算,也就是說,在執行時才計算結果,在堆中建立物件,自然與 s 不相等。而 s4 使用new直接在堆中建立物件,更不可能相等。
那在執行期間,是如何完成String的+號連結操作的呢,要知道String物件可是不可改變的物件。我們使用jad命令 jad MyTest.class 反編譯上面例子的calss檔案回java程式碼,來看看究竟是怎麼實現的:
public class MyTest
{
public MyTest()
{
}
public static void main(String args[])
{
String s = "Love You";
String s2 = "Love You";//已經得到計算結果
String s3 = (new StringBuilder(String.valueOf(s2))).toString();
String s4 = new String("Love You");
System.out.println((new StringBuilder("s == s2 ")).append(s == s2).toString());
System.out.println((new StringBuilder("s == s3 ")).append(s == s3).toString());
System.out.println((new StringBuilder("s == s4 ")).append(s == s4).toString());
}
}
複製程式碼
可以看出,編譯器將 + 號處理成了StringBuilder.append()
方法。也就是說,在執行期間,連結字串的計算都是通過 建立StringBuilder物件,呼叫append()
方法來完成的,而且是每一個連結字串的表示式都要建立一個 StringBuilder物件。因此對於迴圈中反覆執行字串連結時,應該考慮直接使用StringBuilder來代替 + 連結,避免重複建立StringBuilder的效能開銷。
字串常量池
常量池可以參考我上一篇文章,此處不會深入,只講解與String相關的部分。
字串常量池的內容大部分來源於編譯得到的字串字面常量。在執行期間同樣也會增加,
String intern():
返回字串物件的規範化表示形式。 一個初始為空的字串池,它由類 String 私有地維護。 當呼叫 intern 方法時,如果池已經包含一個等於此 String 物件的字串(用 equals(Object) 方法確定),則返回池中的字串。否則,將此 String 物件新增到池中,並返回此 String 物件的引用。 它遵循以下規則:對於任意兩個字串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true。
另外一點值得注意的是,雖然String.intern()的返回值永遠等於字串常量。但這並不代表在系統的每時每刻,相同的字串的intern()返回都會是一樣的(雖然在95%以上的情況下,都是相同的)。因為存在這麼一種可能:在一次intern()呼叫之後,該字串在某一個時刻被回收,之後,再進行一次intern()呼叫,那麼字面量相同的字串重新被加入常量池,但是引用位置已經不同。
三、String 的hashcode()方法
String也是遵守equals的標準的,也就是 s.equals(s1)
為true,則s.hashCode()==s1.hashCode()
也為true。此處並不關注eqauls方法,而是講解 hashCode()
方法,String.hashCode()
有點意思,而且在面試中也可能被問到。先來看一下程式碼:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
複製程式碼
##為什麼要選31作為乘數呢?
從網上的資料來看,一般有如下兩個原因:
-
31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一。另外一些相近的質數,比如37、41、43等等,也都是不錯的選擇。那麼為啥偏偏選中了31呢?請看第二個原因。
-
31可以被 JVM 優化,31 * i = (i << 5) - i。
出處:http://www.cnblogs.com/jinggod/p/8425182.html
文章有不當之處,歡迎指正,你也可以關注我的微信公眾號:
好好學java
,獲取優質資源。