java基础(五) String性质深入解析
引言
??本文將講解String的幾個性質。
一、String的不可變性
??對于初學者來說,很容易誤認為String對象是可以改變的,特別是+鏈接時,對象似乎真的改變了。然而,String對象一經創(chuàng)建就不可以修改。接下來,我們一步步 分析String是怎么維護其不可改變的性質;
1. 手段一:final類 和 final的私有成員
我們先看一下String的部分源碼:
public final class Stringimplements 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;}??我們可以發(fā)現 String是一個final類,且3個成員都是私有的,這就意味著String是不能被繼承的,這就防止出現:程序員通過繼承重寫String類的方法的手段來使得String類是“可變的”的情況。
??從源碼發(fā)現,每個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. 手段二:改變即創(chuàng)建對象的方法
??從上面的分析我們知道,我們是無法從外部修改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
??是不是對運行結果感覺很不解。別急,我們來慢慢理清楚。首先,我們要知道編譯器有個優(yōu)點:在編譯期間會盡可能地優(yōu)化代碼,所以能由編譯器完成的計算,就不會等到運行時計算,如常量表達式的計算就是在編譯期間完成的。所以,s2 的結果其實在編譯期間就已經計算出來了,與 s 的值是一樣,所以兩者相等,即都屬于字面常量,在類加載時創(chuàng)建并維護在字符串常量池中。但 s3 的表達式中含有變量 s2 ,只能是運行時才能執(zhí)行計算,也就是說,在運行時才計算結果,在堆中創(chuàng)建對象,自然與 s 不相等。而 s4 使用new直接在堆中創(chuàng)建對象,更不可能相等。
??那在運行期間,是如何完成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()方法。也就是說,在運行期間,鏈接字符串的計算都是通過 創(chuàng)建StringBuilder對象,調用append()方法來完成的,而且是每一個鏈接字符串的表達式都要創(chuàng)建一個 StringBuilder對象。因此對于循環(huán)中反復執(zhí)行字符串鏈接時,應該考慮直接使用StringBuilder來代替 + 鏈接,避免重復創(chuàng)建StringBuilder的性能開銷。
字符串常量池
常量池可以參考我上一篇文章,此處不會深入,只講解與String相關的部分。
??字符串常量池的內容大部分來源于編譯得到的字符串字面常量。在運行期間同樣也會增加,
String intern():
返回字符串對象的規(guī)范化表示形式。
一個初始為空的字符串池,它由類 String 私有地維護。
當調用 intern 方法時,如果池已經包含一個等于此 String 對象的字符串(用 equals(Object) 方法確定),則返回池中的字符串。否則,將此 String 對象添加到池中,并返回此 String 對象的引用。
它遵循以下規(guī)則:對于任意兩個字符串 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 乘子的優(yōu)選質數之一。另外一些相近的質數,比如37、41、43等等,也都是不錯的選擇。那么為啥偏偏選中了31呢?請看第二個原因。
31可以被 JVM 優(yōu)化,31 * i = (i << 5) - i。
作者:jinggod
出處:http://www.cnblogs.com/jinggod/p/8425182.html
總結
以上是生活随笔為你收集整理的java基础(五) String性质深入解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java基础(四) java运算顺序的深
- 下一篇: java基础(六) switch语句的深