[TOC]
字符串就是一連串的字符序列,Java提供了String、StringBuilder、StringBuffer三個(gè)類來封裝字符串
String
String類是不可變類,String對象被創(chuàng)建以后,對象中的字符序列是不可改變的,直到這個(gè)對象被銷毀
為什么是不可變的
jdk1.8
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
//jdk1.9中將char數(shù)組替換為byte數(shù)組,緊湊字符串帶來的優(yōu)勢:更小的內(nèi)存占用,更快的操作速度。
//構(gòu)造函數(shù)
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
//構(gòu)造函數(shù)
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//返回一個(gè)新的char[]
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
}
根據(jù)上面的代碼,我們看看String究竟是怎么保證不可變的。
- String類被final修飾,不可被繼承
- string內(nèi)部所有成員都設(shè)置為私有變量,外部無法訪問
- 沒有向外暴露修改
value的接口 -
value被final修飾,所以變量的引用不可變。 -
char[]·為引用類型仍可以通過引用修改實(shí)例對象,為此String(char value[])構(gòu)造函數(shù)內(nèi)部使用的copyOf而不是直接將value[]復(fù)制給內(nèi)部變量`。 - 在獲取value時(shí),并沒有將value的引用直接返回,而是采用了
arraycopy()的方式返回一個(gè)新的char[] -
String類中的函數(shù)也處處透露著不可變的味道,比如: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) {
//重新創(chuàng)建新的char[],不改變原有對象中的值
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++;
}
//最后返回新創(chuàng)建的String對象
return new String(buf, true);
}
}
return this;
}
當(dāng)然不可變也不是絕對的,還是可以通過反射獲取到變value引用,然后通過value[]修改數(shù)組的方式改變value對象實(shí)例
String a = "Hello World!";
String b = new String("Hello World!");
String c = "Hello World!";
//通過反射修改字符串引用的value數(shù)組
Field field = a.getClass().getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(a);
System.out.println(value);//Hello World!
value[5] = '&';
System.out.println(value);//Hello&World!
// 驗(yàn)證b、c是否被改變
System.out.println(b);//Hello&World!
System.out.println(c);//Hello&World!
寫到這里該如何引出不可變的好處呢?忘記反射吧,我們聊聊不可變的好處吧
不可變的優(yōu)點(diǎn)
保證了線程安全
同一個(gè)字符串實(shí)例可以被多個(gè)線程共享。
保證了基本的信息安全
比如,網(wǎng)絡(luò)通信的IP地址,類加載器會根據(jù)一個(gè)類的完全限定名來讀取此類諸如此類,不可變性提供了安全性。
字符串緩存(常量池)的需要
具統(tǒng)計(jì),常見應(yīng)用使用的字符串中有大約一半是重復(fù)的,為了避免創(chuàng)建重復(fù)字符串,降低內(nèi)存消耗和對象創(chuàng)建時(shí)的開銷。JVM提供了字符串緩存的功能——字符串常量池。如果字符串是可變的,我們就可以通過引用改變常量池總的同一個(gè)內(nèi)存空間的值,其他指向此空間的引用也會發(fā)生改變。
支持hash映射和緩存。
因?yàn)樽址遣豢勺兊?,所以在它?chuàng)建的時(shí)候hashcode就被緩存了,不需要重新計(jì)算。這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串。
不可變的缺點(diǎn)
由于它的不可變性,像字符串拼接、裁剪等普遍性的操作,往往對應(yīng)用性能有明顯影響。
為了解決這個(gè)問題,java為我們提供了兩種解決方案
- 字符串常量池
- StringBuilder、StringBuffer是可變的
字符串常量池
還是剛才反射的示例
String a = "Hello World!";
String b = new String("Hello World!");
String c = "Hello World!";
//判斷字符串變量是否指向同一塊內(nèi)存
System.out.println(a == b);
System.out.println(a == c);
System.out.println(b == c);
// 通過反射觀察a, b, c 三者中變量value數(shù)組的真實(shí)位置
Field a_field = a.getClass().getDeclaredField("value");
a_field.setAccessible(true);
System.out.println(a_field.get(a));
Field b_field = b.getClass().getDeclaredField("value");
b_field.setAccessible(true);
System.out.println(b_field.get(b));
Field c_field = c.getClass().getDeclaredField("value");
c_field.setAccessible(true);
System.out.println(c_field.get(c));
//通過反射發(fā)現(xiàn)String對象中變量value指向了同一塊內(nèi)存
輸出
false
true
false
[C@6f94fa3e
[C@6f94fa3e
[C@6f94fa3e
字符串常量的創(chuàng)建過程:
- 判斷常量池中是否存在"Hello World!"常量,如果有直接返回該常量在池中的引用地址
- 如果沒有,先創(chuàng)建一個(gè)
char["Hello World!".length()]數(shù)組對象,然后在常量池中創(chuàng)建一個(gè)字符串對象并用數(shù)組對象初始化字符串對象的成員變量value,然后將這個(gè)字符串的引用返回,比如賦值給a
由此可見,a和c對象指向常量池中相同的內(nèi)存空間不言自明。
而b對象的創(chuàng)建是建立在以上的創(chuàng)建過程的基礎(chǔ)之上的。
"Hello World!"常量創(chuàng)建完成時(shí)返回的引用,會經(jīng)過String的構(gòu)造函數(shù)。
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
構(gòu)造函數(shù)內(nèi)部將引用的對象成員變量value賦值給了內(nèi)部成員變量value,然后將新創(chuàng)建的字符創(chuàng)對象引用賦值給了b,這個(gè)過程發(fā)生在堆中。
再來感受下下面這兩行代碼有什么區(qū)別
String b = new String(a);
String b = new String("Hello World!");
StringBuilder和StringBuffer
二者都是可變的
為了彌補(bǔ)String的缺陷,Java先后提供了StringBuffer和StringBuilder可變字符串類。
二者都繼承至AbstractStringBuilder,AbstractStringBuilder使用了char[] value字符數(shù)組
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
}
可以看出AbstractStringBuilder類和其成員變量value都沒有使用final關(guān)鍵字。
value數(shù)組的默認(rèn)長度
StringBuilder和StringBuffer的value數(shù)組默認(rèn)初始長度是16
public StringBuilder() {
super(16);
}
public StringBuffer() {
super(16);
}
如果我們拼接的字符串長度大概是可以預(yù)計(jì)的,那么最好指定合適的capacity,避免多次擴(kuò)容的開銷。
擴(kuò)容產(chǎn)生多重開銷:拋棄原有數(shù)組,創(chuàng)建新的數(shù)組,進(jìn)行arrycopy。
二者的區(qū)別
StringBuilder是非線程安全的,StringBuffer是線程安全的。
StringBuffer類中的方法使用了synchronized同步鎖來保證線程安全。
關(guān)于鎖的話題非常大,會單獨(dú)成文來說明,這里推薦一篇不錯(cuò)的博客,有興趣的可以看看