主頁 > 後端開發 > 原來只想簡單看一下String原始碼,沒想到整理了這么多知識點

原來只想簡單看一下String原始碼,沒想到整理了這么多知識點

2020-12-18 06:44:20 後端開發

原來只想簡單看一下String原始碼,沒想到整理了這么多知識點

不知道大家有沒有這樣得經歷,就是無意中點進去得一個業面,然后鉆到里面瀏覽了好久,我就是這樣得,今天無意中,ctrl+左鍵,就點進了string得原始碼,正好今天下午沒啥事,就在里面看一下,沒想到,下次緩過來,就是我同事拍我讓我去吃飯,哈哈哈哈,不過好處就是,我這邊也整理了一些string類得知識點,也分享給大家,整理得不好還望海涵

文章首發個人公眾號:Java架構師聯盟,每日更新技術好文

一、String類

想要了解一個類,最好的辦法就是看這個類的實作源代碼,來看一下String類的原始碼:

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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    ........
}

從上面可以看出幾點:

1)String類是final類,也即意味著String類不能被繼承,并且它的成員方法都默認為final方法,在Java中,被final修飾的類是不允許被繼承的,并且該類中的成員方法都默認為final方法,

2)上面列舉出了String類中所有的成員屬性,從上面可以看出String類其實是通過char陣列來保存字串的,

下面再繼續看String類的一些方法實作:

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
}

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    char buf[] = new char[count + otherLen];
    getChars(0, count, buf, 0);
    str.getChars(0, otherLen, buf, count);
    return new String(0, count + otherLen, buf);
}

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = count;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */
        int off = offset;   /* avoid getfield opcode */

        while (++i < len) {
        if (val[off + i] == oldChar) {
            break;
        }
        }
        if (i < len) {
        char buf[] = new char[len];
        for (int j = 0 ; j < i ; j++) {
            buf[j] = val[off+j];
        }
        while (i < len) {
            char c = val[off + i];
            buf[i] = (c == oldChar) ? newChar : c;
            i++;
        }
        return new String(0, len, buf);
        }
    }
    return this;
}

從上面的三個方法可以看出,無論是sub操、concat還是replace操作都不是在原有的字串上進行的,而是重新生成了一個新的字串物件,也就是說進行這些操作后,最原始的字串并沒有被改變,

在這里要永遠記住一點:“String物件一旦被創建就是固定不變的了,對String物件的任何改變都不影響到原物件,相關的任何change操作都會生成新的物件”

二、字串常量池

我們知道字串的分配和其他物件分配一樣,是需要消耗高昂的時間和空間的,而且字串我們使用的非常多,JVM為了提高性能和減少記憶體的開銷,在實體化字串的時候進行了一些優化:使用字串常量池每當我們創建字串常量時,JVM會首先檢查字串常量池,如果該字串已經存在常量池中,那么就直接回傳常量池中的實體參考,如果字串不存在常量池中,就會實體化該字串并且將其放到常量池中,由于String字串的不可變性我們可以十分肯定常量池中一定不存在兩個相同的字串(這點對理解上面至關重要),

Java中的常量池,實際上分為兩種形態:靜態常量池運行時常量池
所謂靜態常量池,即*.class檔案中的常量池,class檔案中的常量池不僅僅包含字串(數字)字面量,還包含類、方法的資訊,占用class檔案絕大部分空間,
運行時常量池,則是jvm虛擬機在完成類裝載操作后,將class檔案中的常量池載入到記憶體中,并保存在方法區中,我們常說的常量池,就是指方法區中的運行時常量池,

來看下面的程式:

String a = "chenssy";
String b = "chenssy";

a、b和字面上的chenssy都是指向JVM字串常量池中的"chenssy"物件,他們指向同一個物件,

String c = new String("chenssy");

new關鍵字一定會產生一個物件chenssy(注意這個chenssy和上面的chenssy不同),同時這個物件是存盤在堆中,所以上面應該產生了兩個物件:保存在堆疊中的c和保存堆中chenssy,但是在Java中根本就不存在兩個完全一模一樣的字串物件,故堆中的chenssy應該是參考字串常量池中chenssy,所以c、chenssy、池chenssy的關系應該是:c--->chenssy--->池chenssy,整個關系如下:

原來只想簡單看一下String原始碼,沒想到整理了這么多知識點

通過上面的圖我們可以非常清晰的認識他們之間的關系,所以我們修改記憶體中的值,他變化的是所有,

總結:雖然a、b、c、chenssy是不同的物件,但是從String的內部結構我們是可以理解上面的,String c = new String("chenssy");雖然c的內容是創建在堆中,但是他的內部value還是指向JVM常量池的chenssy的value,它構造chenssy時所用的引數依然是chenssy字串常量,

下面再來看幾個例子:

例子1:

/**
 * 采用字面值的方式賦值
 */
public void test1(){
    String str1="aaa";
    String str2="aaa";
    System.out.println("===========test1============");
    System.out.println(str1==str2);//true 可以看出str1跟str2是指向同一個物件 
}

執行上述代碼,結果為:true,
分析:當執行String str1="aaa"時,JVM首先會去字串池中查找是否存在"aaa"這個物件,如果不存在,則在字串池中創建"aaa"這個物件,然后將池中"aaa"這個物件的參考地址回傳給字串常量str1,這樣str1會指向池中"aaa"這個字串物件;如果存在,則不創建任何物件,直接將池中"aaa"這個物件的地址回傳,賦給字串常量,當創建字串物件str2時,字串池中已經存在"aaa"這個物件,直接把物件"aaa"的參考地址回傳給str2,這樣str2指向了池中"aaa"這個物件,也就是說str1和str2指向了同一個物件,因此陳述句System.out.println(str1 == str2)輸出:true,

例子2:

/**
 * 采用new關鍵字新建一個字串物件
 */
public void test2(){
    String str3=new String("aaa");
    String str4=new String("aaa");
    System.out.println("===========test2============");
    System.out.println(str3==str4);//false 可以看出用new的方式是生成不同的物件 
}

執行上述代碼,結果為:false,

分析: 采用new關鍵字新建一個字串物件時,JVM首先在字串池中查找有沒有"aaa"這個字串物件,如果有,則不在池中再去創建"aaa"這個物件了,直接在堆中創建一個"aaa"字串物件,然后將堆中的這個"aaa"物件的地址回傳賦給參考str3,這樣,str3就指向了堆中創建的這個"aaa"字串物件;如果沒有,則首先在字串池中創建一個"aaa"字串物件,然后再在堆中創建一個"aaa"字串物件,然后將堆中這個"aaa"字串物件的地址回傳賦給str3參考,這樣,str3指向了堆中創建的這個"aaa"字串物件,當執行String str4=new String("aaa")時, 因為采用new關鍵字創建物件時,每次new出來的都是一個新的物件,也即是說參考str3和str4指向的是兩個不同的物件,因此陳述句System.out.println(str3 == str4)輸出:false,

例子3:

/**
 * 編譯期確定
 */
public void test3(){
    String s0="helloworld";
    String s1="helloworld";
    String s2="hello"+"world";
    System.out.println("===========test3============");
    System.out.println(s0==s1); //true 可以看出s0跟s1是指向同一個物件 
    System.out.println(s0==s2); //true 可以看出s0跟s2是指向同一個物件 
}

執行上述代碼,結果為:true、true,

分析:因為例子中的s0和s1中的"helloworld”都是字串常量,它們在編譯期就被確定了,所以s0s1為true;而"hello”和"world”也都是字串常量,當一個字串由多個字串常量連接而成時,它自己肯定也是字串常量,所以s2也同樣在編譯期就被決議為一個字串常量,所以s2也是常量池中"helloworld”的一個參考,所以我們得出s0s1==s2,

例子4:

/**
 * 編譯期無法確定
 */
public void test4(){
    String s0="helloworld"; 
    String s1=new String("helloworld"); 
    String s2="hello" + new String("world"); 
    System.out.println("===========test4============");
    System.out.println( s0==s1 ); //false  
    System.out.println( s0==s2 ); //false 
    System.out.println( s1==s2 ); //false
}

執行上述代碼,結果為:false、false、false,

分析:用new String() 創建的字串不是常量,不能在編譯期就確定,所以new String() 創建的字串不放入常量池中,它們有自己的地址空間,

s0還是常量池中"helloworld”的參考,s1因為無法在編譯期確定,所以是運行時創建的新物件"helloworld”的參考,s2因為有后半部分new String(”world”)所以也無法在編譯期確定,所以也是一個新創建物件"helloworld”的參考,

例子5:

/**
 * 繼續-編譯期無法確定
 */
public void test5(){
    String str1="abc";   
    String str2="def";   
    String str3=str1+str2;
    System.out.println("===========test5============");
    System.out.println(str3=="abcdef"); //false
}

執行上述代碼,結果為:false,

分析:因為str3指向堆中的"abcdef"物件,而"abcdef"是字串池中的物件,所以結果為false,JVM對String str="abc"物件放在常量池中是在編譯時做的,而String str3=str1+str2是在運行時刻才能知道的,new物件也是在運行時才做的,而這段代碼總共創建了5個物件,字串池中兩個、堆中三個,+運算子會在堆中建立來兩個String物件,這兩個物件的值分別是"abc"和"def",也就是說從字串池中復制這兩個值,然后在堆中創建兩個物件,然后再建立物件str3,然后將"abcdef"的堆地址賦給str3,

步驟:
1)堆疊中開辟一塊中間存放參考str1,str1指向池中String常量"abc",
2)堆疊中開辟一塊中間存放參考str2,str2指向池中String常量"def",
3)堆疊中開辟一塊中間存放參考str3,
4)str1 + str2通過StringBuilder的最后一步toString()方法還原一個新的String物件"abcdef",因此堆中開辟一塊空間存放此物件,
5)參考str3指向堆中(str1 + str2)所還原的新String物件,
6)str3指向的物件在堆中,而常量"abcdef"在池中,輸出為false,

例子6:

/**
 * 編譯期優化
 */
public void test6(){
    String s0 = "a1"; 
    String s1 = "a" + 1; 
    System.out.println("===========test6============");
    System.out.println((s0 == s1)); //result = true  
    String s2 = "atrue"; 
    String s3= "a" + "true"; 
    System.out.println((s2 == s3)); //result = true  
    String s4 = "a3.4"; 
    String s5 = "a" + 3.4; 
    System.out.println((s4 == s5)); //result = true
}

執行上述代碼,結果為:true、true、true,

分析:在程式編譯期,JVM就將常量字串的"+"連接優化為連接后的值,拿"a" + 1來說,經編譯器優化后在class中就已經是a1,在編譯期其字串常量的值就確定下來,故上面程式最終的結果都為true,

/**
 * 編譯期無法確定
 */
public void test7(){
    String s0 = "ab"; 
    String s1 = "b"; 
    String s2 = "a" + s1; 
    System.out.println("===========test7============");
    System.out.println((s0 == s2)); //result = false
}

執行上述代碼,結果為:false,

分析:JVM對于字串參考,由于在字串的"+"連接中,有字串參考存在,而參考的值在程式編譯期是無法確定的,即"a" + s1無法被編譯器優化,只有在程式運行期來動態分配并將連接后的新地址賦給s2,所以上面程式的結果也就為false,

例子8:

/**
 * 比較字串常量的“+”和字串參考的“+”的區別
 */
public void test8(){
    String test="javalanguagespecification";
    String str="java";
    String str1="language";
    String str2="specification";
    System.out.println("===========test8============");
    System.out.println(test == "java" + "language" + "specification");
    System.out.println(test == str + str1 + str2);
}

執行上述代碼,結果為:true、false,

分析:為什么出現上面的結果呢?這是因為,字串字面量拼接操作是在Java編譯器編譯期間就執行了,也就是說編譯器編譯時,直接把"java"、"language"和"specification"這三個字面量進行"+"操作得到一個"javalanguagespecification" 常量,并且直接將這個常量放入字串池中,這樣做實際上是一種優化,將3個字面量合成一個,避免了創建多余的字串物件,而字串參考的"+"運算是在Java運行期間執行的,即str + str2 + str3在程式執行期間才會進行計算,它會在堆記憶體中重新創建一個拼接后的字串物件,總結來說就是:字面量"+"拼接是在編譯期間進行的,拼接后的字串存放在字串池中;而字串參考的"+"拼接運算實在運行時進行的,新創建的字串存放在堆中,

對于直接相加字串,效率很高,因為在編譯器便確定了它的值,也就是說形如"I"+"love"+"java"; 的字串相加,在編譯期間便被優化成了"Ilovejava",對于間接相加(即包含字串參考),形如s1+s2+s3; 效率要比直接相加低,因為在編譯器不會對參考變數進行優化,

例子9:

/**
 * 編譯期確定
 */
public void test9(){
    String s0 = "ab"; 
    final String s1 = "b"; 
    String s2 = "a" + s1;  
    System.out.println("===========test9============");
    System.out.println((s0 == s2)); //result = true
}

執行上述代碼,結果為:true,

分析:和例子7中唯一不同的是s1字串加了final修飾,對于final修飾的變數,它在編譯時被決議為常量值的一個本地拷貝存盤到自己的常量池中或嵌入到它的位元組碼流中,所以此時的"a" + s1和"a" + "b"效果是一樣的,故上面程式的結果為true,

例子10:

/**
 * 編譯期無法確定
 */
public void test10(){
    String s0 = "ab"; 
    final String s1 = getS1(); 
    String s2 = "a" + s1; 
    System.out.println("===========test10============");
    System.out.println((s0 == s2)); //result = false 
    
}

private static String getS1() {  
    return "b";   
}

執行上述代碼,結果為:false,

分析:這里面雖然將s1用final修飾了,但是由于其賦值是通過方法呼叫回傳的,那么它的值只能在運行期間確定,因此s0和s2指向的不是同一個物件,故上面程式的結果為false,

三、總結

1.String類初始化后是不可變的(immutable)

String使用private final char value[]來實作字串的存盤,也就是說String物件創建之后,就不能再修改此物件中存盤的字串內容,就是因為如此,才說String型別是不可變的(immutable),程式員不能對已有的不可變物件進行修改,我們自己也可以創建不可變物件,只要在介面中不提供修改資料的方法就可以,
然而,String類物件確實有編輯字串的功能,比如replace(),這些編輯功能是通過創建一個新的物件來實作的,而不是對原有物件進行修改,比如:

s = s.replace("World", "Universe");

上面對s.replace()的呼叫將創建一個新的字串"Hello Universe!",并回傳該物件的參考,通過賦值,參考s將指向該新的字串,如果沒有其他參考指向原有字串"Hello World!",原字串物件將被垃圾回收,

原來只想簡單看一下String原始碼,沒想到整理了這么多知識點

2.參考變數與物件

A aa;
這個陳述句宣告一個類A的參考變數aa[我們常常稱之為句柄],而物件一般通過new創建,所以aa僅僅是一個參考變數,它不是物件,

3.創建字串的方式

創建字串的方式歸納起來有兩類:

(1)使用""引號創建字串;

(2)使用new關鍵字創建字串,

結合上面例子,總結如下:

(1)單獨使用""引號創建的字串都是常量,編譯期就已經確定存盤到String Pool中;

(2)使用new String("")創建的物件會存盤到heap中,是運行期新創建的;

new創建字串時首先查看池中是否有相同值的字串,如果有,則拷貝一份到堆中,然后回傳堆中的地址;如果池中沒有,則在堆中創建一份,然后回傳堆中的地址(注意,此時不需要從堆中復制到池中,否則,將使得堆中的字串永遠是池中的子集,導致浪費池的空間)!

(3)使用只包含常量的字串連接符如"aa" + "aa"創建的也是常量,編譯期就能確定,已經確定存盤到String Pool中;

(4)使用包含變數的字串連接符如"aa" + s1創建的物件是運行期才創建的,存盤在heap中;

4.使用String不一定創建物件

在執行到雙引號包含字串的陳述句時,如String a = "123",JVM會先到常量池里查找,如果有的話回傳常量池里的這個實體的參考,否則的話創建一個新實體并置入常量池里,所以,當我們在使用諸如String str = "abc";的格式定義物件時,總是想當然地認為,創建了String類的物件str,擔心陷阱!物件可能并沒有被創建!而可能只是指向一個先前已經創建的物件,只有通過new()方法才能保證每次都創建一個新的物件,

5.使用new String,一定創建物件

在執行String a = new String("123")的時候,首先走常量池的路線取到一個實體的參考,然后在堆上創建一個新的String實體,走以下建構式給value屬性賦值,然后把實體參考賦值給a:

public String(String original) {
    int size = original.count;
    char[] originalValue = https://www.cnblogs.com/biwz/p/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);
     } 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;
    }

從中我們可以看到,雖然是新創建了一個String的實體,但是value是等于常量池中的實體的value,即是說沒有new一個新的字符陣列來存放"123",

6.關于String.intern()

intern方法使用:一個初始為空的字串池,它由類String獨自維護,當呼叫 intern方法時,如果池已經包含一個等于此String物件的字串(用equals(oject)方法確定),則回傳池中的字串,否則,將此String物件添加到池中,并回傳此String物件的參考,

它遵循以下規則:對于任意兩個字串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 因為 true,

String.intern();
再補充介紹一點:存在于.class檔案中的常量池,在運行期間被jvm裝載,并且可以擴充,String的intern()方法就是擴充常量池的一個方法;當一個String實體str呼叫intern()方法時,java查找常量池中是否有相同unicode的字串常量,如果有,則回傳其參考,如果沒有,則在常量池中增加一個unicode等于str的字串并回傳它的參考,

/**
 * 關于String.intern()
 */
public void test11(){
    String s0 = "kvill"; 
    String s1 = new String("kvill"); 
    String s2 = new String("kvill"); 
    System.out.println("===========test11============");
    System.out.println( s0 == s1 ); //false
    System.out.println( "**********" ); 
    s1.intern(); //雖然執行了s1.intern(),但它的回傳值沒有賦給s1
    s2 = s2.intern(); //把常量池中"kvill"的參考賦給s2 
    System.out.println( s0 == s1); //flase
    System.out.println( s0 == s1.intern() ); //true//說明s1.intern()回傳的是常量池中"kvill"的參考
    System.out.println( s0 == s2 ); //true
}

運行結果:false、false、true、true,

7.關于equals和==

(1)對于==,如果作用于基本資料型別的變數(byte,short,char,int,long,float,double,boolean ),則直接比較其存盤的"值"是否相等;如果作用于參考型別的變數(String),則比較的是所指向的物件的地址(即是否指向同一個物件),

(2)equals方法是基類Object中的方法,因此對于所有的繼承于Object的類都會有該方法,在Object類中,equals方法是用來比較兩個物件的參考是否相等,即是否指向同一個物件,

(3)對于equals方法,注意:equals方法不能作用于基本資料型別的變數,如果沒有對equals方法進行重寫,則比較的是參考型別的變數所指向的物件的地址;而String類對equals方法進行了重寫,用來比較指向的字串物件所存盤的字串是否相等,其他的一些類諸如Double,Date,Integer等,都對equals方法進行了重寫用來比較指向的物件所存盤的內容是否相等,

/**
 * 關于equals和==
 */
public void test12(){
    String s1="hello";
    String s2="hello";
    String s3=new String("hello");
    System.out.println("===========test12============");
    System.out.println( s1 == s2); //true,表示s1和s2指向同一物件,它們都指向常量池中的"hello"物件
    //flase,表示s1和s3的地址不同,即它們分別指向的是不同的物件,s1指向常量池中的地址,s3指向堆中的地址
    System.out.println( s1 == s3); 
    System.out.println( s1.equals(s3)); //true,表示s1和s3所指向物件的內容相同
}

8.String相關的+:

String中的 + 常用于字串的連接,看下面一個簡單的例子:

/**
 * String相關的+
 */
public void test13(){
    String a = "aa";
    String b = "bb";
    String c = "xx" + "yy " + a + "zz" + "mm" + b;
    System.out.println("===========test13============");
    System.out.println(c);
}

編譯運行后,主要位元組碼部分如下:

public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    LDC "aa"
    ASTORE 1
   L1
    LINENUMBER 6 L1
    LDC "bb"
    ASTORE 2
   L2
    LINENUMBER 7 L2
    NEW java/lang/StringBuilder
    DUP
    LDC "xxyy "
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "zz"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "mm"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3
   L3
    LINENUMBER 8 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 3
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 9 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE a Ljava/lang/String; L1 L5 1
    LOCALVARIABLE b Ljava/lang/String; L2 L5 2
    LOCALVARIABLE c Ljava/lang/String; L3 L5 3
    MAXSTACK = 3
    MAXLOCALS = 4
}

顯然,通過位元組碼我們可以得出如下幾點結論:
(1).String中使用 + 字串連接符進行字串連接時,連接操作最開始時如果都是字串常量,編譯后將盡可能多的直接將字串常量連接起來,形成新的字串常量參與后續連接(通過反編譯工具jd-gui也可以方便的直接看出);

(2).接下來的字串連接是從左向右依次進行,對于不同的字串,首先以最左邊的字串為引數創建StringBuilder物件,然后依次對右邊進行append操作,最后將StringBuilder物件通過toString()方法轉換成String物件(注意:中間的多個字串常量不會自動拼接),

也就是說String c = "xx" + "yy " + a + "zz" + "mm" + b; 實質上的實作程序是: String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString();

由此得出結論:當使用+進行多個字串連接時,實際上是產生了一個StringBuilder物件和一個String物件,

9.String的不可變性導致字串變數使用+號的代價:

String s = "a" + "b" + "c"; 
String s1  =  "a"; 
String s2  =  "b"; 
String s3  =  "c"; 
String s4  =  s1  +  s2  +  s3;

分析:變數s的創建等價于 String s = "abc"; 由上面例子可知編譯器進行了優化,這里只創建了一個物件,由上面的例子也可以知道s4不能在編譯期進行優化,其物件創建相當于:

StringBuilder temp = new StringBuilder();   
temp.append(a).append(b).append(c);   
String s = temp.toString();

由上面的分析結果,可就不難推斷出String 采用連接運算子(+)效率低下原因分析,形如這樣的代碼:

public class Test {
    public static void main(String args[]) {
        String s = null;
        for(int i = 0; i < 100; i++) {
            s += "a";
        }
    }
}

每做一次 + 就產生個StringBuilder物件,然后append后就扔掉,下次回圈再到達時重新產生個StringBuilder物件,然后 append 字串,如此回圈直至結束, 如果我們直接采用 StringBuilder 物件進行 append 的話,我們可以節省 N - 1 次創建和銷毀物件的時間,所以對于在回圈中要進行字串連接的應用,一般都是用StringBuffer或StringBulider物件來進行append操作,

10.String、StringBuffer、StringBuilder的區別

(1)可變與不可變:String是不可變字串物件,StringBuilder和StringBuffer是可變字串物件(其內部的字符陣列長度可變),

(2)是否多執行緒安全:String中的物件是不可變的,也就可以理解為常量,顯然執行緒安全,StringBuffer 與 StringBuilder 中的方法和功能完全是等價的,只是StringBuffer 中的方法大都采用了synchronized 關鍵字進行修飾,因此是執行緒安全的,而 StringBuilder 沒有這個修飾,可以被認為是非執行緒安全的,

(3)String、StringBuilder、StringBuffer三者的執行效率:
StringBuilder > StringBuffer > String 當然這個是相對的,不一定在所有情況下都是這樣,比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高,因此,這三個類是各有利弊,應當根據不同的情況來進行選擇使用:
當字串相加操作或者改動較少的情況下,建議使用 String str="hello"這種形式;
當字串相加操作較多的情況下,建議使用StringBuilder,如果采用了多執行緒,則使用StringBuffer,

11.String中的final用法和理解

final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a=b;//此句編譯不通過

final StringBuffer a = new StringBuffer("111");
a.append("222");//編譯通過

可見,final只對參考的"值"(即記憶體地址)有效,它迫使參考只能指向初始指向的那個物件,改變它的指向會導致編譯期錯誤,至于它所指向的物件的變化,final是不負責的,

12.關于String str = new String("abc")創建了多少個物件?

這個問題在很多書籍上都有說到比如《Java程式員面試寶典》,包括很多國內大公司筆試面試題都會遇到,大部分網上流傳的以及一些面試書籍上都說是2個物件,這種說法是片面的,

首先必須弄清楚創建物件的含義,創建是什么時候創建的?這段代碼在運行期間會創建2個物件么?毫無疑問不可能,用javap -c反編譯即可得到JVM執行的位元組碼內容:

原來只想簡單看一下String原始碼,沒想到整理了這么多知識點

很顯然,new只呼叫了一次,也就是說只創建了一個物件,而這道題目讓人混淆的地方就是這里,這段代碼在運行期間確實只創建了一個物件,即在堆上創建了"abc"物件,而為什么大家都在說是2個物件呢,這里面要澄清一個概念,該段代碼執行程序和類的加載程序是有區別的,在類加載的程序中,確實在運行時常量池中創建了一個"abc"物件,而在代碼執行程序中確實只創建了一個String物件,
因此,這個問題如果換成 String str = new String("abc")涉及到幾個String物件?合理的解釋是2個,
個人覺得在面試的時候如果遇到這個問題,可以向面試官詢問清楚”是這段代碼執行程序中創建了多少個物件還是涉及到多少個物件“再根據具體的來進行回答,

13.字串池的優缺點:
字串池的優點就是避免了相同內容的字串的創建,節省了記憶體,省去了創建相同字串的時間,同時提升了性能;另一方面,字串池的缺點就是犧牲了JVM在常量池中遍歷物件所需要的時間,不過其時間成本相比而言比較低,

四、綜合實體

package com.spring.test;

public class StringTest {
    public static void main(String[] args) {  
        /** 
         * 情景一:字串池 
          * JAVA虛擬機(JVM)中存在著一個字串池,其中保存著很多String物件; 
         * 并且可以被共享使用,因此它提高了效率, 
         * 由于String類是final的,它的值一經創建就不可改變, 
         * 字串池由String類維護,我們可以呼叫intern()方法來訪問字串池,  
         */  
        String s1 = "abc";     
        //↑ 在字串池創建了一個物件  
        String s2 = "abc";     
        //↑ 字串pool已經存在物件“abc”(共享),所以創建0個物件,累計創建一個物件  
        System.out.println("s1 == s2 : "+(s1==s2));    
        //↑ true 指向同一個物件,  
        System.out.println("s1.equals(s2) : " + (s1.equals(s2)));    
        //↑ true  值相等  
        //↑------------------------------------------------------over  
        /** 
         * 情景二:關于new String("") 
         *  
         */  
        String s3 = new String("abc");  
        //↑ 創建了兩個物件,一個存放在字串池中,一個存在與堆區中;  
        //↑ 還有一個物件參考s3存放在堆疊中  
        String s4 = new String("abc");  
        //↑ 字串池中已經存在“abc”物件,所以只在堆中創建了一個物件  
        System.out.println("s3 == s4 : "+(s3==s4));  
        //↑false   s3和s4堆疊區的地址不同,指向堆區的不同地址;  
        System.out.println("s3.equals(s4) : "+(s3.equals(s4)));  
        //↑true  s3和s4的值相同  
        System.out.println("s1 == s3 : "+(s1==s3));  
        //↑false 存放的地區多不同,一個堆疊區,一個堆區  
        System.out.println("s1.equals(s3) : "+(s1.equals(s3)));  
        //↑true  值相同  
        //↑------------------------------------------------------over  
        /** 
         * 情景三:  
         * 由于常量的值在編譯的時候就被確定(優化)了, 
         * 在這里,"ab"和"cd"都是常量,因此變數str3的值在編譯時就可以確定, 
         * 這行代碼編譯后的效果等同于: String str3 = "abcd"; 
         */  
        String str1 = "ab" + "cd";  //1個物件  
        String str11 = "abcd";   
        System.out.println("str1 = str11 : "+ (str1 == str11));  
        //↑------------------------------------------------------over  
        /** 
         * 情景四:  
         * 區域變數str2,str3存盤的是存盤兩個拘留字串物件(intern字串物件)的地址, 
         *  
         * 第三行代碼原理(str2+str3): 
         * 運行期JVM首先會在堆中創建一個StringBuilder類, 
         * 同時用str2指向的拘留字串物件完成初始化, 
         * 然后呼叫append方法完成對str3所指向的拘留字串的合并, 
         * 接著呼叫StringBuilder的toString()方法在堆中創建一個String物件, 
         * 最后將剛生成的String物件的堆地址存放在區域變數str3中, 
         *  
         * 而str5存盤的是字串池中"abcd"所對應的拘留字串物件的地址, 
         * str4與str5地址當然不一樣了, 
         *  
         * 記憶體中實際上有五個字串物件: 
         *       三個拘留字串物件、一個String物件和一個StringBuilder物件, 
         */  
        String str2 = "ab";  //1個物件  
        String str3 = "cd";  //1個物件                                         
        String str4 = str2+str3;                                        
        String str5 = "abcd";    
        System.out.println("str4 = str5 : " + (str4==str5)); // false  
        //↑------------------------------------------------------over  
        /** 
         * 情景五: 
         *  JAVA編譯器對string + 基本型別/常量 是當成常量運算式直接求值來優化的, 
         *  運行期的兩個string相加,會產生新的物件的,存盤在堆(heap)中 
         */  
        String str6 = "b";  
        String str7 = "a" + str6;  
        String str67 = "ab";  
        System.out.println("str7 = str67 : "+ (str7 == str67));  
        //↑str6為變數,在運行期才會被決議,  
        final String str8 = "b";  
        String str9 = "a" + str8;  
        String str89 = "ab";  
        System.out.println("str9 = str89 : "+ (str9 == str89));  
        //↑str8為常量變數,編譯期會被優化  
        //↑------------------------------------------------------over  
    }
}

運行結果:

s1 == s2 : true
s1.equals(s2) : true
s3 == s4 : false
s3.equals(s4) : true
s1 == s3 : false
s1.equals(s3) : true
str1 = str11 : true
str4 = str5 : false
str7 = str67 : false
str9 = str89 : true

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/236347.html

標籤:Java

上一篇:記憶體問題探微

下一篇:記一次網路請求連接超時的事故

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more