1. 程式人生 > >java基礎(五) String性質深入解析

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

java

引言

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

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