java基礎鞏固-淺析String原始碼及其不可變性

weixin_34138377發表於2018-06-13

字串可以說是廣泛應用在日常程式設計中,jdk從1.0就提供了String類來建立和操作字串。同時它也是不可改變類(基本型別的包裝類都不可改變)的典型代表。

原始碼檢視(基於1.8)

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[]; //這邊只是引用並不是真正物件
    ...
}
//首先string類建立的物件是不可變的(一個物件在建立完成後不能再改變它狀態,說明是不可變的,併發程式最喜歡不可變數了),
//裡面最主要的成員為char型別的陣列

幾個構造方法

//空的構造方法 例如 String a = new String(); a為""空字元
public String() {
    this.value = new char[0];
}

//帶參構造方法 將源的hash和value賦給目標String
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

幾個常用經典的String類方法

1.equals
    //如果引用指向的記憶體值都相等 直接返回true
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        //instanceof判斷是否屬於或子類 但Stringfinal修飾不可繼承 只考慮是否為String型別
        //上面說過String的成員為char陣列,equals內則比較char類陣列元素是否一一相等 
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            //長度不相等返回false
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                //從後往前單個字元判斷,如果有不相等,返回false
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

2.subString(beginIndex,endIndex)

subString這邊存在一個小插曲

在jdk1.7以前,該方法存在記憶體洩漏問題。之所以存在是因為在此之前,String三個引數的構造方法是這麼寫的。成員變數為這三個,jdk7以後取消掉了offset和count加入了hash,雖然原來的構造方法簡潔高效但存在gc問題。所以7以後放棄了效能採取了更為保守的寫法。

    /** The value is used for character storage. */
    private final char value[];

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;
   ...
   public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
 //雖然這邊返回的是新的String物件,但構造方法中還引用著原先的value
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
    }
   ...
   String(int offset, int count, char value[]) {  
     this.value = value;  
     this.offset = offset;  
     this.count = count;  
    }  
//這邊開始this.value = value; 出現問題,這三個個原來為String類中的三個私有成員變數,因為這種實現還在引用原先的字串變數value[] 通過offset(起始位置)和count(字元所佔個數)返回一個新的字串,這樣可能導致jvm認為最初被擷取的字串還被引用就不對其gc,如果這個原始字串很大,就會佔用著記憶體,出現記憶體洩漏等gc問題。

jdk1.7以後的寫法變化

//雖然這邊還是有offse和count引數 但不是成員變數了
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0
...
public String substring(int beginIndex, int endIndex) {
      if (beginIndex < 0) {
          throw new StringIndexOutOfBoundsException(beginIndex);
      }
      if (endIndex > value.length) {
          throw new StringIndexOutOfBoundsException(endIndex);
      }
      int subLen = endIndex - beginIndex; 
      if (subLen < 0) {
          throw new StringIndexOutOfBoundsException(subLen);
      }
      return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);//建構函式引數順序也有所變化
}
...
public String(char value[], int offset, int count) {
  if (offset < 0) {
    throw new StringIndexOutOfBoundsException(offset);
  }
  if (count < 0) {
    throw new StringIndexOutOfBoundsException(count);
  }
  // Note: offset or count might be near -1>>>1.
  if (offset > value.length - count) {
    throw new StringIndexOutOfBoundsException(offset + count);
  }
    //新的不是引用之前的而是重新新建了一個。
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

3.hashcode
public int hashCode() {
    int h = hash;
    //如果hash沒有被計算過,並且字串不為空,則進行hashCode計算
    if (h == 0 && value.length > 0) {
        char val[] = value;
        //計算過程
        //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //hash賦值
        hash = h;
    }
    return h;
}
//String重寫了Object類的hashcode方法,根據值來計算hashcode值,不過Object的該方法為native本地方法。
//該方法設計十分巧妙,它先從0判斷字元大小,如果
//hashcode其實就是雜湊碼 這種演算法的話 同樣的字串的值一定相等 但不同的字串其實也有可能得到同樣的hashcode值 
n=3
i=0 -> h = 31 * 0 + val[0]  
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]
//以字串 "123"為例 1的的ascil碼為49 所以 "1".hashcode()的值為49,2為50...
h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] = 31 * (31 * 49 + 50) + 51 = 48690

詳細演算法參考這裡

String的不可變性

//這邊可能存在一個疑問 s物件是否發生了改變
String s = "hello";
s = "world";
//String不可變不是在原記憶體地址上修改資料,而是重新指向一個新物件,新地址,所以這裡的hello物件並沒有被改變。
//同樣的類似replace方法原始碼中
  public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);//這邊返回的是新的一個String物件 而不改變原來的物件
            }
        }
        return this; //如果都一樣 就指向同一個物件了
    }
10006199-76addb54fee533f3.png
String物件不可變

String物件為什麼不可變

String用final修飾是為了防止被繼承進而破壞,而讓String物件不可變主要也是為了安全。

這裡原來我有一個誤區,僅僅認為說final修飾了String類,所以String物件不可變,想法很天真,,final修飾類的話主要是讓類不可被繼承,final修飾基本型別變數不能對基本型別物件重新賦值,但對於引用型別的變數(如陣列),它儲存的只是一個引用,final只需保證這個引用的地址不改變,即一直引用同一個物件即可,但是這個物件還是能改變的。

比如 final int[] arr = {1};
arr[0] = 3; //改變

arr = new int[]{3}; //因為這個是個新的陣列 這樣改變了陣列的地址 編譯不通過 除非去掉final

//String類原始碼中的成員變數 (jdk1.8)
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
10006199-4174caf368f16002.png
String.png

思考

1.String物件是否真的不可變

可以通過反射改變
String的成員變數為final修飾,就是初始化之後不可改變,但是這幾個成員中value比較特殊,因為他是個引用變數而不是真正的物件,value[]是final修飾的,也就是說不能再指向其他陣列物件,但是可以改變陣列內部的結構來改變。
注:簡單來說就是final修飾陣列,指定陣列所指向的記憶體空間固定,陣列內部值還能改。因為陣列是引用型別,記憶體地址是不可改變的。
例項程式碼
public static void testReflection() throws Exception {
    String s = "Hello,World"; 
    System.out.println("s = " + s); //Hello World  
    //獲取String類中的value欄位
    Field valueOfString = String.class.getDeclaredField("value");    
    //改變value屬性的訪問許可權
    valueOfString.setAccessible(true);    
    //獲取s物件上的value屬性的值
    char[] value = (char[]) valueOfString.get(s);   
    //改變value所引用的陣列中的第5個字元
    value[5] = '_';  
    System.out.println("s = " + s);  //Hello_World
}

String為什麼要設計成不可變

1.允許String物件快取hashcode:
Java中String物件的雜湊碼被頻繁地使用, 比如在hashMap 等容器中。字串不變性保證了hash碼的唯一性,因此可以放心地進行快取。
2.安全性
String被許多的Java類(庫)用來當做引數,例如 網路連線地址URL,檔案路徑path,還有反射機制所需要的String引數等, 假若String不是固定不變的,將會引起各種安全隱患。
對於一個方法而言,引數是為該方法提供資訊的,而不是讓方法改變自己。
3.字串常量池的需要
字串常量池(String pool, String intern pool, String保留池) java堆記憶體放了一個特殊的區域用於常量池, 當建立一個String物件時,假如此字串值已經存在於常量池中,則不會建立一個新的物件,而是引用已經存在的物件。

相關文章