String 的聲明

由 JDK 中關(guān)于String的聲明可以知道:
- 不同字符串可能共享同一個底層char數(shù)組,例如字符串 String s=”abc” 與 s.substring(1) 就共享同一個char數(shù)組:char[] c = {‘a(chǎn)’,’b’,’c’}。其中,前者的 offset 和 count 的值分別為0和3,后者的 offset 和 count 的值分別為1和2。
- offset 和 count 兩個成員變量不是多余的,比如,在執(zhí)行substring操作時。
注意:
- String不屬于八種基本數(shù)據(jù)類型,String 的實例是一個對象。因為對象的默認(rèn)值是null,所以String的默認(rèn)值也是null;但它又是一種特殊的對象,有其它對象沒有的一些特性(String 的不可變性導(dǎo)致其像八種基本類型一樣,比如,作為方法參數(shù)時,像基本類型的傳值效果一樣)。 例如,以下代碼片段:
public class StringTest {
public static void changeStr(String str) {
String s = str;
str += "welcome";
System.out.println(s);
}
public static void main(String[] args) {
String str = "1234";
changeStr(str);
System.out.println(str);
}
}/* Output:
1234
1234
*///:~
- new String() 和 new String(“”)都是聲明一個新的空字符串,是空串不是null;
String 的不可變性
1. 不可變類
- 不可變類:所謂的不可變類是指這個類的實例一旦創(chuàng)建完成后,就不能改變其成員變量值。如JDK內(nèi)部自帶的很多不可變類:Interger、Long和String等。
- 可變類:相對于不可變類,可變類創(chuàng)建實例后可以改變其成員變量值,開發(fā)中創(chuàng)建的大部分類都屬于可變類。
2. 不可變類的設(shè)計方法
- 類添加final修飾符,保證類不被繼承。
如果類可以被繼承會破壞類的不可變性機(jī)制,只要繼承類覆蓋父類的方法并且繼承類可以改變成員變量值,那么一旦子類以父類的形式出現(xiàn)時,不能保證當(dāng)前類是否可變。 - 保證所有成員變量必須私有,并且加上final修飾。
通過這種方式保證成員變量不可改變。但只做到這一步還不夠,因為如果是對象成員變量有可能再外部改變其值。所以第4點彌補(bǔ)這個不足。 - 不提供改變成員變量的方法,包括setter
避免通過其他接口改變成員變量的值,破壞不可變特性。 - 通過構(gòu)造器初始化所有成員,進(jìn)行深拷貝(deep copy)。
如果構(gòu)造器傳入的對象直接賦值給成員變量,還是可以通過對傳入對象的修改進(jìn)而導(dǎo)致改變內(nèi)部變量的值。例如:
public final class ImmutableDemo {
private final int[] myArray;
public ImmutableDemo(int[] array) {
this.myArray = array; // wrong
}
}
這種方式不能保證不可變性,myArray和array指向同一塊內(nèi)存地址,用戶可以在ImmutableDemo之外通過修改array對象的值來改變myArray內(nèi)部的值。
為了保證內(nèi)部的值不被修改,可以采用深度copy來創(chuàng)建一個新內(nèi)存保存?zhèn)魅氲闹?。正確做法:
public final class MyImmutableDemo {
private final int[] myArray;
public MyImmutableDemo(int[] array) {
this.myArray = array.clone();
}
}
- 在getter方法中,不要直接返回對象本身,而是克隆對象,并返回對象的拷貝。
這種做法也是防止對象外泄,防止通過getter獲得內(nèi)部可變成員對象后對成員變量直接操作,導(dǎo)致成員變量發(fā)生改變。
3. String對象的不可變性
string對象在內(nèi)存創(chuàng)建后就不可改變,不可變對象的創(chuàng)建一般滿足以上5個原則,我們看看String代碼是如何實現(xiàn)的。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** 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;
/** Cache the hash code for the string */
private int hash; // Default to 0
....
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length); // deep copy操作
}
...
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;
}
...
}
如上代碼所示,可以觀察到以下設(shè)計細(xì)節(jié):
- String類被final修飾,不可繼承
- string內(nèi)部所有成員都設(shè)置為私有變量
- 不存在value的setter
- 并將value和offset設(shè)置為final。
- 當(dāng)傳入可變數(shù)組value[]時,進(jìn)行copy而不是直接將value[]復(fù)制給內(nèi)部變量.
- 獲取value時不是直接返回對象引用,而是返回對象的copy.
這都符合上面總結(jié)的不變類型的特性,也保證了String類型是不可變的類。
4. String對象的不可變性的優(yōu)缺點
從上一節(jié)分析,String數(shù)據(jù)不可變類,那設(shè)置這樣的特性有什么好處呢?我總結(jié)為以下幾點:
- 字符串常量池的需要.
字符串常量池可以將一些字符常量放在常量池中重復(fù)使用,避免每次都重新創(chuàng)建相同的對象、節(jié)省存儲空間。但如果字符串是可變的,此時相同內(nèi)容的String還指向常量池的同一個內(nèi)存空間,當(dāng)某個變量改變了該內(nèi)存的值時,其他遍歷的值也會發(fā)生改變。所以不符合常量池設(shè)計的初衷。 - 線程安全考慮。
同一個字符串實例可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。字符串自己便是線程安全的。 - 類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改成了myhacked.Connection,那么會對你的數(shù)據(jù)庫造成不可知的破壞。
- 支持hash映射和緩存。
因為字符串是不可變的,所以在它創(chuàng)建的時候hashcode就被緩存了,不需要重新計算。這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串。
缺點:
如果有對String對象值改變的需求,那么會創(chuàng)建大量的String對象。
5. String對象的是否真的不可變
雖然String對象將value設(shè)置為final,并且還通過各種機(jī)制保證其成員變量不可改變。但是還是可以通過反射機(jī)制的手段改變其值。例如:
//創(chuàng)建字符串"Hello World", 并賦給引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
//獲取String類中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改變value屬性的訪問權(quán)限
valueFieldOfString.setAccessible(true);
//獲取s對象上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改變value所引用的數(shù)組中的第5個字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
打印結(jié)果為:
s = Hello World
s = Hello_World
發(fā)現(xiàn)String的值已經(jīng)發(fā)生了改變。也就是說,通過反射是可以修改所謂的“不可變”對象的。
String 對象創(chuàng)建方式
- 字面值形式: JVM會自動根據(jù)字符串常量池中字符串的實際情況來決定是否創(chuàng)建新對象 (要么不創(chuàng)建,要么創(chuàng)建一個對象,關(guān)鍵要看常量池中有沒有)
JDK 中明確指出:
String s = "abc";
等價于:
char data[] = {'a', 'b', 'c'};
String str = new String(data);
該種方式先在棧中創(chuàng)建一個對String類的對象引用變量s,然后去查找 “abc”是否被保存在字符串常量池中。若”abc”已經(jīng)被保存在字符串常量池中,則在字符串常量池中找到值為”abc”的對象,然后將s 指向這個對象; 否則,在 堆 中創(chuàng)建char數(shù)組 data,然后在 堆 中創(chuàng)建一個String對象object,它由 data 數(shù)組支持,緊接著這個String對象 object 被存放進(jìn)字符串常量池,最后將 s 指向這個對象。
例如:
private static void test01(){
String s0 = "kvill"; // 1
String s1 = "kvill"; // 2
String s2 = "kv" + "ill"; // 3
System.out.println(s0 == s1); // true
System.out.println(s0 == s2); // true
}
執(zhí)行第 1 行代碼時,“kvill” 入池并被 s0 指向;執(zhí)行第 2 行代碼時,s1 從常量池查詢到” kvill” 對象并直接指向它;所以,s0 和 s1 指向同一對象。 由于 ”kv” 和 ”ill” 都是字符串字面值,所以 s2 在編譯期由編譯器直接解析為 “kvill”,所以 s2 也是常量池中”kvill”的一個引用。 所以,我們得出 s0==s1==s2;
- 通過 new 創(chuàng)建字符串對象 : 一概在堆中創(chuàng)建新對象,無論字符串字面值是否相等 (要么創(chuàng)建一個,要么創(chuàng)建兩個對象,關(guān)鍵要看常量池中有沒有)
String s = new String("abc");
等價于:
String original = "abc";
String s = new String(original);
所以,通過 new 操作產(chǎn)生一個字符串(“abc”)時,會先去常量池中查找是否有“abc”對象,如果沒有,則創(chuàng)建一個此字符串對象并放入常量池中。然后,在堆中再創(chuàng)建“abc”對象,并返回該對象的地址。所以,對于 String str=new String(“abc”):如果常量池中原來沒有”abc”,則會產(chǎn)生兩個對象(一個在常量池中,一個在堆中);否則,產(chǎn)生一個對象。
用 new String() 創(chuàng)建的字符串對象位于堆中,而不是常量池中。它們有自己獨立的地址空間,例如:
private static void test02(){
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = "kv" + new String("ill");
String s = "ill";
String s3 = "kv" + s;
System.out.println(s0 == s1); // false
System.out.println(s0 == s2); // false
System.out.println(s1 == s2); // false
System.out.println(s0 == s3); // false
System.out.println(s1 == s3); // false
System.out.println(s2 == s3); // false
}
例子中,s0 還是常量池中”kvill”的引用,s1 指向運行時創(chuàng)建的新對象”kvill”,二者指向不同的對象。對于s2,因為后半部分是 new String(“ill”),所以無法在編譯期確定,在運行期會 new 一個 StringBuilder 對象, 并由 StringBuilder 的 append 方法連接并調(diào)用其 toString 方法返回一個新的 “kvill” 對象。此外,s3 的情形與 s2 一樣,均含有編譯期無法確定的元素。因此,以上四個 “kvill” 對象互不相同。StringBuilder 的 toString 為:
public String toString() {
return new String(value, 0, count); // new 的方式創(chuàng)建字符串
}
構(gòu)造函數(shù) String(String original) 的源碼為:
/**
* 根據(jù)源字符串的底層數(shù)組長度與該字符串本身長度是否相等決定是否共用支撐數(shù)組
*/
public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
if (originalValue.length > size) {
// The array representing the String is bigger than the new
// String itself. Perhaps this constructor is being called
// in order to trim the baggage, so make a copy of the array.
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off + size); // 創(chuàng)建新數(shù)組并賦給 v
} else {
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}
由源碼可以知道,所創(chuàng)建的對象在大多數(shù)情形下會與源字符串 original 共享 char數(shù)組 。但是,什么情況下不會共享呢?
String s1 = "Abcd"; // s1 的value為Abcd的數(shù)組,offset為 0,count為 4
String s2 = a.substring(3); // s2 的value也為Abcd的數(shù)組,offset為 3,count為 1
String c = new String(s2); // s2.value.length 為 4,而 original.count = size = 1, 即 s2.value.length > size 成立
substring()方法的一些問題
- substring() 的作用:
substring(int beginIndex, int endIndex)方法截取字符串并返回其[beginIndex,endIndex-1]范圍內(nèi)的內(nèi)容。
String x = "abcdef";
x = x.substring(1,3);
System.out.println(x);
輸出內(nèi)容:
bc
-
調(diào)用substring()時發(fā)生了什么?
你可能知道,因為x是不可變的,當(dāng)使用x.substring(1,3)對x賦值的時候,它會指向一個全新的字符串:
圖片1.png
然而,這個圖不是完全正確的表示堆中發(fā)生的事情。因為在jdk6 和 jdk7中調(diào)用substring時發(fā)生的事情并不一樣。
JDK 6中的substring
String是通過字符數(shù)組實現(xiàn)的。在jdk 6 中,String類包含三個成員變量:char value[], int offset,int count。他們分別用來存儲真正的字符數(shù)組,數(shù)組的第一個位置索引以及字符串中包含的字符個數(shù)。
當(dāng)調(diào)用substring方法的時候,會創(chuàng)建一個新的string對象,但是這個string的值仍然指向堆中的同一個字符數(shù)組。這兩個對象中只有count和offset 的值是不同的。

下面是證明上說觀點的Java源碼中的關(guān)鍵代碼:
//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導(dǎo)致的問題
如果你有一個很長很長的字符串,但是當(dāng)你使用substring進(jìn)行切割的時候你只需要很短的一段。這可能導(dǎo)致性能問題,因為你需要的只是一小段字符序列,但是你卻引用了整個字符串(因為這個非常長的字符數(shù)組一直在被引用,所以無法被回收,就可能導(dǎo)致內(nèi)存泄露)。在JDK 6中,一般用以下方式來解決該問題,原理其實就是生成一個新的字符串并引用他。
x = x.substring(x, y) + ""
JDK 7 中的substring
上面提到的問題,在jdk 7中得到解決。在jdk 7 中,substring方法會在堆內(nèi)存中創(chuàng)建一個新的數(shù)組。
Java源碼中關(guān)于這部分的主要代碼如下:
//JDK 7
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);
}
字符串常量池
1、字符串池
字符串的分配,和其他的對象分配一樣,耗費高昂的時間與空間代價。JVM為了提高性能和減少內(nèi)存開銷,在實例化字符串字面值的時候進(jìn)行了一些優(yōu)化。為了減少在JVM中創(chuàng)建的字符串的數(shù)量,字符串類維護(hù)了一個字符串常量池,每當(dāng)以字面值形式創(chuàng)建一個字符串時,JVM會首先檢查字符串常量池:如果字符串已經(jīng)存在池中,就返回池中的實例引用;如果字符串不在池中,就會實例化一個字符串并放到池中。Java能夠進(jìn)行這樣的優(yōu)化是因為字符串是不可 變的,可以不用擔(dān)心數(shù)據(jù)沖突進(jìn)行共享。 例如:
public class Program
{
public static void main(String[] args)
{
String str1 = "Hello";
String str2 = "Hello";
System.out.print(str1 == str2); // true
}
}
一個初始為空的字符串池,它由類 String 私有地維護(hù)。當(dāng)以字面值形式創(chuàng)建一個字符串時,總是先檢查字符串池是否含存在該對象,若存在,則直接返回。此外,通過 new 操作符創(chuàng)建的字符串對象不指向字符串池中的任何對象。
2、手動入池
一個初始為空的字符串池,它由類 String 私有地維護(hù)。 當(dāng)調(diào)用 intern 方法時,如果池已經(jīng)包含一個等于此 String 對象的字符串(用 equals(Object) 方法確定),則返回池中的字符串。否則,將此 String 對象添加到池中,并返回此 String 對象的引用。特別地,手動入池遵循以下規(guī)則:
對于任意兩個字符串 s 和 t ,當(dāng)且僅當(dāng) s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true 。
public class TestString{
public static void main(String args[]){
String str1 = "abc";
String str2 = new String("abc");
String str3 = s2.intern();
System.out.println( str1 == str2 ); //false
System.out.println( str1 == str3 ); //true
}
}
所以,對于 String str1 = “abc”,str1 引用的是 常量池(方法區(qū)) 的對象;而 String str2 = new String(“abc”),str2引用的是 堆 中的對象,所以內(nèi)存地址不一樣。但是由于內(nèi)容一樣,所以 str1 和 str3 指向同一對象。
intern方法不同版本的JDK中有何不同?
先看下以下代碼:
String str1 = new StringBuilder("Hello").append("World").toString();
System.out.println(str1.intern() == str1);
打印結(jié)果:
jdk6 下false
jdk7 下true
Java 6 和Java7 中intern的表現(xiàn)有所不同,導(dǎo)致不同的原因是因為在Java 7中常量池的位置從PermGen區(qū)改到了Java堆區(qū)中。
jdk1.6中 intern 方法會把首次遇到的字符串實例復(fù)制到永久待(常量池)中,并返回此引用;但在jdk1.7中,只是會把首次遇到的字符串實例的引用添加到常量池中(沒有復(fù)制),并返回此引用。
對于以上代碼中的str1.intern() ,在jdk1.6中,會把“HollisChuang”這個字符串復(fù)制到常量池中,并返回他的引用。所以str1.intern()的值和str1的值,即兩個對象的地址是不一樣的。
對于以上代碼中的str1.intern() ,在jdk1.7中,會把str1的引用保存到常量池中,并把這個引用返回。所以str1.intern()的值和str1的值是相等的。
擴(kuò)展另外一個例子:
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
以上代碼,無論在1.6還是1.7中都會返回false。
原因是"ja" + "va" = "java",這個常量在常量池中已經(jīng)有了,因為這個字符串在Java中隨處可見,曾經(jīng)被初始化過。所以,在1.7中str2.intern()返回的內(nèi)容是很久之前初始化的時候的那個引用,自然和剛剛創(chuàng)建的字符串的引用不相等。
- 實例
看下面幾個場景來深入理解 String。
1). 情景一:字符串常量池
Java虛擬機(jī)(JVM)中存在著一個字符串常量池,其中保存著很多String對象,并且這些String對象可以被共享使用,因此提高了效率。之所以字符串具有字符串常量池,是因為String對象是不可變的,因此可以被共享。字符串常量池由String類維護(hù),我們可以通過intern()方法使字符串池手動入池。
String s1 = "abc";
//↑ 在字符串池創(chuàng)建了一個對象
String s2 = "abc";
//↑ 字符串pool已經(jīng)存在對象“abc”(共享),所以創(chuàng)建0個對象,累計創(chuàng)建一個對象
System.out.println("s1 == s2 : "+(s1==s2));
//↑ true 指向同一個對象,
System.out.println("s1.equals(s2) : " + (s1.equals(s2)));
//↑ true 值相等
2). 情景二:關(guān)于new String(“…”)
String s3 = new String("abc");
//↑ 創(chuàng)建了兩個對象,一個存放在字符串池中,一個存在與堆區(qū)中;
//↑ 還有一個對象引用s3存放在棧中
String s4 = new String("abc");
//↑ 字符串池中已經(jīng)存在“abc”對象,所以只在堆中創(chuàng)建了一個對象
System.out.println("s3 == s4 : "+(s3==s4));
//↑false s3和s4棧區(qū)的地址不同,指向堆區(qū)的不同地址;
System.out.println("s3.equals(s4) : "+(s3.equals(s4)));
//↑true s3和s4的值相同
System.out.println("s1 == s3 : "+(s1==s3));
//↑false 存放的地區(qū)都不同,一個方法區(qū),一個堆區(qū)
System.out.println("s1.equals(s3) : "+(s1.equals(s3)));
//↑true 值相同
通過 new String(“…”) 來創(chuàng)建字符串時,在該構(gòu)造函數(shù)的參數(shù)值為字符串字面值的前提下,若該字面值不在字符串常量池中,那么會創(chuàng)建兩個對象:一個在字符串常量池中,一個在堆中;否則,只會在堆中創(chuàng)建一個對象。對于不在同一區(qū)域的兩個對象,二者的內(nèi)存地址必定不同。
3). 情景三:字符串連接符“+”
String str2 = "ab"; //1個對象
String str3 = "cd"; //1個對象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4 = str5 : " + (str4==str5)); // false
我們看這個例子,局部變量 str2,str3 指向字符串常量池中的兩個對象。在運行時,第三行代碼(str2+str3)實質(zhì)上會被分解成五個步驟,分別是:
(1). 調(diào)用 String 類的靜態(tài)方法 String.valueOf() 將 str2 轉(zhuǎn)換為字符串表示;
(2). JVM 在堆中創(chuàng)建一個 StringBuilder對象,同時用str2指向轉(zhuǎn)換后的字符串對象進(jìn)行初始化;
(3). 調(diào)用StringBuilder對象的append方法完成與str3所指向的字符串對象的合并;
(4). 調(diào)用 StringBuilder 的 toString() 方法在堆中創(chuàng)建一個 String對象;
(5). 將剛剛生成的String對象的堆地址存賦給局部變量引用str4。
而引用str5指向的是字符串常量池中字面值”abcd”所對應(yīng)的字符串對象。由上面的內(nèi)容我們可以知道,引用str4和str5指向的對象的地址必定不一樣。這時,內(nèi)存中實際上會存在五個字符串對象: 三個在字符串常量池中的String對象、一個在堆中的String對象和一個在堆中的StringBuilder對象。
4). 情景四:字符串的編譯期優(yōu)化
String str1 = "ab" + "cd"; //1個對象
String str11 = "abcd";
System.out.println("str1 = str11 : "+ (str1 == str11)); // true
final String str8 = "cd";
String str9 = "ab" + str8;
String str89 = "abcd";
System.out.println("str9 = str89 : "+ (str9 == str89)); // true
//↑str8為常量變量,編譯期會被優(yōu)化
String str6 = "b";
String str7 = "a" + str6;
String str67 = "ab";
System.out.println("str7 = str67 : "+ (str7 == str67)); // false
//↑str6為變量,在運行期才會被解析。
Java 編譯器對于類似“常量+字面值”的組合,其值在編譯的時候就能夠被確定了。在這里,str1 和 str9 的值在編譯時就可以被確定,因此它們分別等價于: String str1 = “abcd”; 和 String str9 = “abcd”;
Java 編譯器對于含有 “String引用”的組合,則在運行期會產(chǎn)生新的對象 (通過調(diào)用StringBuilder類的toString()方法),因此這個對象存儲在堆中。
4、小結(jié)
使用字面值形式創(chuàng)建的字符串與通過 new 創(chuàng)建的字符串一定是不同的,因為二者的存儲位置不同:前者在方法區(qū),后者在堆;
我們在使用諸如String str = “abc”;的格式創(chuàng)建字符串對象時,總是想當(dāng)然地認(rèn)為,我們創(chuàng)建了String類的對象str。但是事實上, 對象可能并沒有被創(chuàng)建。唯一可以肯定的是,指向 String 對象 的引用被創(chuàng)建了。至于這個引用到底是否指向了一個新的對象,必須根據(jù)上下文來考慮;
字符串常量池的理念是享元模式
Java 編譯器對 “常量+字面值” 的組合 是當(dāng)成常量表達(dá)式直接求值來優(yōu)化的;對于含有“String引用”的組合,其在編譯期不能被確定,會在運行期創(chuàng)建新對象。
三大字符串類 : String、StringBuilder 和 StringBuffer
1. String 與 StringBuilder
簡要的說, String 類型 和 StringBuilder 類型的主要性能區(qū)別在于 String 是不可變的對象。 事實上,在對 String 類型進(jìn)行“改變”時,實質(zhì)上等同于生成了一個新的 String 對象,然后將指針指向新的 String 對象。由于頻繁的生成對象會對系統(tǒng)性能產(chǎn)生影響,特別是當(dāng)內(nèi)存中沒有引用指向的對象多了以后,JVM 的垃圾回收器就會開始工作,繼而會影響到程序的執(zhí)行效率。所以,對于經(jīng)常改變內(nèi)容的字符串,最好不要聲明為 String 類型。但如果我們使用的是 StringBuilder 類,那么情形就不一樣了。因為,我們的每次修改都是針對 StringBuilder 對象本身的,而不會像對String操作那樣去生成新的對象并重新給變量引用賦值。所以,在一般情況下,推薦使用 StringBuilder ,特別是字符串對象經(jīng)常改變的情況下。
在某些特別情況下,String 對象的字符串拼接可以直接被JVM 在編譯期確定下來,這時,StringBuilder 在速度上就不占任何優(yōu)勢了。
因此,在絕大部分情況下, 在效率方面:StringBuilder > String 。
2.StringBuffer 與 StringBuilder
首先需要明確的是,StringBuffer 始于 JDK 1.0,而 StringBuilder 始于 JDK 5.0;此外,從 JDK 1.5 開始,對含有字符串變量 (非字符串字面值) 的連接操作(+),JVM 內(nèi)部是采用 StringBuilder 來實現(xiàn)的,而在這之前,這個操作是采用 StringBuffer 實現(xiàn)的。
JDK的實現(xiàn)中 StringBuffer 與 StringBuilder 都繼承自 AbstractStringBuilder。AbstractStringBuilder的實現(xiàn)原理為:AbstractStringBuilder中采用一個 char數(shù)組 來保存需要append的字符串,char數(shù)組有一個初始大小,當(dāng)append的字符串長度超過當(dāng)前char數(shù)組容量時,則對char數(shù)組進(jìn)行動態(tài)擴(kuò)展,即重新申請一段更大的內(nèi)存空間,然后將當(dāng)前char數(shù)組拷貝到新的位置,因為重新分配內(nèi)存并拷貝的開銷比較大,所以每次重新申請內(nèi)存空間都是采用申請大于當(dāng)前需要的內(nèi)存空間的方式,這里是 2 倍。
StringBuffer 和 StringBuilder 都是可變的字符序列,但是二者最大的一個不同點是:StringBuffer 是線程安全的,而 StringBuilder 則不是。StringBuilder 提供的API與StringBuffer的API是完全兼容的,即,StringBuffer 與 StringBuilder 中的方法和功能完全是等價的,但是后者一般要比前者快。因此,可以這么說,StringBuilder 的提出就是為了在單線程環(huán)境下替換 StringBuffer 。
在單線程環(huán)境下,優(yōu)先使用 StringBuilder。
3.實例
1). 編譯時優(yōu)化與字符串連接符的本質(zhì)
我們先來看下面這個例子:
public class Test2 {
public static void main(String[] args) {
String s = "a" + "b" + "c";
String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = s1 + s2 + s3;
System.out.println(s);
System.out.println(s4);
}
}
由上面的敘述,我們可以知道,變量s的創(chuàng)建等價于 String s = “abc”; 而變量s4的創(chuàng)建相當(dāng)于:
StringBuilder temp = new StringBuilder(s1);
temp.append(s2).append(s3);
String s4 = temp.toString();
但事實上,是不是這樣子呢?我們將其反編譯一下,來看看Java編譯器究竟做了什么:
//將上述 Test2 的 class 文件反編譯
public class Test2
{
public Test2(){}
public static void main(String args[])
{
String s = "abc"; // 編譯期優(yōu)化
String s1 = "a";
String s2 = "b";
String s3 = "c";
//底層使用 StringBuilder 進(jìn)行字符串的拼接
String s4 = (new StringBuilder(String.valueOf(s1))).append(s2).append(s3).toString();
System.out.println(s);
System.out.println(s4);
}
}
根據(jù)上面的反編譯結(jié)果,很好的印證了我們在上面提出的字符串連接符的本質(zhì)。
2). 另一個例子:字符串連接符的本質(zhì)
由上面的分析結(jié)果,我們不難推斷出 String 采用連接運算符(+)效率低下原因分析,形如這樣的代碼:
public class Test {
public static void main(String args[]) {
String s = null;
for(int i = 0; i < 100; i++) {
s += "a";
}
}
}
會被編譯器編譯為:
public class Test
{
public Test(){}
public static void main(String args[])
{
String s = null;
for (int i = 0; i < 100; i++)
s = (new StringBuilder(String.valueOf(s))).append("a").toString();
}
}
也就是說,每做一次 字符串連接操作 “+” 就產(chǎn)生一個 StringBuilder 對象,然后 append 后就扔掉。下次循環(huán)再到達(dá)時,再重新 new 一個 StringBuilder 對象,然后 append 字符串,如此循環(huán)直至結(jié)束。事實上,如果我們直接采用 StringBuilder 對象進(jìn)行 append 的話,我們可以節(jié)省 N - 1 次創(chuàng)建和銷毀對象的時間。所以,對于在循環(huán)中要進(jìn)行字符串連接的應(yīng)用,一般都是用StringBulider對象來進(jìn)行append操作。
String 與 (深)克隆
1、克隆的定義與意義
顧名思義,克隆就是制造一個對象的副本。一般地,根據(jù)所要克隆的對象的成員變量中是否含有引用類型,可以將克隆分為兩種:淺克隆(Shallow Clone) 和 深克隆(Deep Clone),默認(rèn)情況下使用Object中的clone方法進(jìn)行克隆就是淺克隆,即完成對象域?qū)τ虻目截悺?br>
(1). Object 中的 clone() 方法

在使用clone()方法時,若該類未實現(xiàn) Cloneable 接口,則拋出 java.lang.CloneNotSupportedException 異常。下面我們以Employee這個例子進(jìn)行說明:
public class Employee {
private String name;
private double salary;
private Date hireDay;
...
public static void main(String[] args) throws CloneNotSupportedException {
Employee employee = new Employee();
employee.clone();
System.out.println("克隆完成...");
}
}/* Output:
~Exception in thread "main" java.lang.CloneNotSupportedException: P1_1.Employee
*///:
(2). Cloneable 接口
Cloneable 接口是一個標(biāo)識性接口,即該接口不包含任何方法(甚至沒有clone()方法),但是如果一個類想合法的進(jìn)行克隆,那么就必須實現(xiàn)這個接口。下面我們看JDK對它的描述:
- A class implements the Cloneable interface to indicate to the java.lang.Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.
- Invoking Object’s clone method on an instance that does not implement the Cloneable interface results in the exception CloneNotSupportedException being thrown.
- By convention, classes that implement this interface should override Object.clone (which is protected) with a public method.
- Note that this interface does not contain the clone() method. Therefore, it is not possible to clone an object merely by virtue of the fact that it implements this interface. Even if the clone method is invoked reflectively, there is no guarantee that it will succeed.
/**
* @author unascribed
* @see java.lang.CloneNotSupportedException
* @see java.lang.Object#clone()
* @since JDK1.0
*/
public interface Cloneable {
}
2、Clone & Copy
假設(shè)現(xiàn)在有一個Employee對象,Employee tobby = new Employee(“CMTobby”,5000),通常, 我們會有這樣的賦值Employee tom=tobby,這個時候只是簡單了copy了一下reference,tom 和 tobby 都指向內(nèi)存中同一個object,這樣tom或者tobby對對象的修改都會影響到對方。打個比方,如果我們通過tom.raiseSalary()方法改變了salary域的值,那么tobby通過getSalary()方法得到的就是修改之后的salary域的值,顯然這不是我們愿意看到的。如果我們希望得到tobby所指向的對象的一個精確拷貝,同時兩者互不影響,那么我們就可以使用Clone來滿足我們的需求。Employee cindy=tobby.clone(),這時會生成一個新的Employee對象,并且和tobby具有相同的屬性值和方法。
3、Shallow Clone & Deep Clone

這個時候,我們就需要進(jìn)行 Deep Clone 了,以便對那些引用類型的域進(jìn)行特殊的處理,例如本例中的hireDay。我們可以重新定義 clone方法,對hireDay做特殊處理,如下代碼所示:
class Employee implements Cloneable
{
private String name;
private int id;
private Date hireDay;
...
@Override
public Object clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
// Date 支持克隆且重寫了clone()方法,Date 的定義是:
// public class Date implements java.io.Serializable, Cloneable, Comparable<Date>
cloned.hireDay = (Date) hireDay.clone() ;
return cloned;
}
}
因此,Object 在對某個對象實施 Clone 時,對其是一無所知的,它僅僅是簡單執(zhí)行域?qū)τ虻腃opy。 其中,對八種基本類型的克隆是沒有問題的,但當(dāng)對一個引用類型進(jìn)行克隆時,只是克隆了它的引用。因此,克隆對象和原始對象共享了同一個對象成員變量,故而提出了深克隆 : 在對整個對象淺克隆后,還需對其引用變量進(jìn)行克隆,并將其更新到淺克隆對象中去。
4、一個克隆的示例
在這里,我們通過一個簡單的例子來說明克隆在Java中的使用,如下所示:
// 父類 Employee
public class Employee implements Cloneable{
private String name;
private double salary;
private Date hireDay;
public Employee(String name, double salary, Date hireDay) {
this.name = name;
this.salary = salary;
this.hireDay = hireDay;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public Date getHireDay() {
return hireDay;
}
public void setHireDay(Date hireDay) {
this.hireDay = hireDay;
}
@Override
public Object clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((hireDay == null) ? 0 : hireDay.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
long temp;
temp = Double.doubleToLongBits(salary);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
if (hireDay == null) {
if (other.hireDay != null)
return false;
} else if (!hireDay.equals(other.hireDay))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (Double.doubleToLongBits(salary) != Double
.doubleToLongBits(other.salary))
return false;
return true;
}
@Override
public String toString() {
return name + " : " + String.valueOf(salary) + " : " + hireDay.toString();
}
}
// 子類 Manger
public class Manger extends Employee implements Cloneable {
private String edu;
public Manger(String name, double salary, Date hireDay, String edu) {
super(name, salary, hireDay);
this.edu = edu;
}
public String getEdu() {
return edu;
}
public void setEdu(String edu) {
this.edu = edu;
}
@Override
public String toString() {
return this.getName() + " : " + this.getSalary() + " : "
+ this.getHireDay() + " : " + this.getEdu();
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((edu == null) ? 0 : edu.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
Manger other = (Manger) obj;
if (edu == null) {
if (other.edu != null)
return false;
} else if (!edu.equals(other.edu))
return false;
return true;
}
public static void main(String[] args) throws CloneNotSupportedException {
Manger manger = new Manger("Rico", 20000.0, new Date(), "NEU");
// 輸出manger
System.out.println("Manger對象 = " + manger.toString());
Manger clonedManger = (Manger) manger.clone();
// 輸出克隆的manger
System.out.println("Manger對象的克隆對象 = " + clonedManger.toString());
System.out.println("Manger對象和其克隆對象是否相等: "
+ manger.equals(clonedManger) + "\r\n");
// 修改、輸出manger
manger.setEdu("TJU");
System.out.println("修改后的Manger對象 = " + manger.toString());
// 再次輸出manger
System.out.println("原克隆對象= " + clonedManger.toString());
System.out.println("修改后的Manger對象和原克隆對象是否相等: "
+ manger.equals(clonedManger));
}
}
/* Output:
Manger對象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
Manger對象的克隆對象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
Manger對象和其克隆對象是否相等: true
修改后的Manger對象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : TJU
原克隆對象= Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
修改后的Manger對象和原克隆對象是否相等: false
*///:
5、Clone()方法的保護(hù)機(jī)制
在Object中clone()是被申明為 protected 的,這樣做是有一定的道理的。以 Employee 類為例,如果我們在Employee中重寫了protected Object clone()方法, ,就大大限制了可以“克隆”Employee對象的范圍,即可以保證只有在和Employee類在同一包中類及Employee類的子類里面才能“克隆”Employee對象。進(jìn)一步地,如果我們沒有在Employee類重寫clone()方法,則只有Employee類及其子類才能夠“克隆”Employee對象。
這里面涉及到一個大家可能都會忽略的一個知識點,那就是關(guān)于protected的用法。實際上,很多的有關(guān)介紹Java語言的書籍,都對protected介紹的比較的簡單,就是:被protected修飾的成員或方法對于本包和其子類可見。這種說法有點太過含糊,常常會對大家造成誤解。
6、注意事項
Clone()方法的使用比較簡單,注意如下幾點即可:
什么時候使用shallow Clone,什么時候使用deep Clone?
這個主要看具體對象的域是什么性質(zhì)的,基本類型還是引用類型。調(diào)用Clone()方法的對象所屬的類(Class)必須實現(xiàn) Clonable 接口,否則在調(diào)用Clone方法的時候會拋出CloneNotSupportedException;
所有數(shù)組對象都實現(xiàn)了 Clonable 接口,默認(rèn)支持克?。?/p>
如果我們實現(xiàn)了 Clonable 接口,但沒有重寫Object類的clone方法,那么執(zhí)行域?qū)τ虻目截悾?/p>
明白 String 在克隆中的特殊性
String 在克隆時只是克隆了它的引用。
奇怪的是,在修改克隆后的 String 對象時,其原來的對象并未改變。原因是:String是在內(nèi)存中不可以被改變的對象。雖然在克隆時,源對象和克隆對象都指向了同一個String對象,但當(dāng)其中一個對象修改這個String對象的時候,會新分配一塊內(nèi)存用來保存修改后的String對象并將其引用指向新的String對象,而原來的String對象因為還存在指向它的引用,所以不會被回收。這樣,對于String而言,雖然是復(fù)制的引用,但是當(dāng)修改值的時候,并不會改變被復(fù)制對象的值。所以在使用克隆時,我們可以將 String類型 視為與基本類型,只需淺克隆即可。
String 總結(jié)
(1). 使用字面值形式創(chuàng)建字符串時,不一定會創(chuàng)建對象,但其引用一定指向位于字符串常量池的某個對象;
(2). 使用 new String(“…”)方式創(chuàng)建字符串時,一定會創(chuàng)建對象,甚至可能會同時創(chuàng)建兩個對象(一個位于字符串常量池中,一個位于堆中);
(3). String 對象是不可變的,對String 對象的任何改變都會導(dǎo)致一個新的 String 對象的產(chǎn)生,而不會影響到原String 對象;
(4). StringBuilder 與 StringBuffer 具有共同的父類,具有相同的API,分別適用于單線程和多線程環(huán)境下。特別地,在單線程環(huán)境下,StringBuilder 是 StringBuffer 的替代品,前者效率相對較高;
(5). 字符串比較時用的什么方法,內(nèi)部實現(xiàn)如何?
使用equals方法 : 先比較引用是否相同(是否是同一對象),再檢查是否為同一類型(str instanceof String), 最后比較內(nèi)容是否一致(String 的各個成員變量的值或內(nèi)容是否相同)。這也同樣適用于諸如 Integer 等的八種包裝器類。
