java基礎(五) String性質深入解析

程式設計師歐陽思海發表於2018-04-10

引言

本文將講解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,獲取優質資源。

相關文章