String 的聲明
由 JDK 中關(guān)于String的聲明可以知道:
- 不同字符串可能共享同一個(gè)底層char數(shù)組,例如字符串 String s=”abc” 與 s.substring(1) 就共享同一個(gè)char數(shù)組:char[] c = {‘a(chǎn)’,’b’,’c’}。其中宾抓,前者的 offset 和 count 的值分別為0和3蚜印,后者的 offset 和 count 的值分別為1和2。
- offset 和 count 兩個(gè)成員變量不是多余的岳遥,比如,在執(zhí)行substring操作時(shí)傅联。
注意:
- String不屬于八種基本數(shù)據(jù)類型先改,String 的實(shí)例是一個(gè)對(duì)象。因?yàn)閷?duì)象的默認(rèn)值是null蒸走,所以String的默認(rèn)值也是null仇奶;但它又是一種特殊的對(duì)象,有其它對(duì)象沒有的一些特性(String 的不可變性導(dǎo)致其像八種基本類型一樣载碌,比如猜嘱,作為方法參數(shù)時(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(“”)都是聲明一個(gè)新的空字符串弦撩,是空串不是null;
String 的不可變性
1. 不可變類
- 不可變類:所謂的不可變類是指這個(gè)類的實(shí)例一旦創(chuàng)建完成后步咪,就不能改變其成員變量值。如JDK內(nèi)部自帶的很多不可變類:Interger益楼、Long和String等猾漫。
- 可變類:相對(duì)于不可變類,可變類創(chuàng)建實(shí)例后可以改變其成員變量值感凤,開發(fā)中創(chuàng)建的大部分類都屬于可變類悯周。
2. 不可變類的設(shè)計(jì)方法
- 類添加final修飾符,保證類不被繼承陪竿。
如果類可以被繼承會(huì)破壞類的不可變性機(jī)制禽翼,只要繼承類覆蓋父類的方法并且繼承類可以改變成員變量值,那么一旦子類以父類的形式出現(xiàn)時(shí)族跛,不能保證當(dāng)前類是否可變闰挡。 - 保證所有成員變量必須私有,并且加上final修飾礁哄。
通過這種方式保證成員變量不可改變长酗。但只做到這一步還不夠,因?yàn)槿绻菍?duì)象成員變量有可能再外部改變其值桐绒。所以第4點(diǎn)彌補(bǔ)這個(gè)不足夺脾。 - 不提供改變成員變量的方法,包括setter
避免通過其他接口改變成員變量的值茉继,破壞不可變特性劳翰。 - 通過構(gòu)造器初始化所有成員,進(jìn)行深拷貝(deep copy)馒疹。
如果構(gòu)造器傳入的對(duì)象直接賦值給成員變量佳簸,還是可以通過對(duì)傳入對(duì)象的修改進(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對(duì)象的值來改變myArray內(nèi)部的值听想。
為了保證內(nèi)部的值不被修改,可以采用深度copy來創(chuàng)建一個(gè)新內(nèi)存保存?zhèn)魅氲闹德黼省U_做法:
public final class MyImmutableDemo {
private final int[] myArray;
public MyImmutableDemo(int[] array) {
this.myArray = array.clone();
}
}
- 在getter方法中汉买,不要直接返回對(duì)象本身,而是克隆對(duì)象佩脊,并返回對(duì)象的拷貝蛙粘。
這種做法也是防止對(duì)象外泄,防止通過getter獲得內(nèi)部可變成員對(duì)象后對(duì)成員變量直接操作威彰,導(dǎo)致成員變量發(fā)生改變出牧。
3. String對(duì)象的不可變性
string對(duì)象在內(nèi)存創(chuàng)建后就不可改變,不可變對(duì)象的創(chuàng)建一般滿足以上5個(gè)原則歇盼,我們看看String代碼是如何實(shí)現(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è)計(jì)細(xì)節(jié):
- String類被final修飾豹缀,不可繼承
- string內(nèi)部所有成員都設(shè)置為私有變量
- 不存在value的setter
- 并將value和offset設(shè)置為final伯复。
- 當(dāng)傳入可變數(shù)組value[]時(shí),進(jìn)行copy而不是直接將value[]復(fù)制給內(nèi)部變量.
- 獲取value時(shí)不是直接返回對(duì)象引用邢笙,而是返回對(duì)象的copy.
這都符合上面總結(jié)的不變類型的特性啸如,也保證了String類型是不可變的類。
4. String對(duì)象的不可變性的優(yōu)缺點(diǎn)
從上一節(jié)分析氮惯,String數(shù)據(jù)不可變類叮雳,那設(shè)置這樣的特性有什么好處呢?我總結(jié)為以下幾點(diǎn):
- 字符串常量池的需要.
字符串常量池可以將一些字符常量放在常量池中重復(fù)使用筐骇,避免每次都重新創(chuàng)建相同的對(duì)象债鸡、節(jié)省存儲(chǔ)空間。但如果字符串是可變的铛纬,此時(shí)相同內(nèi)容的String還指向常量池的同一個(gè)內(nèi)存空間厌均,當(dāng)某個(gè)變量改變了該內(nèi)存的值時(shí),其他遍歷的值也會(huì)發(fā)生改變告唆。所以不符合常量池設(shè)計(jì)的初衷棺弊。 - 線程安全考慮。
同一個(gè)字符串實(shí)例可以被多個(gè)線程共享擒悬。這樣便不用因?yàn)榫€程安全問題而使用同步模她。字符串自己便是線程安全的。 - 類加載器要用到字符串懂牧,不可變性提供了安全性侈净,以便正確的類被加載尊勿。譬如你想加載java.sql.Connection類,而這個(gè)值被改成了myhacked.Connection畜侦,那么會(huì)對(duì)你的數(shù)據(jù)庫造成不可知的破壞元扔。
- 支持hash映射和緩存。
因?yàn)樽址遣豢勺兊男牛栽谒鼊?chuàng)建的時(shí)候hashcode就被緩存了澎语,不需要重新計(jì)算。這就使得字符串很適合作為Map中的鍵验懊,字符串的處理速度要快過其它的鍵對(duì)象擅羞。這就是HashMap中的鍵往往都使用字符串。
缺點(diǎn):
如果有對(duì)String對(duì)象值改變的需求义图,那么會(huì)創(chuàng)建大量的String對(duì)象减俏。
5. String對(duì)象的是否真的不可變
雖然String對(duì)象將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對(duì)象上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改變value所引用的數(shù)組中的第5個(gè)字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
打印結(jié)果為:
s = Hello World
s = Hello_World
發(fā)現(xiàn)String的值已經(jīng)發(fā)生了改變骑晶。也就是說痛垛,通過反射是可以修改所謂的“不可變”對(duì)象的。
String 對(duì)象創(chuàng)建方式
- 字面值形式: JVM會(huì)自動(dòng)根據(jù)字符串常量池中字符串的實(shí)際情況來決定是否創(chuàng)建新對(duì)象 (要么不創(chuàng)建桶蛔,要么創(chuàng)建一個(gè)對(duì)象匙头,關(guān)鍵要看常量池中有沒有)
JDK 中明確指出:
String s = "abc";
等價(jià)于:
char data[] = {'a', 'b', 'c'};
String str = new String(data);
該種方式先在棧中創(chuàng)建一個(gè)對(duì)String類的對(duì)象引用變量s,然后去查找 “abc”是否被保存在字符串常量池中仔雷。若”abc”已經(jīng)被保存在字符串常量池中蹂析,則在字符串常量池中找到值為”abc”的對(duì)象,然后將s 指向這個(gè)對(duì)象; 否則碟婆,在 堆 中創(chuàng)建char數(shù)組 data电抚,然后在 堆 中創(chuàng)建一個(gè)String對(duì)象object,它由 data 數(shù)組支持竖共,緊接著這個(gè)String對(duì)象 object 被存放進(jìn)字符串常量池蝙叛,最后將 s 指向這個(gè)對(duì)象。
例如:
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 行代碼時(shí)公给,“kvill” 入池并被 s0 指向借帘;執(zhí)行第 2 行代碼時(shí),s1 從常量池查詢到” kvill” 對(duì)象并直接指向它淌铐;所以肺然,s0 和 s1 指向同一對(duì)象。 由于 ”kv” 和 ”ill” 都是字符串字面值腿准,所以 s2 在編譯期由編譯器直接解析為 “kvill”际起,所以 s2 也是常量池中”kvill”的一個(gè)引用。 所以,我們得出 s0==s1==s2;
- 通過 new 創(chuàng)建字符串對(duì)象 : 一概在堆中創(chuàng)建新對(duì)象街望,無論字符串字面值是否相等 (要么創(chuàng)建一個(gè)倦沧,要么創(chuàng)建兩個(gè)對(duì)象,關(guān)鍵要看常量池中有沒有)
String s = new String("abc");
等價(jià)于:
String original = "abc";
String s = new String(original);
所以它匕,通過 new 操作產(chǎn)生一個(gè)字符串(“abc”)時(shí)展融,會(huì)先去常量池中查找是否有“abc”對(duì)象,如果沒有豫柬,則創(chuàng)建一個(gè)此字符串對(duì)象并放入常量池中告希。然后,在堆中再創(chuàng)建“abc”對(duì)象烧给,并返回該對(duì)象的地址燕偶。所以,對(duì)于 String str=new String(“abc”):如果常量池中原來沒有”abc”础嫡,則會(huì)產(chǎn)生兩個(gè)對(duì)象(一個(gè)在常量池中指么,一個(gè)在堆中);否則榴鼎,產(chǎn)生一個(gè)對(duì)象伯诬。
用 new String() 創(chuàng)建的字符串對(duì)象位于堆中,而不是常量池中巫财。它們有自己獨(dú)立的地址空間盗似,例如:
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 指向運(yùn)行時(shí)創(chuàng)建的新對(duì)象”kvill”赫舒,二者指向不同的對(duì)象。對(duì)于s2闽瓢,因?yàn)楹蟀氩糠质?new String(“ill”)接癌,所以無法在編譯期確定,在運(yùn)行期會(huì) new 一個(gè) StringBuilder 對(duì)象扣讼, 并由 StringBuilder 的 append 方法連接并調(diào)用其 toString 方法返回一個(gè)新的 “kvill” 對(duì)象缺猛。此外,s3 的情形與 s2 一樣届谈,均含有編譯期無法確定的元素枯夜。因此,以上四個(gè) “kvill” 對(duì)象互不相同艰山。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)建的對(duì)象在大多數(shù)情形下會(huì)與源字符串 original 共享 char數(shù)組 。但是曙搬,什么情況下不會(huì)共享呢摔吏?
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()時(shí)發(fā)生了什么癣籽?
你可能知道,因?yàn)閤是不可變的滤祖,當(dāng)使用x.substring(1,3)對(duì)x賦值的時(shí)候筷狼,它會(huì)指向一個(gè)全新的字符串:
圖片1.png
然而,這個(gè)圖不是完全正確的表示堆中發(fā)生的事情匠童。因?yàn)樵趈dk6 和 jdk7中調(diào)用substring時(shí)發(fā)生的事情并不一樣埂材。
JDK 6中的substring
String是通過字符數(shù)組實(shí)現(xiàn)的。在jdk 6 中汤求,String類包含三個(gè)成員變量:char value[]俏险, int offset,int count扬绪。他們分別用來存儲(chǔ)真正的字符數(shù)組竖独,數(shù)組的第一個(gè)位置索引以及字符串中包含的字符個(gè)數(shù)。
當(dāng)調(diào)用substring方法的時(shí)候勒奇,會(huì)創(chuàng)建一個(gè)新的string對(duì)象预鬓,但是這個(gè)string的值仍然指向堆中的同一個(gè)字符數(shù)組巧骚。這兩個(gè)對(duì)象中只有count和offset 的值是不同的赊颠。
下面是證明上說觀點(diǎn)的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)致的問題
如果你有一個(gè)很長很長的字符串,但是當(dāng)你使用substring進(jìn)行切割的時(shí)候你只需要很短的一段劈彪。這可能導(dǎo)致性能問題竣蹦,因?yàn)槟阈枰闹皇且恍《巫址蛄校悄銋s引用了整個(gè)字符串(因?yàn)檫@個(gè)非常長的字符數(shù)組一直在被引用沧奴,所以無法被回收痘括,就可能導(dǎo)致內(nèi)存泄露)。在JDK 6中滔吠,一般用以下方式來解決該問題纲菌,原理其實(shí)就是生成一個(gè)新的字符串并引用他。
x = x.substring(x, y) + ""
JDK 7 中的substring
上面提到的問題疮绷,在jdk 7中得到解決翰舌。在jdk 7 中,substring方法會(huì)在堆內(nèi)存中創(chuàng)建一個(gè)新的數(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椅贱、字符串池
字符串的分配懂算,和其他的對(duì)象分配一樣,耗費(fèi)高昂的時(shí)間與空間代價(jià)庇麦。JVM為了提高性能和減少內(nèi)存開銷计技,在實(shí)例化字符串字面值的時(shí)候進(jìn)行了一些優(yōu)化。為了減少在JVM中創(chuàng)建的字符串的數(shù)量山橄,字符串類維護(hù)了一個(gè)字符串常量池垮媒,每當(dāng)以字面值形式創(chuàng)建一個(gè)字符串時(shí),JVM會(huì)首先檢查字符串常量池:如果字符串已經(jīng)存在池中航棱,就返回池中的實(shí)例引用涣澡;如果字符串不在池中,就會(huì)實(shí)例化一個(gè)字符串并放到池中丧诺。Java能夠進(jìn)行這樣的優(yōu)化是因?yàn)樽址遣豢?變的入桂,可以不用擔(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
}
}
一個(gè)初始為空的字符串池驳阎,它由類 String 私有地維護(hù)抗愁。當(dāng)以字面值形式創(chuàng)建一個(gè)字符串時(shí),總是先檢查字符串池是否含存在該對(duì)象呵晚,若存在蜘腌,則直接返回。此外饵隙,通過 new 操作符創(chuàng)建的字符串對(duì)象不指向字符串池中的任何對(duì)象撮珠。
2、手動(dòng)入池
一個(gè)初始為空的字符串池金矛,它由類 String 私有地維護(hù)芯急。 當(dāng)調(diào)用 intern 方法時(shí),如果池已經(jīng)包含一個(gè)等于此 String 對(duì)象的字符串(用 equals(Object) 方法確定)驶俊,則返回池中的字符串娶耍。否則,將此 String 對(duì)象添加到池中饼酿,并返回此 String 對(duì)象的引用榕酒。特別地,手動(dòng)入池遵循以下規(guī)則:
對(duì)于任意兩個(gè)字符串 s 和 t 故俐,當(dāng)且僅當(dāng) s.equals(t) 為 true 時(shí)想鹰,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
}
}
所以药版,對(duì)于 String str1 = “abc”辑舷,str1 引用的是 常量池(方法區(qū)) 的對(duì)象;而 String str2 = new String(“abc”)刚陡,str2引用的是 堆 中的對(duì)象惩妇,所以內(nèi)存地址不一樣株汉。但是由于內(nèi)容一樣,所以 str1 和 str3 指向同一對(duì)象歌殃。
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)致不同的原因是因?yàn)樵贘ava 7中常量池的位置從PermGen區(qū)改到了Java堆區(qū)中氓皱。
jdk1.6中 intern 方法會(huì)把首次遇到的字符串實(shí)例復(fù)制到永久待(常量池)中路召,并返回此引用;但在jdk1.7中波材,只是會(huì)把首次遇到的字符串實(shí)例的引用添加到常量池中(沒有復(fù)制)股淡,并返回此引用。
對(duì)于以上代碼中的str1.intern() 廷区,在jdk1.6中唯灵,會(huì)把“HollisChuang”這個(gè)字符串復(fù)制到常量池中,并返回他的引用隙轻。所以str1.intern()的值和str1的值埠帕,即兩個(gè)對(duì)象的地址是不一樣的。
對(duì)于以上代碼中的str1.intern() 玖绿,在jdk1.7中敛瓷,會(huì)把str1的引用保存到常量池中,并把這個(gè)引用返回斑匪。所以str1.intern()的值和str1的值是相等的呐籽。
擴(kuò)展另外一個(gè)例子:
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
以上代碼,無論在1.6還是1.7中都會(huì)返回false蚀瘸。
原因是"ja" + "va" = "java"狡蝶,這個(gè)常量在常量池中已經(jīng)有了,因?yàn)檫@個(gè)字符串在Java中隨處可見苍姜,曾經(jīng)被初始化過牢酵。所以,在1.7中str2.intern()返回的內(nèi)容是很久之前初始化的時(shí)候的那個(gè)引用衙猪,自然和剛剛創(chuàng)建的字符串的引用不相等。
- 實(shí)例
看下面幾個(gè)場(chǎng)景來深入理解 String布近。
1). 情景一:字符串常量池
Java虛擬機(jī)(JVM)中存在著一個(gè)字符串常量池垫释,其中保存著很多String對(duì)象,并且這些String對(duì)象可以被共享使用撑瞧,因此提高了效率棵譬。之所以字符串具有字符串常量池,是因?yàn)镾tring對(duì)象是不可變的预伺,因此可以被共享订咸。字符串常量池由String類維護(hù)曼尊,我們可以通過intern()方法使字符串池手動(dòng)入池。
String s1 = "abc";
//↑ 在字符串池創(chuàng)建了一個(gè)對(duì)象
String s2 = "abc";
//↑ 字符串pool已經(jīng)存在對(duì)象“abc”(共享),所以創(chuàng)建0個(gè)對(duì)象脏嚷,累計(jì)創(chuàng)建一個(gè)對(duì)象
System.out.println("s1 == s2 : "+(s1==s2));
//↑ true 指向同一個(gè)對(duì)象骆撇,
System.out.println("s1.equals(s2) : " + (s1.equals(s2)));
//↑ true 值相等
2). 情景二:關(guān)于new String(“…”)
String s3 = new String("abc");
//↑ 創(chuàng)建了兩個(gè)對(duì)象,一個(gè)存放在字符串池中父叙,一個(gè)存在與堆區(qū)中神郊;
//↑ 還有一個(gè)對(duì)象引用s3存放在棧中
String s4 = new String("abc");
//↑ 字符串池中已經(jīng)存在“abc”對(duì)象,所以只在堆中創(chuàng)建了一個(gè)對(duì)象
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ū)都不同,一個(gè)方法區(qū)甜癞,一個(gè)堆區(qū)
System.out.println("s1.equals(s3) : "+(s1.equals(s3)));
//↑true 值相同
通過 new String(“…”) 來創(chuàng)建字符串時(shí)夕晓,在該構(gòu)造函數(shù)的參數(shù)值為字符串字面值的前提下,若該字面值不在字符串常量池中悠咱,那么會(huì)創(chuàng)建兩個(gè)對(duì)象:一個(gè)在字符串常量池中运授,一個(gè)在堆中;否則乔煞,只會(huì)在堆中創(chuàng)建一個(gè)對(duì)象吁朦。對(duì)于不在同一區(qū)域的兩個(gè)對(duì)象,二者的內(nèi)存地址必定不同渡贾。
3). 情景三:字符串連接符“+”
String str2 = "ab"; //1個(gè)對(duì)象
String str3 = "cd"; //1個(gè)對(duì)象
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4 = str5 : " + (str4==str5)); // false
我們看這個(gè)例子逗宜,局部變量 str2,str3 指向字符串常量池中的兩個(gè)對(duì)象空骚。在運(yùn)行時(shí)纺讲,第三行代碼(str2+str3)實(shí)質(zhì)上會(huì)被分解成五個(gè)步驟,分別是:
(1). 調(diào)用 String 類的靜態(tài)方法 String.valueOf() 將 str2 轉(zhuǎn)換為字符串表示囤屹;
(2). JVM 在堆中創(chuàng)建一個(gè) StringBuilder對(duì)象熬甚,同時(shí)用str2指向轉(zhuǎn)換后的字符串對(duì)象進(jìn)行初始化;
(3). 調(diào)用StringBuilder對(duì)象的append方法完成與str3所指向的字符串對(duì)象的合并肋坚;
(4). 調(diào)用 StringBuilder 的 toString() 方法在堆中創(chuàng)建一個(gè) String對(duì)象乡括;
(5). 將剛剛生成的String對(duì)象的堆地址存賦給局部變量引用str4。
而引用str5指向的是字符串常量池中字面值”abcd”所對(duì)應(yīng)的字符串對(duì)象智厌。由上面的內(nèi)容我們可以知道诲泌,引用str4和str5指向的對(duì)象的地址必定不一樣。這時(shí)铣鹏,內(nèi)存中實(shí)際上會(huì)存在五個(gè)字符串對(duì)象: 三個(gè)在字符串常量池中的String對(duì)象敷扫、一個(gè)在堆中的String對(duì)象和一個(gè)在堆中的StringBuilder對(duì)象。
4). 情景四:字符串的編譯期優(yōu)化
String str1 = "ab" + "cd"; //1個(gè)對(duì)象
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為常量變量诚卸,編譯期會(huì)被優(yōu)化
String str6 = "b";
String str7 = "a" + str6;
String str67 = "ab";
System.out.println("str7 = str67 : "+ (str7 == str67)); // false
//↑str6為變量葵第,在運(yùn)行期才會(huì)被解析绘迁。
Java 編譯器對(duì)于類似“常量+字面值”的組合,其值在編譯的時(shí)候就能夠被確定了卒密。在這里缀台,str1 和 str9 的值在編譯時(shí)就可以被確定,因此它們分別等價(jià)于: String str1 = “abcd”; 和 String str9 = “abcd”;
Java 編譯器對(duì)于含有 “String引用”的組合栅受,則在運(yùn)行期會(huì)產(chǎn)生新的對(duì)象 (通過調(diào)用StringBuilder類的toString()方法)将硝,因此這個(gè)對(duì)象存儲(chǔ)在堆中。
4屏镊、小結(jié)
使用字面值形式創(chuàng)建的字符串與通過 new 創(chuàng)建的字符串一定是不同的依疼,因?yàn)槎叩拇鎯?chǔ)位置不同:前者在方法區(qū),后者在堆而芥;
我們?cè)谑褂弥T如String str = “abc”律罢;的格式創(chuàng)建字符串對(duì)象時(shí),總是想當(dāng)然地認(rèn)為棍丐,我們創(chuàng)建了String類的對(duì)象str误辑。但是事實(shí)上, 對(duì)象可能并沒有被創(chuàng)建歌逢。唯一可以肯定的是巾钉,指向 String 對(duì)象 的引用被創(chuàng)建了。至于這個(gè)引用到底是否指向了一個(gè)新的對(duì)象秘案,必須根據(jù)上下文來考慮砰苍;
字符串常量池的理念是享元模式
Java 編譯器對(duì) “常量+字面值” 的組合 是當(dāng)成常量表達(dá)式直接求值來優(yōu)化的;對(duì)于含有“String引用”的組合阱高,其在編譯期不能被確定赚导,會(huì)在運(yùn)行期創(chuàng)建新對(duì)象。
三大字符串類 : String赤惊、StringBuilder 和 StringBuffer
1. String 與 StringBuilder
簡要的說吼旧, String 類型 和 StringBuilder 類型的主要性能區(qū)別在于 String 是不可變的對(duì)象。 事實(shí)上未舟,在對(duì) String 類型進(jìn)行“改變”時(shí)圈暗,實(shí)質(zhì)上等同于生成了一個(gè)新的 String 對(duì)象,然后將指針指向新的 String 對(duì)象处面。由于頻繁的生成對(duì)象會(huì)對(duì)系統(tǒng)性能產(chǎn)生影響厂置,特別是當(dāng)內(nèi)存中沒有引用指向的對(duì)象多了以后,JVM 的垃圾回收器就會(huì)開始工作魂角,繼而會(huì)影響到程序的執(zhí)行效率。所以智绸,對(duì)于經(jīng)常改變內(nèi)容的字符串野揪,最好不要聲明為 String 類型访忿。但如果我們使用的是 StringBuilder 類,那么情形就不一樣了斯稳。因?yàn)楹C覀兊拿看涡薷亩际轻槍?duì) StringBuilder 對(duì)象本身的,而不會(huì)像對(duì)String操作那樣去生成新的對(duì)象并重新給變量引用賦值挣惰。所以卧斟,在一般情況下,推薦使用 StringBuilder 憎茂,特別是字符串對(duì)象經(jīng)常改變的情況下珍语。
在某些特別情況下,String 對(duì)象的字符串拼接可以直接被JVM 在編譯期確定下來竖幔,這時(shí)板乙,StringBuilder 在速度上就不占任何優(yōu)勢(shì)了。
因此拳氢,在絕大部分情況下募逞, 在效率方面:StringBuilder > String 。
2.StringBuffer 與 StringBuilder
首先需要明確的是馋评,StringBuffer 始于 JDK 1.0放接,而 StringBuilder 始于 JDK 5.0;此外留特,從 JDK 1.5 開始纠脾,對(duì)含有字符串變量 (非字符串字面值) 的連接操作(+),JVM 內(nèi)部是采用 StringBuilder 來實(shí)現(xiàn)的磕秤,而在這之前乳乌,這個(gè)操作是采用 StringBuffer 實(shí)現(xiàn)的。
JDK的實(shí)現(xiàn)中 StringBuffer 與 StringBuilder 都繼承自 AbstractStringBuilder市咆。AbstractStringBuilder的實(shí)現(xiàn)原理為:AbstractStringBuilder中采用一個(gè) char數(shù)組 來保存需要append的字符串汉操,char數(shù)組有一個(gè)初始大小,當(dāng)append的字符串長度超過當(dāng)前char數(shù)組容量時(shí)蒙兰,則對(duì)char數(shù)組進(jìn)行動(dòng)態(tài)擴(kuò)展磷瘤,即重新申請(qǐng)一段更大的內(nèi)存空間,然后將當(dāng)前char數(shù)組拷貝到新的位置搜变,因?yàn)橹匦路峙鋬?nèi)存并拷貝的開銷比較大采缚,所以每次重新申請(qǐng)內(nèi)存空間都是采用申請(qǐng)大于當(dāng)前需要的內(nèi)存空間的方式,這里是 2 倍挠他。
StringBuffer 和 StringBuilder 都是可變的字符序列扳抽,但是二者最大的一個(gè)不同點(diǎn)是:StringBuffer 是線程安全的,而 StringBuilder 則不是。StringBuilder 提供的API與StringBuffer的API是完全兼容的贸呢,即镰烧,StringBuffer 與 StringBuilder 中的方法和功能完全是等價(jià)的,但是后者一般要比前者快楞陷。因此怔鳖,可以這么說,StringBuilder 的提出就是為了在單線程環(huán)境下替換 StringBuffer 固蛾。
在單線程環(huán)境下结执,優(yōu)先使用 StringBuilder。
3.實(shí)例
1). 編譯時(shí)優(yōu)化與字符串連接符的本質(zhì)
我們先來看下面這個(gè)例子:
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)建等價(jià)于 String s = “abc”; 而變量s4的創(chuàng)建相當(dāng)于:
StringBuilder temp = new StringBuilder(s1);
temp.append(s2).append(s3);
String s4 = temp.toString();
但事實(shí)上,是不是這樣子呢览芳?我們將其反編譯一下斜姥,來看看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é)果,很好的印證了我們?cè)谏厦嫣岢龅淖址B接符的本質(zhì)沧竟。
2). 另一個(gè)例子:字符串連接符的本質(zhì)
由上面的分析結(jié)果铸敏,我們不難推斷出 String 采用連接運(yùn)算符(+)效率低下原因分析递宅,形如這樣的代碼:
public class Test {
public static void main(String args[]) {
String s = null;
for(int i = 0; i < 100; i++) {
s += "a";
}
}
}
會(huì)被編譯器編譯為:
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)生一個(gè) StringBuilder 對(duì)象,然后 append 后就扔掉累铅。下次循環(huán)再到達(dá)時(shí)糕非,再重新 new 一個(gè) StringBuilder 對(duì)象蒙具,然后 append 字符串,如此循環(huán)直至結(jié)束朽肥。事實(shí)上禁筏,如果我們直接采用 StringBuilder 對(duì)象進(jìn)行 append 的話,我們可以節(jié)省 N - 1 次創(chuàng)建和銷毀對(duì)象的時(shí)間衡招。所以篱昔,對(duì)于在循環(huán)中要進(jìn)行字符串連接的應(yīng)用,一般都是用StringBulider對(duì)象來進(jìn)行append操作始腾。
String 與 (深)克隆
1州刽、克隆的定義與意義
顧名思義,克隆就是制造一個(gè)對(duì)象的副本浪箭。一般地穗椅,根據(jù)所要克隆的對(duì)象的成員變量中是否含有引用類型,可以將克隆分為兩種:淺克隆(Shallow Clone) 和 深克隆(Deep Clone)奶栖,默認(rèn)情況下使用Object中的clone方法進(jìn)行克隆就是淺克隆匹表,即完成對(duì)象域?qū)τ虻目截悺?br>
(1). Object 中的 clone() 方法
在使用clone()方法時(shí)门坷,若該類未實(shí)現(xiàn) Cloneable 接口,則拋出 java.lang.CloneNotSupportedException 異常桑孩。下面我們以Employee這個(gè)例子進(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 接口是一個(gè)標(biāo)識(shí)性接口拜鹤,即該接口不包含任何方法(甚至沒有clone()方法)框冀,但是如果一個(gè)類想合法的進(jìn)行克隆流椒,那么就必須實(shí)現(xiàn)這個(gè)接口。下面我們看JDK對(duì)它的描述:
- 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)在有一個(gè)Employee對(duì)象宣虾,Employee tobby = new Employee(“CMTobby”,5000),通常, 我們會(huì)有這樣的賦值Employee tom=tobby温数,這個(gè)時(shí)候只是簡單了copy了一下reference绣硝,tom 和 tobby 都指向內(nèi)存中同一個(gè)object,這樣tom或者tobby對(duì)對(duì)象的修改都會(huì)影響到對(duì)方撑刺。打個(gè)比方鹉胖,如果我們通過tom.raiseSalary()方法改變了salary域的值,那么tobby通過getSalary()方法得到的就是修改之后的salary域的值够傍,顯然這不是我們?cè)敢饪吹降母ΣぁH绻覀兿M玫絫obby所指向的對(duì)象的一個(gè)精確拷貝,同時(shí)兩者互不影響冕屯,那么我們就可以使用Clone來滿足我們的需求寂诱。Employee cindy=tobby.clone(),這時(shí)會(huì)生成一個(gè)新的Employee對(duì)象安聘,并且和tobby具有相同的屬性值和方法痰洒。
3、Shallow Clone & Deep Clone
這個(gè)時(shí)候点骑,我們就需要進(jìn)行 Deep Clone 了酣难,以便對(duì)那些引用類型的域進(jìn)行特殊的處理,例如本例中的hireDay黑滴。我們可以重新定義 clone方法憨募,對(duì)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 在對(duì)某個(gè)對(duì)象實(shí)施 Clone 時(shí),對(duì)其是一無所知的晚缩,它僅僅是簡單執(zhí)行域?qū)τ虻腃opy尾膊。 其中,對(duì)八種基本類型的克隆是沒有問題的荞彼,但當(dāng)對(duì)一個(gè)引用類型進(jìn)行克隆時(shí)冈敛,只是克隆了它的引用。因此鸣皂,克隆對(duì)象和原始對(duì)象共享了同一個(gè)對(duì)象成員變量抓谴,故而提出了深克隆 : 在對(duì)整個(gè)對(duì)象淺克隆后,還需對(duì)其引用變量進(jìn)行克隆寞缝,并將其更新到淺克隆對(duì)象中去癌压。
4、一個(gè)克隆的示例
在這里第租,我們通過一個(gè)簡單的例子來說明克隆在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對(duì)象 = " + manger.toString());
Manger clonedManger = (Manger) manger.clone();
// 輸出克隆的manger
System.out.println("Manger對(duì)象的克隆對(duì)象 = " + clonedManger.toString());
System.out.println("Manger對(duì)象和其克隆對(duì)象是否相等: "
+ manger.equals(clonedManger) + "\r\n");
// 修改、輸出manger
manger.setEdu("TJU");
System.out.println("修改后的Manger對(duì)象 = " + manger.toString());
// 再次輸出manger
System.out.println("原克隆對(duì)象= " + clonedManger.toString());
System.out.println("修改后的Manger對(duì)象和原克隆對(duì)象是否相等: "
+ manger.equals(clonedManger));
}
}
/* Output:
Manger對(duì)象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
Manger對(duì)象的克隆對(duì)象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
Manger對(duì)象和其克隆對(duì)象是否相等: true
修改后的Manger對(duì)象 = Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : TJU
原克隆對(duì)象= Rico : 20000.0 : Mon Mar 13 15:36:03 CST 2017 : NEU
修改后的Manger對(duì)象和原克隆對(duì)象是否相等: false
*///:
5慎宾、Clone()方法的保護(hù)機(jī)制
在Object中clone()是被申明為 protected 的丐吓,這樣做是有一定的道理的。以 Employee 類為例趟据,如果我們?cè)贓mployee中重寫了protected Object clone()方法券犁, ,就大大限制了可以“克隆”Employee對(duì)象的范圍汹碱,即可以保證只有在和Employee類在同一包中類及Employee類的子類里面才能“克隆”Employee對(duì)象粘衬。進(jìn)一步地,如果我們沒有在Employee類重寫clone()方法咳促,則只有Employee類及其子類才能夠“克隆”Employee對(duì)象稚新。
這里面涉及到一個(gè)大家可能都會(huì)忽略的一個(gè)知識(shí)點(diǎn),那就是關(guān)于protected的用法跪腹。實(shí)際上褂删,很多的有關(guān)介紹Java語言的書籍,都對(duì)protected介紹的比較的簡單冲茸,就是:被protected修飾的成員或方法對(duì)于本包和其子類可見屯阀。這種說法有點(diǎn)太過含糊缅帘,常常會(huì)對(duì)大家造成誤解。
6难衰、注意事項(xiàng)
Clone()方法的使用比較簡單钦无,注意如下幾點(diǎn)即可:
什么時(shí)候使用shallow Clone,什么時(shí)候使用deep Clone盖袭?
這個(gè)主要看具體對(duì)象的域是什么性質(zhì)的失暂,基本類型還是引用類型。調(diào)用Clone()方法的對(duì)象所屬的類(Class)必須實(shí)現(xiàn) Clonable 接口苍凛,否則在調(diào)用Clone方法的時(shí)候會(huì)拋出CloneNotSupportedException趣席;
所有數(shù)組對(duì)象都實(shí)現(xiàn)了 Clonable 接口,默認(rèn)支持克麓己;
如果我們實(shí)現(xiàn)了 Clonable 接口想罕,但沒有重寫Object類的clone方法悠栓,那么執(zhí)行域?qū)τ虻目截悾?/p>
明白 String 在克隆中的特殊性
String 在克隆時(shí)只是克隆了它的引用。
奇怪的是按价,在修改克隆后的 String 對(duì)象時(shí)惭适,其原來的對(duì)象并未改變。原因是:String是在內(nèi)存中不可以被改變的對(duì)象楼镐。雖然在克隆時(shí)癞志,源對(duì)象和克隆對(duì)象都指向了同一個(gè)String對(duì)象,但當(dāng)其中一個(gè)對(duì)象修改這個(gè)String對(duì)象的時(shí)候框产,會(huì)新分配一塊內(nèi)存用來保存修改后的String對(duì)象并將其引用指向新的String對(duì)象凄杯,而原來的String對(duì)象因?yàn)檫€存在指向它的引用,所以不會(huì)被回收秉宿。這樣戒突,對(duì)于String而言,雖然是復(fù)制的引用描睦,但是當(dāng)修改值的時(shí)候膊存,并不會(huì)改變被復(fù)制對(duì)象的值。所以在使用克隆時(shí)忱叭,我們可以將 String類型 視為與基本類型隔崎,只需淺克隆即可。
String 總結(jié)
(1). 使用字面值形式創(chuàng)建字符串時(shí)韵丑,不一定會(huì)創(chuàng)建對(duì)象爵卒,但其引用一定指向位于字符串常量池的某個(gè)對(duì)象;
(2). 使用 new String(“…”)方式創(chuàng)建字符串時(shí)埂息,一定會(huì)創(chuàng)建對(duì)象技潘,甚至可能會(huì)同時(shí)創(chuàng)建兩個(gè)對(duì)象(一個(gè)位于字符串常量池中遥巴,一個(gè)位于堆中);
(3). String 對(duì)象是不可變的享幽,對(duì)String 對(duì)象的任何改變都會(huì)導(dǎo)致一個(gè)新的 String 對(duì)象的產(chǎn)生铲掐,而不會(huì)影響到原String 對(duì)象;
(4). StringBuilder 與 StringBuffer 具有共同的父類值桩,具有相同的API摆霉,分別適用于單線程和多線程環(huán)境下。特別地奔坟,在單線程環(huán)境下携栋,StringBuilder 是 StringBuffer 的替代品,前者效率相對(duì)較高咳秉;
(5). 字符串比較時(shí)用的什么方法婉支,內(nèi)部實(shí)現(xiàn)如何?
使用equals方法 : 先比較引用是否相同(是否是同一對(duì)象)澜建,再檢查是否為同一類型(str instanceof String)向挖, 最后比較內(nèi)容是否一致(String 的各個(gè)成員變量的值或內(nèi)容是否相同)。這也同樣適用于諸如 Integer 等的八種包裝器類炕舵。