java 常用類-String-1

EternityNoah發表於2020-09-20

一、字串相關的類

1.1 String 的特性

  1. String類:代表字串。Java 程式中的所有字串字面值(如 "abc" )都作為此類的例項實現。
  2. String是一個final類,代表不可變的字元序列。
    • final修飾的類不能被繼承
  3. 字串是常量,用雙引號引起來表示。它們的值在建立之後不能更改。
  4. String物件的字元內容是儲存在一個字元陣列value[]中的。
    • 資料儲存結構中,有鏈式儲存結構和順序儲存結構兩種。顯然String底層選擇了char型陣列型別的順序儲存結構。

1.2 String部分原始碼

Windows電腦,idea中。雙擊shift,輸入String,即可出來。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** 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.1 String 的特性”中的內容了!!!字串,字串,就是字元構建起來的串。

1.3 String再次理解

1.3.1 上程式碼
public class StringTest {
    @Test
    public void test1() {
        // String name = "abc"; 字面量的定義方式
        String name = "abc";
        String des = "abc";
        name = "hell world";
        System.out.println("name:"+name);// hello world
        System.out.println("des:"+des);// abc
    }

    @Test
    public void test2() {
        final int a = 2;
        a = 3; // 編譯不通過
    }
}

  1. 第一個問題:為什麼name = "hell world",編譯成功,並能執行,不是說“它們的值在建立之後不能更改”嗎?;而test2()中卻編譯失敗?

    • 首先,String屬性引用型別,而int是基本型別。

    • 引用型別變數儲存地址值,而a是一個指向int型別的引用,指向2這個字面值。因此final int 修飾的變數就變為常量了,常量是不能修改其值的,所以test2()編譯失敗。

    • 那麼怎麼理解String 底層儲存使用final修飾的char型陣列,更改其值為什麼編譯成功,並能執行?簡單的JVM走一波。圖一:

      從圖中我們可以看出:

      • 常量池當中,是不會儲存兩個相同的字串的。
      • name、des儲存的是地址值。name和des都指向0x8888這個地址

      圖二:

      從圖中我們可以看出:

      • 這就很好解釋了為什麼輸出結果是name為hello world 而des依然為abc。
      • 同時也解釋了“它們的值在建立之後不能更改”,原始的值“abc”的確沒有被修改,而是在字串常量池中重新建立了一份。
      • 這也可以看出頻繁的對陣列進行增刪改操作的話,很耗費記憶體資源。
1.3.2 初步總結
  1. String:字串,使用一對""引起來表示。
  2. String宣告為final的,不可被繼承。
  3. String實現了Serializable介面:表示字串是支援序列化的。
    • 實現了Comparable介面:表示String可以比較大小
  4. String內部定義了final char[] value用於儲存字串資料
  5. String:代表不可變的字元序列。簡稱:不可變性。
    • 當對字串重新賦值時,需要重寫指定記憶體區域賦值,不能使用原有的value進行賦值。
    • 當對現有的字串進行連線操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
    • 當呼叫String的replace()方法修改指定字元或字串時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
  6. 通過字面量的方式(區別於new)給一個字串賦值,此時的字串值宣告在字串常量池中。
  7. 字串常量池中是不會儲存相同內容的字串的。

1.4 判斷對錯

@Test
    public void test3() {
        String s1="javaWeb";
        String s2="javaWeb";

        String s3 = new String("javaWeb");
        String s4 = new String("javaWeb");

        System.out.println(s1==s2);
        System.out.println(s1==s3);
        System.out.println(s1==s4);
        System.out.println(s3==s4);
    }

直觀判斷:

  1. s1==s2。在字串常量池中相同的字串只會儲存一份,s1和s2儲存相同的地址。所以為true
  2. s1==s3。他們分別指向不同地方,所以為false
  3. s1==s4。他們分別指向不同地方,所以為false
  4. s3==s4。只要是new便會在堆空間中開闢一份空間,JVM才不會管你new的內容是否和前面的相同。所以為false

集合JVM判斷:

結合JVM圖形就可以很清楚的明白到底為什麼錯,為什麼對了。

1.5 判斷物件的屬性

public class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

}

 @Test
    public void test4() {
        Person p1 = new Person("Tom", 12);
        Person p2 = new Person("Tom", 12);
        System.out.println(p1.name==p2.name);// true?false?
    }

顯然為true,Tom儲存在字串常量池中,有且僅有一份。故p1.name==p2.name必然為true

再來:

 @Test
    public void test4() {
        Person p1 = new Person("Tom", 12);
        Person p2 = new Person("Tom", 12);
        System.out.println(p1.name==p2.name);

        p2.name="cxk";
        System.out.println(p1.name==p2.name);// true?false
    }

肯定是false, p2.name="cxk";會先在字串常量池中查詢,沒有就在常量池中建一個, p2.name地址指向它即可。【字串的不可變性】

1.6 擴充:

  1. String s = new String("abc");方式建立物件,在記憶體中建立了幾個物件?

    答:2個。一個是堆空間中new結構,另一個是char[ ]對應的常量池中的資料:”abc“;

  2. 來,搞一下這個。

    public void test3(){
            String s1 = "javaEE";
            String s2 = "hadoop";
    
            String s3 = "javaEEhadoop";
            String s4 = "javaEE" + "hadoop";
            String s5 = s1 + "hadoop";
            String s6 = "javaEE" + s2;
            String s7 = s1 + s2;
    
            System.out.println(s3 == s4);
            System.out.println(s3 == s5);
            System.out.println(s3 == s6);
            System.out.println(s3 == s7);
            System.out.println(s5 == s6);
            System.out.println(s5 == s7);
            System.out.println(s6 == s7);
        
            String s8 = s6.intern();
            System.out.println(s3 == s8);
        }
    
    • s3 == s4為true。字面量或者字面量之間的連線,指向同一個物件,在常量池中宣告。
    • s5、s6、s7都是字面量拼接變數或者變數拼接變數,他們不是在常量池中宣告而是在堆中宣告。可以把它想象為new,從而儘管內容相同,但是地址值卻是是不相同的。

    結論:

    • 常量與常量的拼接結果在常量池。且常量池中不會存在相同內容的常量。  只要其中有一個是變數,結果就在堆中
    • 如果拼接的結果呼叫intern()方法,返回值就在常量池中

1.7 面試題

程式碼一:

public class StringTest2 {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public void change(String str, char ch[]) {
        str = "test ok";
        System.out.println("======"+str);
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        StringTest2 ex = new StringTest2();
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str + " and ");
        System.out.println(ex.ch);
    }
}

輸出結果是多少?

解析:做這道題必須明白幾個知識點

  1. java中方法引數的傳遞機制——值傳遞。
    • 值傳遞。即將實際引數的副本(複製品)傳入到方法內,而引數本身不受影響。
      • 形參是基本資料型別:將實參基本資料型別變數的“資料值”傳遞給形參【原始值不受影響】
      • 形參是引用資料型別:將實參引用資料型別變數的“地址值”傳遞給形參【因為傳遞副本為地址值,所以某些原始值會受影響,某些不會,如String型別具有不可變性】
  2. String型別具有不可變性。

分析:

程式碼二:

public class StringTest2 {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public void change(String str, char ch[]) {
        // 第二步:
//對於方法中的str,由於第一步的賦值操作,它的地址值和“String str = new String("good");”中的str一樣,內容都為good。
//接下來方法中對它進行賦值操作“test ok”。
//由於String型別的不可變性(看原始碼類為final修飾,資料儲存結構為也為final修飾的char型陣列),不可修改。
//因此JVM執行'str ="test ok";'時會在字串常量池中新建一個“test ok”並且更新方法中str的地址值。 
//至此兩個str的地址值不同了,指向的內容也不同了。 可以看“程式碼三”中的對比結果。
        str = "test ok";
       
        // 第二步;
//對於方法中的ch,由於第一步的賦值操作,它的地址值和“char[] ch = {'t', 'e', 's', 't'};”中的ch一樣,內容都為test。
//接下來,在方法中執行"ch[0] = 'b';" 陣列中的第一個元素被賦值為b。由於是引用型別,此時類中成員變數char[] ch = {'t', 'e', 's', 't'};值也變為best。
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        StringTest2 ex = new StringTest2();
        // 第一步
 // 傳遞的兩個實參,引數一,實際上是把StringTest2類中成員變數str的地址值複製一份給StringTest2類中change方法中的形參變數str
//  同理,StringTest2類中成員變數ch(陣列:也是引用型別)的地址值複製一份給StringTest2類中change方法中的形參變數ch[]
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str + " and ");
        System.out.println(ex.ch);
    }
}

程式碼三:

public class StringTest2 {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public void change(String str, char ch[]) {
        System.out.print("賦值之前兩個str的地址值比較為:");
        System.out.println(this.str==str);
        str = "test ok";
        System.out.print("賦值之後兩個str的地址值比較為:");
        System.out.println(this.str==str);
        System.out.println("====================分割線========================");
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        StringTest2 ex = new StringTest2();
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str + " and ");
        System.out.println(ex.ch);
    }
}

結果:

相關文章