一文弄懂String的所有小秘密

2020-04-25     sandag

String是java中非常常用的一個對象類型。可以說java中使用最多的就是String了。那麼String到底有哪些秘密呢?接下來本文將會一一講解。

String是不可變的

String是不可變的,官方的說法叫做immutable或者constant。

String的底層其實是一個Char的數組。

private final char value[];

所有的String字面量比如」abc」都是String的實現。

考慮下面的賦值操作:

String a="abc";
String b="abc";

對於java虛擬機來說,」abc」是字符串字面量,在JDK 7之後,這個字符串字面量是存儲在java heap中的。而在JDK 7之前是有個專門的方法區來存儲的。

有了「abc」,然後我們將「abc」 賦值給a和b。

可以看到這裡a和b只是java heap中字符串的引用。

再看看下面的代碼發生了什麼:

String c= new String("abc");

首先在java heap中創建了「abc」,然後調用String的構造函數:

public String(String original) {
this.value = original.value;
this.hash = original.hash;
}

在構造函數中,String將底層的字符串數組賦值給value。

因為Array的賦值只是引用的賦值,所以上述new操作並不會產生新的字符串字面值。

但是new操作新創建了一個String對象,並將其賦值給了c。

String的不可變性還在於,String的所有操作都會產生新的字符串字面量。原來的字符串是永遠不會變化的。

字符串不變的好處就在於,它是線程安全的。任何線程都可以很安全的讀取字符串。

傳值還是傳引用

一直以來,java開發者都有這樣的問題,java到底是傳值還是傳引用呢?

我想,這個問題可以從兩方面來考慮。

首先對於基礎類型int,long,double來說,對他們的賦值是值的拷貝。而對於對象來說,賦值操作是引用。

另一方面,在方法調用的參數中,全部都是傳值操作。

public static void main(String[] args) {
String x = new String("ab");
change(x);
System.out.println(x);
}

public static void change(String x) {
x = "cd";
}

我們看上面的例子,上面的例子輸出ab。因為x是對「ab」的引用,但是在change方法中,因為是傳值調用,所以會創建一個新的x,其值是「ab」的引用地址。當x被重新賦值之後,改變的只是拷貝之後的x值。而本身的x值是不變的。

substring() 導致的內存泄露

第一次看到這個話題,大家可能會很驚訝,substring方法居然會導致內存泄露?這個話題要從JDK 6開始講起。

我們先看下JDK 6的實現:

String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}

public String substring(int beginIndex, int endIndex) {
//check boundary
return new String(offset + beginIndex, endIndex - beginIndex, value);
}

可以看到,JDK 6的substring方法底層還是引用的原始的字符串數組。唯一的區別就是offset和count不同。

我們考慮一下下面的應用:

String string = "abcdef";
String subString = string.substring(1, 3);
string = null;

雖然最後我們將String賦值為null,但是subString仍然引用了最初的string。將不會被垃圾回收。

在JDK 7之後,String的實現發送了變化:

public String(char value[], int offset, int count) {
//check boundary
this.value = Arrays.copyOfRange(value, offset, offset + count);
}

public String substring(int beginIndex, int endIndex) {
//check boundary
int subLen = endIndex - beginIndex;
return new String(value, beginIndex, subLen);
}

Arrays.copyOfRange將會拷貝一份新的數組,而不是使用之前的數組。從而不會發生上面的內存泄露的問題。

雖然String是我們經常使用的對象,但是裡面的原理還是值得我們了解的。

文章來源: https://twgreatdaily.com/zh-my/dr2gvXEBnkjnB-0z7zfY.html