首頁 > 軟體

Java-String類最全彙總(上篇)

2023-01-16 14:00:43

建立字串

常見的構造 String 的方式

// 方式一
String str = "Hello Bit";

// 方式二
String str2 = new String("Hello Bit");

// 方式三
char[] array = {'a', 'b', 'c'};
String str3 = new String(array);

注意事項:

  • “hello” 這樣的字串字面值常數, 型別也是 String.
  • String 也是參照型別. String str = “Hello”; 這樣的程式碼記憶體佈局如下

回憶 “參照”

我們曾經在講陣列的時候就提到了參照的概念.

參照類似於 C 語言中的指標, 只是在棧上開闢了一小塊記憶體空間儲存一個地址. 但是參照和指標又不太相同, 指標能進行各種數位運算(指標+1)之類的, 但是參照不能, 這是一種 “沒那麼靈活” 的指標.

另外, 也可以把參照想象成一個標籤, “貼” 到一個物件上. 一個物件可以貼一個標籤, 也可以貼多個. 如果一個物件上面一個標籤都沒有, 那麼這個物件就會被 JVM 當做垃圾物件回收掉.

Java 中陣列, String, 以及自定義的類都是參照型別.

由於 String 是參照型別, 因此對於以下程式碼

String str1 = "Hello";
String str2 = str1;

記憶體佈局如圖

那麼有同學可能會說, 是不是修改 str1 , str2 也會隨之變化呢?

str1 = "world";
System.out.println(str2);
// 執行結果
//Hello

我們發現, “修改” str1 之後, str2 也沒發生變化, 還是 hello?

事實上,

str1 = "world"

這樣的程式碼並不算 “修改” 字串, 而是讓 str1 這個參照指向一個新String 物件.(這裡我們修改的是指向,而非字串本身)

提問:我們是否可以通過str1修改"Hello" --> “World”

答:做不到!!!

下面為大家討論一下傳字串和字元陣列返回時我們本身的字串和字元陣列是否會修改的問題:

public class TestDemo {
    public static void func(String s,char[] array) {
        s = "xiangxinhang";
        array[0] = 'p';
    }
    public static void main(String[] args) {
        String str = "abcdef";
        char[] chars = {'b','i','t'};
        func(str,chars);
        System.out.println(str);
        System.out.println(Arrays.toString(chars));
    }

下面是記憶體圖 圖解:

下面圖中為大家講解了為何字串str和字元陣列chars改變的值不一樣。

字串比較相等

如果現在有兩個int型變數,判斷其相等可以使用 == 完成。

public static void main(String[] args) {
int x = 10 ;
int y = 10 ;
System.out.println(x == y);
}

執行結果如下圖所示:

如果說現在在String類物件上使用 == ?

程式碼1

public static void main(String[] args) {
String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
}

執行結果如下圖所示:

這裡我們str1的"hello"在使用完後放入常數池,str2在賦值時我們直接取常數池中的"hello",所以這裡我們的str1和str2相等。

程式碼2

public static void main(String[] args) {
    String str1 = "Hello";
    String str2 = "He" + "llo";
    System.out.println(str1 == str2);
}

執行結果如下圖所示:

這裡我們str2的"hello"雖然是拼接起來的,但是我們編譯器還是會認為它就是"hello",然後直接從常數池中取出我們str1之前存放的"hello",從而str1和str2相等。

程式碼3

public static void main(String[] args) {
    String str1 = "Hello";
    String str2 = "He" + "llo";
    String str3 = "He";
    String str4 = str3 + "llo";
    System.out.println(str1 == str4);
}

執行結果如下圖所示:

此時str3是一個變數–>編譯的時候不知道是啥,所以我們的str4再建立"He"時用的字串不是從常數池拿的,因此我們str1和str4不相等。

看起來貌似沒啥問題, 再換個程式碼試試, 發現情況不太妙.

程式碼4

public static void main(String[] args) {
    String str1 = new String("Hello");
    String str2 = new String("Hello");
    System.out.println(str1 == str2);
}

我們來分析兩種建立 String 方式的差異.

程式碼1記憶體佈局

我們發現, str1 和 str2 是指向同一個物件的. 此時如 “Hello” 這樣的字串常數是在 字串常數池 中.

關於字串常數池

如 “Hello” 這樣的字串字面值常數, 也是需要一定的記憶體空間來儲存的. 這樣的常數具有一個特點, 就是不需要修改(常數嘛). 所以如果程式碼中有多個地方參照都需要使用 “Hello” 的話, 就直接參照到常數池的這個位置就行了, 而沒必要把 “Hello” 在記憶體中儲存兩次.

程式碼4記憶體佈局

通過

String str1 = new String("Hello");

這樣的方式建立的 String 物件相當於再堆上另外開闢了空間來儲存"Hello" 的內容, 也就是記憶體中存在兩份 “Hello”.

String 使用 == 比較並不是在比較字串內容, 而是比較兩個參照是否是指向同一個物件.

關於物件的比較

物件導向程式語言中, 涉及到物件的比較, 有三種不同的方式, 比較身份, 比較值, 比較型別.

在大部分程式語言中 == 是用來比較比較值的. 但是 Java 中的 == 是用來比較身份的.

如何理解比較值和比較身份呢?

可以想象一個場景, 現在取快遞, 都有包裹儲物櫃. 上面有很多的格子. 每個格子裡面都放著東西.

例如, “第二行, 左數第一列” 這個櫃子和 “第二行, 右數第二列” 這個櫃子是同一個櫃子, 就是 身份相同. 如果身份相同, 那麼裡面放的東西一定也相同 (值一定也相同).

例如, “第一行, 左數第一列” 這個櫃子和 “第一行, 左數第二列” 這兩個櫃子不是同一個櫃子, 但是櫃子開啟後發現裡面放著的是完全一模一樣的兩雙鞋子. 這個時候就是 值相同.

Java 中要想比較字串的內容, 必須採用String類提供的equals方法.

    public static void main(String[] args) {
        String str1 = new String("Hello");
        String str2 = new String("Hello");
        System.out.println(str1.equals(str2));
// System.out.println(str2.equals(str1)); // 或者這樣寫也行
    }

equals 使用注意事項

現在需要比較 str 和 “Hello” 兩個字串是否相等, 我們該如何來寫呢?

String str = new String("Hello");

// 方式一
System.out.println(str.equals("Hello"));
// 方式二
System.out.println("Hello".equals(str));

在上面的程式碼中, 哪種方式更好呢?

我們更推薦使用 “方式二”. 一旦 str 是 null, 方式一的程式碼會丟擲異常, 而方式二不會.(即equals前面的字串必須不為null,否則會空指標異常)

String str = null;

// 方式一
System.out.println(str.equals("Hello"));  // 執行結果 丟擲 java.lang.NullPointerException 異常
// 方式二
System.out.println("Hello".equals(str));  // 執行結果 false

方式一:

方式二:

注意事項: “Hello” 這樣的字面值常數, 本質上也是一個 String 物件, 完全可以使用 equals 等 String 物件的方法.

字串常數池

在上面的例子中, String類的兩種範例化操作, 直接賦值和 new 一個新的 String.

a) 直接賦值

String str1 = "hello" ;
String str2 = "hello" ;
String str3 = "hello" ;
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // true
System.out.println(str2 == str3); // true

為什麼現在並沒有開闢新的堆記憶體空間呢?

String類的設計使用了共用設計模式

在JVM底層實際上會自動維護一個物件池(字串常數池)

  • 如果現在採用了直接賦值的模式進行String類的物件範例化操作,那麼該範例化物件(字串內容)將自動儲存到這個物件池之中.
  • 如果下次繼續使用直接賦值的模式宣告String類物件,此時物件池之中如若有指定內容,將直接進行參照
  • 如若沒有,則開闢新的字串物件而後將其儲存在物件池之中以供下次使用

理解 “池” (pool)

“池” 是程式設計中的一種常見的, 重要的提升效率的方式, 我們會在未來的學習中遇到各種 “記憶體池”, “執行緒池”, “資料庫連線池” …

然而池這樣的概念不是計算機獨有, 也是來自於生活中. 舉個栗子:

現實生活中有一種女神, 稱為 “綠茶”, 在和高富帥談著物件的同時, 還可能和別的屌絲搞曖昧. 這時候這個屌絲被稱為 “備胎”. 那麼為啥要有備胎? 因為一旦和高富帥分手了, 就可以立刻找備胎接盤, 這樣 效率比較高.

如果這個女神, 同時在和很多個屌絲搞曖昧, 那麼這些備胎就稱為 備胎池.

b) 採用構造方法

類物件使用構造方法範例化是標準做法。分析如下程式:

String str = new String("hello");

這樣的做法有兩個缺點:

1.如果使用String構造方法就會開闢兩塊堆記憶體空間,並且其中一塊堆記憶體將成為垃圾空間(字串常數 “hello” 也是一個匿名物件, 用了一次之後就不再使用了, 就成為垃圾空間, 會被 JVM 自動回收掉).

2.字串共用問題. 同一個字串可能會被儲存多次, 比較浪費空間.

我們可以使用 String 的 intern 方法來手動把 String 物件加入到字串常數池中

// 該字串常數並沒有儲存在物件池之中
String str1 = new String("hello") ;
String str2 = "hello" ; 
System.out.println(str1 == str2);

// 執行結果
//false

String str1 = new String("hello").intern() ;
String str2 = "hello" ; 
System.out.println(str1 == str2);

// 執行結果
//true

面試題:請解釋String類中兩種物件範例化的區別

1.直接賦值:只會開闢一塊堆記憶體空間,並且該字串物件可以自動儲存在物件池中以供下次使用。

2.構造方法:會開闢兩塊堆記憶體空間,不會自動儲存在物件池中,可以使用intern()方法手工入池。

綜上, 我們一般採取直接賦值的方式建立 String 物件.

理解字串不可變

字串是一種不可變物件. 它的內容不可改變.

String 類的內部實現也是基於 char[] 來實現的, 但是 String 類並沒有提供 set 方法之類的來修改內部的字元陣列.

public static void main(String[] args) {
    String str = "hello";
    str = str + "world";
    str += "!!!";
    System.out.println(str);
}

形如 += 這樣的操作, 表面上好像是修改了字串, 其實不是. 記憶體變化如下:

+= 之後 str 列印的結果卻是變了, 但是不是 String 物件本身發生改變, 而是 str 參照到了其他的物件.

回顧參照

參照相當於一個指標, 裡面存的內容是一個地址. 我們要區分清楚當前修改到底是修改了地址對應記憶體的內容發生改變了, 還是參照中存的地址改變了.

那麼如果實在需要修改字串, 例如, 現有字串 str = “Hello” , 想改成 str = “hello” , 該怎麼辦?

a) 常見辦法: 藉助原字串, 建立新的字串

public class TestDemo {
    public static void main(String[] args) {
        String str = "Hello";
        str = "h" + str.substring(1);
        System.out.println(str);
    }

b) 特殊辦法: 使用 “反射” 這樣的操作可以破壞封裝, 存取一個類內部的 private 成員.

IDEA 中 ctrl + 左鍵 跳轉到 String 類的定義, 可以看到內部包含了一個 char[] , 儲存了字串的內容.

public class TestDemo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String str = "Hello";
// 獲取 String 類中的 value 欄位. 這個 value 和 String 原始碼中的 value 是匹配的.
        Field valueField = String.class.getDeclaredField("value");
// 將這個欄位的存取屬性設為 true
        valueField.setAccessible(true);
// 把 str 中的 value 屬性獲取到.
        char[] value = (char[]) valueField.get(str);
// 修改 value 的值
        value[0] = 'h';
        System.out.println(str);
    }

關於反射

反射是物件導向程式設計的一種重要特性, 有些程式語言也稱為 “自省”.

指的是程式執行過程中, 獲取/修改某個物件的詳細資訊(型別資訊, 屬性資訊等), 相當於讓一個物件更好的 “認清自己” .

為什麼 String 要不可變?(不可變物件的好處是什麼?)

1.方便實現字串物件池. 如果 String 可變, 那麼物件池就需要考慮何時深拷貝字串的問題了.

2.不可變物件是執行緒安全的.

3.不可變物件更方便快取 hash code, 作為 key 時可以更高效的儲存到 HashMap 中.

注意事項: 如下程式碼不應該在你的開發中出, 會產生大量的臨時物件, 效率比較低.

    public static void main(String[] args) {
        String str = "hello" ;
        for(int x = 0; x < 1000; x++) {
            str += x ;
        }
        System.out.println(str);
    }

注意:字串的拼接 會被優化為 StringBuilder物件

到此這篇關於Java-String類最全彙總(上篇)的文章就介紹到這了,下篇的內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com