String
在Java中,String
是一個引用類型,它本身也是一個class
。但是,Java編譯器對String
有特殊處理,即可以直接用"..."
來表示一個字符串:
String s1 = "Hello!";
實際上字符串在String
內部是通過一個char[]
數組表示的,因此,按下面的寫法也是可以的:
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
因為String
太常用了,所以Java提供了"..."
這種字符串字面量表示方法。
Java字符串的一個重要特點就是字符串不可變。這種不可變性是通過內部的private final char[]
字段,以及沒有任何修改char[]
的方法實現的。
我們來看一個例子:
根據上面代碼的輸出,試解釋字符串內容是否改變。
字符串比較
當我們想要比較兩個字符串是否相同時,要特別注意,我們實際上是想比較字符串的內容是否相同。必須使用equals()
方法而不能用==
。
我們看下面的例子:
從表面上看,兩個字符串用==
和equals()
比較都為true
,但實際上那只是Java編譯器在編譯期,會自動把所有相同的字符串當作一個對象放入常量池,自然s1
和s2
的引用就是相同的。
所以,這種==
比較返回true
純屬巧合。換一種寫法,==
比較就會失敗:
結論:兩個字符串比較,必須總是使用equals()
方法。
要忽略大小寫比較,使用equalsIgnoreCase()
方法。
String
類還提供了多種方法來搜索子串、提取子串。常用的方法有:
// 是否包含子串: "Hello".contains("ll"); // true
注意到contains()
方法的參數是CharSequence
而不是String
,因為CharSequence
是String
的父類。
搜索子串的更多的例子:
"Hello".indexOf("l"); // 2 "Hello".lastIndexOf("l"); // 3 "Hello".startsWith("He"); // true "Hello".endsWith("lo"); // true
提取子串的例子:
"Hello".substring(2); // "llo" "Hello".substring(2, 4); "ll"
注意索引號是從0
開始的。
去除首尾空白字符
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
:
" \tHello\r\n ".trim(); // "Hello"
注意:trim()
並沒有改變字符串的內容,而是返回了一個新字符串。
另一個strip()
方法也可以移除字符串首尾空白字符。它和trim()
不同的是,類似中文的空格字符\u3000
也會被移除:
"\u3000Hello\u3000".strip(); // "Hello" " Hello ".stripLeading(); // "Hello " " Hello ".stripTrailing(); // " Hello"
String
還提供了isEmpty()
和isBlank()
來判斷字符串是否為空和空白字符串:
"".isEmpty(); // true,因為字符串長度為0 " ".isEmpty(); // false,因為字符串長度不為0 " \n".isBlank(); // true,因為只包含空白字符 " Hello ".isBlank(); // false,因為包含非空白字符
替換子串
要在字符串中替換子串,有兩種方法。一種是根據字符或字符串替換:
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替換為'w' s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替換為"~~"
另一種是通過正則表達式替換:
String s = "A,,B;C ,D"; s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
上面的代碼通過正則表達式,把匹配的子串統一替換為","
。關於正則表達式的用法我們會在后面詳細講解。
分割字符串
要分割字符串,使用split()
方法,並且傳入的也是正則表達式:
String s = "A,B,C,D"; String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
拼接字符串
拼接字符串使用靜態方法join()
,它用指定的字符串連接字符串數組:
String[] arr = {"A", "B", "C"}; String s = String.join("***", arr); // "A***B***C"
類型轉換
要把任意基本類型或引用類型轉換為字符串,可以使用靜態方法valueOf()
。這是一個重載方法,編譯器會根據參數自動選擇合適的方法:
String.valueOf(123); // "123" String.valueOf(45.67); // "45.67" String.valueOf(true); // "true" String.valueOf(new Object()); // 類似java.lang.Object@636be97c
要把字符串轉換為其他類型,就需要根據情況。例如,把字符串轉換為int
類型:
int n1 = Integer.parseInt("123"); // 123 int n2 = Integer.parseInt("ff", 16); // 按十六進制轉換,255
把字符串轉換為boolean
類型:
boolean b1 = Boolean.parseBoolean("true"); // true boolean b2 = Boolean.parseBoolean("FALSE"); // false
要特別注意,Integer
有個getInteger(String)
方法,它不是將字符串轉換為int
,而是把該字符串對應的系統變量轉換為Integer
:
Integer.getInteger("java.version"); // 版本號,11
轉換為char[]
String
和char[]
類型可以互相轉換,方法是:
char[] cs = "Hello".toCharArray(); // String -> char[] String s = new String(cs); // char[] -> String
如果修改了char[]
數組,String
並不會改變:
這是因為通過new String(char[])
創建新的String
實例時,它並不會直接引用傳入的char[]
數組,而是會復制一份,所以,修改外部的char[]
數組不會影響String
實例內部的char[]
數組,因為這是兩個不同的數組。
從String
的不變性設計可以看出,如果傳入的對象有可能改變,我們需要復制而不是直接引用。
例如,下面的代碼設計了一個Score
類保存一組學生的成績:
觀察兩次輸出,由於Score
內部直接引用了外部傳入的int[]
數組,這會造成外部代碼對int[]
數組的修改,影響到Score
類的字段。如果外部代碼不可信,這就會造成安全隱患。
請修復Score
的構造方法,使得外部代碼對數組的修改不影響Score
實例的int[]
字段。
字符編碼
在早期的計算機系統中,為了給字符編碼,美國國家標准學會(American National Standard Institute:ANSI)制定了一套英文字母、數字和常用符號的編碼,它占用一個字節,編碼范圍從0
到127
,最高位始終為0
,稱為ASCII
編碼。例如,字符'A'
的編碼是0x41
,字符'1'
的編碼是0x31
。
如果要把漢字也納入計算機編碼,很顯然一個字節是不夠的。GB2312
標准使用兩個字節表示一個漢字,其中第一個字節的最高位始終為1
,以便和ASCII
編碼區分開。例如,漢字'中'
的GB2312
編碼是0xd6d0
。
類似的,日文有Shift_JIS
編碼,韓文有EUC-KR
編碼,這些編碼因為標准不統一,同時使用,就會產生沖突。
為了統一全球所有語言的編碼,全球統一碼聯盟發布了Unicode
編碼,它把世界上主要語言都納入同一個編碼,這樣,中文、日文、韓文和其他語言就不會沖突。
Unicode
編碼需要兩個或者更多字節表示,我們可以比較中英文字符在ASCII
、GB2312
和Unicode
的編碼:
英文字符'A'
的ASCII
編碼和Unicode
編碼:
┌────┐
ASCII: │ 41 │
└────┘
┌────┬────┐
Unicode: │ 00 │ 41 │
└────┴────┘
英文字符的Unicode
編碼就是簡單地在前面添加一個00
字節。
中文字符'中'
的GB2312
編碼和Unicode
編碼:
┌────┬────┐
GB2312: │ d6 │ d0 │
└────┴────┘
┌────┬────┐
Unicode: │ 4e │ 2d │
└────┴────┘
那我們經常使用的UTF-8
又是什么編碼呢?因為英文字符的Unicode
編碼高字節總是00
,包含大量英文的文本會浪費空間,所以,出現了UTF-8
編碼,它是一種變長編碼,用來把固定長度的Unicode
編碼變成1~4字節的變長編碼。通過UTF-8
編碼,英文字符'A'
的UTF-8
編碼變為0x41
,正好和ASCII
碼一致,而中文'中'
的UTF-8
編碼為3字節0xe4b8ad
。
UTF-8
編碼的另一個好處是容錯能力強。如果傳輸過程中某些字符出錯,不會影響后續字符,因為UTF-8
編碼依靠高字節位來確定一個字符究竟是幾個字節,它經常用來作為傳輸編碼。
在Java中,char
類型實際上就是兩個字節的Unicode
編碼。如果我們要手動把字符串轉換成其他編碼,可以這樣做:
byte[] b1 = "Hello".getBytes(); // 按ISO8859-1編碼轉換,不推薦 byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8編碼轉換 byte[] b2 = "Hello".getBytes("GBK"); // 按GBK編碼轉換 byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8編碼轉換
注意:轉換編碼后,就不再是char
類型,而是byte
類型表示的數組。
如果要把已知編碼的byte[]
轉換為String
,可以這樣做:
byte[] b = ... String s1 = new String(b, "GBK"); // 按GBK轉換 String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8轉換
始終牢記:Java的String
和char
在內存中總是以Unicode編碼表示。
延伸閱讀
對於不同版本的JDK,String
類在內存中有不同的優化方式。具體來說,早期JDK版本的String
總是以char[]
存儲,它的定義如下:
public final class String { private final char[] value; private final int offset; private final int count; }
而較新的JDK版本的String
則以byte[]
存儲:如果String
僅包含ASCII字符,則每個byte
存儲一個字符,否則,每兩個byte
存儲一個字符,這樣做的目的是為了節省內存,因為大量的長度較短的String
通常僅包含ASCII字符:
public final class String { private final byte[] value; private final byte coder; // 0 = LATIN1, 1 = UTF16
對於使用者來說,String
內部的優化不影響任何已有代碼,因為它的public
方法簽名是不變的。
小結
-
Java字符串
String
是不可變對象; -
字符串操作不改變原字符串內容,而是返回新字符串;
-
常用的字符串操作:提取子串、查找、替換、大小寫轉換等;
-
Java使用Unicode編碼表示
String
和char
; -
轉換編碼就是將
String
和byte[]
轉換,需要指定編碼; -
轉換為
byte[]
時,始終優先考慮UTF-8
編碼。