java基礎鞏固-淺析String原始碼及其不可變性
字串可以說是廣泛應用在日常程式設計中,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; //如果都一樣 就指向同一個物件了
}
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
思考
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物件時,假如此字串值已經存在於常量池中,則不會建立一個新的物件,而是引用已經存在的物件。
相關文章
- html基礎鞏固HTML
- String原始碼淺析原始碼
- 《怎麼鞏固JAVA基礎語法基礎(上)》Java
- 鞏固系統韌性三個基礎策略
- String 原始碼淺析(一)原始碼
- PHP 函式基礎鞏固PHP函式
- Java 基礎鞏固:再談抽象類和介面Java抽象
- [Java基礎]String 為什麼是不可變的?Java
- 基礎鞏固 --多執行緒執行緒
- String 原始碼淺析————終結篇原始碼
- java基礎:String — 原始碼分析(一)Java原始碼
- 【JDK原始碼分析】String的儲存區與不可變性JDK原始碼
- 太讚了!用Java實現的線上聊天小專案,適合鞏固基礎(附原始碼)Java原始碼
- 鞏固好基礎,才能學好LinuxLinux
- Java中String類不可變性的好處Java
- Java容器ArrayList原始碼淺析Java原始碼
- Java基礎知識強化(用於自我鞏固)以及審查Java
- Java基礎鞏固第三天(泛型、ArrayList、LinkList、HashSet)Java泛型
- 基礎鞏固、探尋Java裝箱和拆箱的奧妙!Java
- JAVA面試 基礎加強與鞏固:反射、註解、泛型等Java面試反射泛型
- 長篇總結之JavaScript,鞏固前端基礎JavaScript前端
- Go 基礎鞏固加強-1.0-指標Go指標
- Java7 ConcurrentHashMap原始碼淺析JavaHashMap原始碼
- Java集合之ConcurrentHashMap原始碼淺析JavaHashMap原始碼
- 學Python要避免哪些坑,如何鞏固好基礎Python
- 鞏固Python基礎必學的22個語法Python
- Redux原始碼淺析Redux原始碼
- redux 原始碼淺析Redux原始碼
- Koa 原始碼淺析原始碼
- webmagic原始碼淺析Web原始碼
- 淺析Redux原始碼Redux原始碼
- ARouter原始碼淺析原始碼
- java多型性淺析Java多型
- java基礎(五) String性質深入解析Java
- app直播原始碼,AnimatedOpacity 漸變元件淺析APP原始碼元件
- [鞏固基礎]總結Python基礎知識的14張思維導圖Python
- Flutter 原始碼系列:DropdownButton 原始碼淺析Flutter原始碼
- Discuz! Q 原始碼淺析原始碼