String詳解

String 的聲明

String的定義.png

由 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ì)方法

  1. 類添加final修飾符,保證類不被繼承陪竿。
    如果類可以被繼承會(huì)破壞類的不可變性機(jī)制禽翼,只要繼承類覆蓋父類的方法并且繼承類可以改變成員變量值,那么一旦子類以父類的形式出現(xiàn)時(shí)族跛,不能保證當(dāng)前類是否可變闰挡。
  2. 保證所有成員變量必須私有,并且加上final修飾礁哄。
    通過這種方式保證成員變量不可改變长酗。但只做到這一步還不夠,因?yàn)槿绻菍?duì)象成員變量有可能再外部改變其值桐绒。所以第4點(diǎn)彌補(bǔ)這個(gè)不足夺脾。
  3. 不提供改變成員變量的方法,包括setter
    避免通過其他接口改變成員變量的值茉继,破壞不可變特性劳翰。
  4. 通過構(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();   
    }   
}
  1. 在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):

  1. 字符串常量池的需要.
    字符串常量池可以將一些字符常量放在常量池中重復(fù)使用筐骇,避免每次都重新創(chuàng)建相同的對(duì)象债鸡、節(jié)省存儲(chǔ)空間。但如果字符串是可變的铛纬,此時(shí)相同內(nèi)容的String還指向常量池的同一個(gè)內(nèi)存空間厌均,當(dāng)某個(gè)變量改變了該內(nèi)存的值時(shí),其他遍歷的值也會(huì)發(fā)生改變告唆。所以不符合常量池設(shè)計(jì)的初衷棺弊。
  2. 線程安全考慮。
    同一個(gè)字符串實(shí)例可以被多個(gè)線程共享擒悬。這樣便不用因?yàn)榫€程安全問題而使用同步模她。字符串自己便是線程安全的。
  3. 類加載器要用到字符串懂牧,不可變性提供了安全性侈净,以便正確的類被加載尊勿。譬如你想加載java.sql.Connection類,而這個(gè)值被改成了myhacked.Connection畜侦,那么會(huì)對(duì)你的數(shù)據(jù)庫造成不可知的破壞元扔。
  4. 支持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)建方式

  1. 字面值形式: 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;

  1. 通過 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()方法的一些問題

  1. 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
  1. 調(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 的值是不同的赊颠。


圖片2.png

下面是證明上說觀點(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ù)組冬骚。
圖片3.png

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)建的字符串的引用不相等。

  1. 實(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() 方法

20170313093548412.png

在使用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

  Clone是如何完成的呢浴韭?Object中的clone()方法在對(duì)某個(gè)對(duì)象實(shí)施克隆時(shí)對(duì)其是一無所知的丘喻,它僅僅是簡單地執(zhí)行域?qū)τ虻腸opy,這就是Shallow Clone念颈。這樣泉粉,問題就來了,以Employee為例舍肠,它里面有一個(gè)域hireDay不是基本類型的變量搀继,而是一個(gè)reference變量,經(jīng)過Clone之后克隆類只會(huì)產(chǎn)生一個(gè)新的Date類型的引用翠语,它和原始引用都指向同一個(gè) Date 對(duì)象叽躯,這樣克隆類就和原始類共享了一部分信息,顯然這種情況不是我們?cè)敢饪吹降募±ǎ^程下圖所示:
clone.png

這個(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 等的八種包裝器類炕舵。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末何之,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子咽筋,更是在濱河造成了極大的恐慌溶推,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奸攻,死亡現(xiàn)場(chǎng)離奇詭異蒜危,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)舞箍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門舰褪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人疏橄,你說我怎么就攤上這事占拍。” “怎么了捎迫?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵晃酒,是天一觀的道長。 經(jīng)常有香客問我窄绒,道長贝次,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任彰导,我火速辦了婚禮蛔翅,結(jié)果婚禮上敲茄,老公的妹妹穿的比我還像新娘。我一直安慰自己山析,他們只是感情好堰燎,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著笋轨,像睡著了一般秆剪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上爵政,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天仅讽,我揣著相機(jī)與錄音,去河邊找鬼钾挟。 笑死洁灵,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的等龙。 我是一名探鬼主播处渣,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蛛砰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起黍衙,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤泥畅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后琅翻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體位仁,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年方椎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了聂抢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡棠众,死狀恐怖琳疏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闸拿,我是刑警寧澤空盼,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站新荤,受9級(jí)特大地震影響揽趾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苛骨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一篱瞎、第九天 我趴在偏房一處隱蔽的房頂上張望苟呐。 院中可真熱鬧,春花似錦俐筋、人聲如沸牵素。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽两波。三九已至,卻和暖如春闷哆,著一層夾襖步出監(jiān)牢的瞬間腰奋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來泰國打工抱怔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留劣坊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓屈留,卻偏偏與公主長得像局冰,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子灌危,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 使用Java語言進(jìn)行編程康二,我們每天都要用到String類,但是以前只是拿來就用勇蝙,并不知道String類的實(shí)現(xiàn)原理和...
    lunabird閱讀 433評(píng)論 0 1
  • 從網(wǎng)上復(fù)制的沫勿,看別人的比較全面,自己搬過來味混,方便以后查找产雹。原鏈接:https://www.cnblogs.com/...
    lxtyp閱讀 1,345評(píng)論 0 9
  • 一 : String內(nèi)存解析 案例1 內(nèi)存分析 : str1->存放在字符串常量池中 假設(shè)地址為 0x12138 ...
    TianTianBaby223閱讀 1,723評(píng)論 4 1
  • 【姓名】吳從嚴(yán) 【導(dǎo)師】袁文魁 【分舵】文魁派第一分舵 【舵主】劉麗瓊 【導(dǎo)圖解說】 這堂課的主題是如何運(yùn)用雙值分...
    希波克拉底先生閱讀 448評(píng)論 3 0
  • 序章 人這一生都會(huì)有一些你不敢做的事,比如說:第一翁锡,當(dāng)你想追你喜歡的那個(gè)男孩子時(shí)蔓挖,你會(huì)錯(cuò)過;第二是喜歡他馆衔,但卻...
    仲夜夢(mèng)閱讀 141評(píng)論 0 0