java從入門到放棄--[1.6]字符串

字符串

Java 字符串就是 Unicode 字符序列。Java 沒有內置的字符類型,而是在標準 Java 類庫中提供了一個預定義類 String。每個用雙引號括起來的字符串都是 String 類的一個實例。可以通過直接賦值或者 new 操作符來創建字符串。

String str1 = "";
String str2 = "hello, this is a string.";
String str3 = new String("create string with new");

String 類沒有提供用於修改字符串的方法,所以我們將 String 類對象稱爲不可變字符串,它被聲明爲 final class,所有的屬性也被定義爲 final 的。但是我們可以修改字符串變量,讓它指向另外一個字符串。

爲了提高內存利用率,JVM 有一個字符串常量池,每次使用雙引號定義字符串,JVM 會先到該常量池中來檢測是否已經存在,存在則直接該對象的引用;否則在常量池中創建一個新增並返回該值的引用。

使用 new 創建字符串(new String("字符串");)時,會直接在堆中創建該字符串並返回其引用。從 Java 6 開始,String 類提供了 intern() 方法,調用該方法時,JVM 去字符串常量池檢測是否已存在該字符串,如果已經存在則直接返回引用;如果不存在則在常量池中添加並返回其引用。

Java 6 中,字符串常量池存在 PermGen 裏,也就是(“永久代”),而這個空間是有限的,基本不會被 FullGC 之外的垃圾收集機制掃描。如果使用不當,經常會發生 OOM。在後續版本中,將字符串常量池放在了堆中,而且默認緩存大小也在不斷擴大。在 Java 8 中永久代 PermGen 也被元數據區 MetaSpace 替代。

 

下邊我們通過一些示例來驗證一下:

String str1 = "hello";
String str2 = "hello";
String str3 = "hello" + "world";
String str4 = str2 + "world";
String str5 = new String("hello");
String str6 = new String("hello");
String str7 = str6.intern();
String str8 = new String("hello").intern();
​
System.out.println("str1 = str2, " + (str1 == str2));
System.out.println("str3 = str4, " + (str3 == str4));
System.out.println("str1 = str5, " + (str1 == str5));
System.out.println("str5 = str6, " + (str5 == str6));
System.out.println("str1 = str7, " + (str1 == str7));
System.out.println("str1 = str8, " + (str1 == str8));
​
String str9 = "hello";
str9 += "world";
System.out.println("str3 = str9, " + (str3 == str9));

字符串操作

  • 長度

    • int length() 返回採用 UTF-16 編碼表示的給定字符串所需要的代碼單元數量。也即是 String 類內部 char 數組的長度。(char 數據類型是一個採用 UTF-16 編碼表示 Unicode 代碼點的代碼單元)

    • int codePointCount(int beginIndex, int endIndex) 表示字符串的實際長度,及代碼點數。

      String str = "hello,\uD835\uDD5D\uD835\uDD60\uD835\uDD60\uD835\uDD5C";
      System.out.println(str);
      System.out.println("length is: " + str.length());
      System.out.println("code point count is: " + str.codePointCount(0, str.length()));
  • 子串

    • String substring(int beginIndex)

    • String substring(int beginIndex, int endIndex)

      String str = "hello, world!";
      ​
      System.out.println(str.substring(1));
      ​
      System.out.println(str.substring(0, 1));
      System.out.println(str.substring(0, str.length() - 1));
      System.out.println(str.substring(0, str.length() + 1));
  • 拼接

    可以直接使用 ++= 運算符來進行字符串的拼接,例如:

    • String str = "hello" + " world!";

    • String str = "hello"; str += " world!";

    • String str = "hello"; str = str + " world!";

  • 格式化

    爲了讓字符串拼接更簡潔直觀,我們可以使用字符串格式化方法 String.format

    • %s 字符串

    • %c 字符類型

    • %b 布爾類型

    • %d 整數類型(十進制數)

    • %x 整數類型(十六進制數)

    • %o 整數類型(八進制數)

    • %f 浮點類型

    • %a 浮點類型(十六進制數)

    • %% 百分比類型

    • %n 換行

    示例:

    System.out.printf("hello, %s %n", "world");
    System.out.printf("大寫a:%c %n", 'A');
    System.out.printf("100 > 50:%b %n", 100 > 50);
    System.out.printf("100除以2:%d %n", 100 / 2);
    System.out.printf("100的16進制數是:%x %n", 100);
    System.out.printf("100的8進制數是:%o %n", 100);
    System.out.printf("100元打8.5折扣是:%f 元%n", 50 * 0.85);
    System.out.printf("上述價格的16進制數是:%a %n", 50 * 0.85);
    System.out.printf("上面的折扣是%d%% %n", 85);
  • 相等判斷

    • equals 判斷是否相等。

    • equalsIgnoreCase 不區分大小寫判斷是否相等。

  • 前綴判斷

    "hello".startsWith("h")

  • 後綴判斷

    "hello".endsWith("o")

  • 包含判斷

    "hello".contains("ll")

  • 查找

    • indexOf 從前邊查找

    • lastIndexOf 從後邊開始找

    示例:

    String str = "hello, world!";
    System.out.println(str.indexOf("e"));
    System.out.println(str.indexOf('e'));
    System.out.println(str.indexOf(101));
    ​
    System.out.println(str.indexOf("e", 2));
    ​
    System.out.println(str.indexOf("l"));
    System.out.println(str.lastIndexOf("l"));
    System.out.println(str.lastIndexOf("l", 9));
  • 查找替換

    • replace

    • replaceAll

    示例:

    System.out.println("hello, world!".replace('o', 'A'));
    System.out.println("hello, world!".replace("o", "OOO"));
    System.out.println("hello, world!".replaceAll("o", "OOO"));
  • 去空格

    System.out.println(" hello, world! ".trim());

  • 大小寫轉換

    • System.out.println("Hello, world!".toUpperCase());

    • System.out.println("Hello, world!".toLowerCase());

  • 空串和 Null 串

    • 空串是一個長度爲0且內容爲空的 String 對象。

    • String 存放 null,表示沒有任何對象與該變量關聯。

String / StringBuilder / StringBuffer

String 在拼接過程中或操作不當時,可能會產生大量的中間對象。而 StringBuffer 就是爲了解決這個問題而提供的一個類,StringBuffer 是線程安全的,如果沒有線程安全的需要則使用 StringBuilder(Java 1.5 中新增)。

StringBufferStringBuilder 都繼承自 AbstractStringBuilder 類,而 StringBuffer 類的所有方法都使用關鍵字 synchronized 來保證線程安全。它們的底層都是通過可修改的 char 數組(Java 9 以後改爲 byte 數組實現)來實現修改。以下內容沒有特別說明則均基於 Java 8。

StringBuffer,StringBuilder 在創建時,如果未指定容量,默認容量爲 16。如果容量可預估,則最好在創建時指定合適的大小,這樣可以避免多次擴容。擴容會產生多重開銷:拋棄原有數組、創建新的數組、進行 arraycopy。

StringBuffer,StringBuilder 常用方法:

  • append 在字符串結尾追加

  • length 返回當前長度

  • setLength 設置字符串長度

示例:

String str1 = "hello" + " world" + "!";
System.out.println(str1);
​
StringBuffer strB1 = new StringBuffer();
strB1.append("hello");
strB1.append(" world");
strB1.append("!");
System.out.println(strB1.toString());
​
StringBuilder strB2 = new StringBuilder();
strB2.append("hello");
strB2.append(" world");
strB2.append("!");
System.out.println(strB2.toString());
​
System.out.println("strB2 length is " + strB2.length());
​
strB2.setLength(strB2.length() - 1);
System.out.println(strB2.toString());
​
strB2.setLength(strB2.length() + 10);
System.out.println(strB2.toString());

JVM 對字符串的優化

現代 JVM 的實現是很智能的,編譯時 JVM 對 String 操作進行一些優化以提高程序的運行效率。

示例1

String str = "hello" + ", " + "world!";
System.out.println(str);

JVM 優化後

String str = "hello, world!";
System.out.println(str);

示例2

String str1 = "hello";
String str2 = str1 + ", world!";
System.out.println(str2);

JVM 優化後

String str1 = "hello";
StringBuilder str2 = new StringBuilder();
str2.append(str1);
str2.append(", world!");
System.out.println(str2.toString());

示例3

long start = System.currentTimeMillis();
String str = "";
for (int i = 0; i < 50000; i++) {
    str += i;
}
System.out.println(str.length());
System.out.println("耗時:" + (System.currentTimeMillis() - start) + "ms");

JVM 優化後

long start = System.currentTimeMillis();
String str = "";
for (int i = 0; i < 50000; i++) {
    StringBuilder tmp = new StringBuilder();
    tmp.append(str);
    tmp.append(i);
    str = tmp.toString();
}
System.out.println(str.length());
System.out.println("耗時:" + (System.currentTimeMillis() - start) + "ms");

for 循環經過優化後,雖然節省了空間,但是 StringBuilder 是在 for 循環內,每次都會創建。性能並不會提升,反而可能會下降。按下邊實現方式改寫代碼後性能提升好幾個數量級。

long start = System.currentTimeMillis();
StringBuilder str = new StringBuilder();
for (int i = 0; i < 50000; i++) {
    str.append(i);
}
System.out.println(str.length());
System.out.println("耗時:" + (System.currentTimeMillis() - start) + "ms");

說明:可以通過 javap -c 編譯生成的class文件 反編譯出字節碼,然後來分析

應用練習

去掉字符串開頭/結尾/中間的空格(不使用 trim 方法)

public String trimAll(String str) {
    StringBuilder tmp = new StringBuilder();
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (c == ' ') {
            continue;
        }
        tmp.append(c);
    }
    return tmp.toString();
}

反轉字符串,比如輸入 123,反轉結果 321

public String reverse(String str) {
    StringBuilder tmp = new StringBuilder();
    char[] chars = str.toCharArray();
    for (int i = chars.length - 1; i >= 0; i--) {
        char c = chars[i];
        tmp.append(c);
    }
    return tmp.toString();
}

常見面試問題

==equals 的區別

  1. == 對於基本類型來說是值比較;而對於引用類型比較的是引用,是否是指向同一個對象的引用。

  2. equals 默認是引用比較,而 Integer、String 等包裝類都重寫了 equals 方法,改爲了值比較。

所以對象都可以看作是繼承自 Object,我們來看一下 Object 的 equals 實現,如果自定義類未覆寫 equals,調用對象實例的 equals 方法默認是引用比較。

public boolean equals(Object obj) {
    return (this == obj);
}

我們再來看一下 Integer 類的 equals 方法

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

下列代碼的執行結果

String s1 = "hello" + ", world!";
String s2 = "hello";
s2 += ", world!";
String s3 = "hello, world!";
String s4 = s2.intern();
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1 == s4);
System.out.println(s2 == s3);
System.out.println(s2 == s4);

執行結果:

false
true
true
false
false

String / StringBuffer / StringBuilder 的區別

  • String 爲不可變字符串;StringBuffer 和 StringBuilder 爲字符串可變對象。

  • String 的 substring 等修改操作每次都會產生一個新的 String 對象;字符串拼接性能 String 低於 StringBuffer,而 StringBuffer 低於 StringBuilder。

  • StringBuffer 是線程安全的,StringBuilder 而線程不安全的。二者都是繼承自 AbstractStringBuilder ,它們的唯一區別是 StringBuffer 的所有方法都使用了 synchronized 修飾符來保證線程安全。

String 對象的 intern 的作用

String 對象的 intern 方法用於字符串的顯示排重。調用此方法時,JVM 去字符串常量池查找池中是否已經存在該字符串,如果已存在則直接返回它的引用;如果不存在則在池中先創建然後返回其引用。

String 不可變性的優點

  • 字符串不可變,因此可以通過字符串常量池來實現,共享對象,從而節省空間,提高性能。

  • 多線程安全,因爲字符串不可變,所以當字符串被多個線程共享時不會存在線程安全問題。

  • 適合做緩存的 Key,因爲字符串不可變,因此它的哈希值也就不變;創建時它的哈希值就被緩存了,不需要重新計算,速度更快。

String 是否可以被繼承

String 不能被繼承。因爲 String 被聲明爲 final,所以不能被繼承。

 

發佈了213 篇原創文章 · 獲贊 508 · 訪問量 89萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章