【Java程式設計思想】13.字串
字串操作是計算機程式設計中最常見的行為。
13.1 不可變 String
String 物件是不可變的。String 類中每一個看起來會修改 String 值的方法,實際上都是建立了一個全新的 String 物件去包含修改後的字串內容;而最初的 String 物件則沒有改變。
每當吧 Stirng 物件作為方法的引數時,都會複製一份引用,而該引用所指的物件一直待在單一的物理位置上,從未動過。
13.2 過載 “+” 與 StringBuilder
操作符的過載的意思是,一個操作符在用於特定的類時,被賦予了特殊的意義。
用於 String 的 “+” 與 “+=” 是 Java 中僅有的兩個過載過的操作符,而 Java 不允許程式設計師過載任何其他操作符。
使用 “+” 可以連線 String,但是原理上,大致是使用類似 append()
方法,生成新的 String 物件,以包含連線後的字串。這種工作方式,期間涉及大量的中間物件生成與回收,會帶來一定的效能問題。
但其實通過反編譯後的位元組碼,我們可以知道在使用 “+” 的時候,編譯器會自動引入 java.lang.StringBuilder
類,並使用 append()
方法,最後 toString()
轉換拼接好的字串。
可以看出編譯器是會自動優化效能的,但是使用多個 Stirng 物件和操作符拼接和使用 Stringbuilder
有什麼不同呢:
假設在迴圈內拼接字串,編譯後的位元組碼會顯示,使用操作符的方式每次在迴圈體內部都會新建一個 Stringbuilder
13.3 無意識的遞迴
對於 ArrayList.toString()
,他會遍歷 ArrayList
中包含的所有物件,呼叫每個元素上的 toString()
方法。這就是一種無意識的遞迴。
public class InfiniteRecursion {
@Override
public String toString() {
return " InfiniteRecursion address: " + this + "\n";
}
}
對於上述程式碼,在其他型別的物件與字串用 “+” 相連線的時候,會發生自動型別轉換,這個時候會呼叫該物件的 toString()
Object.toString()
方法,因此不應該使用 this,而是應該呼叫 super.toString()
方法。
13.4 String 上的操作
String 物件的一些基本方法:
方法 | 引數、過載版本 | 應用 |
---|---|---|
構造器 | 過載版本、預設版本、String、StringBuilder、StringBuffer、char 陣列、byte 陣列 | 建立 String 物件 |
length() | String 中字元的個數 | |
charAt() | Int 索引 | 取得 String 中該索引位置上的 char |
getChars()/getBytes() | 要複製部分的七點和終點的索引,複製的目標陣列,目標陣列的其實索引 | 複製 char 或 byte 到一個目標陣列中 |
toCharArray() | 生成一個 char[],包含 String 的所有字元 | |
equals()/equalsIgnoreCase() | 與之進行比較的 String | 比較兩個 String 的內容是否相同 |
compareTo() | 與之進行比較的 String | 按詞典順序比較 String 的內容,比較結果為負數、零或正數。注意,大小寫並不等價 |
contains() | 要搜尋的 CharSequence | 如果該 String 物件包含引數的內容,返回 true |
contentEquals() | 與之進行比較的 CharSequence 或 StringBuffer | 如果該 String 與引數的內容完全一致,則返回 true |
equalsIgnoreCase() | 與之進行比較的 String | 忽略大小寫,如果兩個 String 的內容相同,則返回 true |
regionMatcher() | 該 String 的索引偏移量,另一個 String 及其索引偏移量,要比較的長度。過載版本增加忽略大小寫功能 | 返回 boolean 結果,以表明所比較區域是否相等 |
startsWith() | 可能的起始 String,過載版本在引數中增加了偏移量 | 返回 boolean 結果,以表明該 String 是否以此引數起始 |
endsWith() | 該 String 可能的字尾 String | 返回 boolean 結果,以表明該 String 是否是該字串的字尾 |
indexOf()/lastIndexOf() | 過載版本包括:char;char 與起始索引;String;Stirng 與起始索引 | 如果該 String 並不包含此引數,就返回-1,否則返回此引數在 String 中的起始索引。lastIndexOf()是從後向前搜尋 |
subString()(subSequence()) | 過載版本:起始索引;起始索引+終點座標 | 返回一個新的 Stirng,以包含引數指定的子字串 |
concat() | 要連線的 Stirng | 返回一個新的 String 物件,內容為原始 String 連線上引數 String |
replace() | 要替換掉的字元,用來進行替換的新字元。也可以用一個 CharSequence 來替換另一個 CharSequence | 返回替換字元後的新 Stirng 物件,如果沒有替換髮生,則返回原始的 String 物件 |
toLowerCase()/toUpperCase() | 將字元的大小寫改變後,返回一個新 String 物件。如果沒有改變發生,則返回原始的 String 物件 | |
trim() | 將 String 兩端的空白字元刪除後,返回一個新的 String 物件。如果沒有改變發生,則返回原始的 String 物件 | |
valueOf() | 過載版本:Object;char[];char[],偏移量,與字元個數;boolean;char;int;long;float;double | 返回一個表示引數內容的 Stirng |
intern() | 為每個唯一的字元序列生成一個且僅生成一個 String 引用 |
總體上來說,在要改變字串內容時,String 類的方法都會返回一個新的 Stirng 物件;如果內容沒有改變,String 的方法只是返回指向原物件的引用。
13.5 格式化輸出
Java 中的 printf()
可以使用格式修飾符來連線字串。
printf("Row 1: [%d %f]\n", x, y);
Java 中還提供了與 printf()
等價的 format()
方法。該方法可用於 PrintStream
或 PrintWriter
物件。
Java 中所有新的格式化功能都由 java.util.Formatter
處理,當建立一個 Formatter
物件的時候,需要向其構造器傳遞一些資訊,告訴它最終的結果將向哪裡輸出。如下
Formatter f = new Formatter(System.out);
再插入資料時,如果想要更精確的控制格式,那麼需要更復雜的格式修飾符。以下是其抽象的語法:
%[argument_index$][flags][width][.precision]conversion
其中
width 控制一個域的最小尺寸,width 可以用於各種型別的資料轉換,並且其行為方式都一樣。預設情況下資料右對齊,可以通過使用“-”標誌來改變對齊方向。
precision 用來指明最大尺寸,並不是所有型別的資料都能使用 precision,而且應用於不同型別的資料轉換時,precision 的意義也不同:對於 String 表示列印時輸出字元的最大數量;對於浮點數表示小數部分要顯示出來的位數(預設6位小數)位數過多舍入,過少則補零;而 precision 沒法應用於整數。
常用的型別轉換字元:
轉換字元 | 描述 |
---|---|
d | 整數型(十進位制) |
c | Unicode 字元 |
b | Boolean 值 |
s | String |
f | 浮點數(十進位制) |
e | 浮點數(科學計數) |
x | 整數(十六進位制) |
h | 雜湊碼(十六進位制) |
% | 字元“%” |
String.format()
是一個 static 方法,他接受與 Formatter.format()
方法一樣的引數,但是返回一個 String 物件。
String.format("%05X: ", str);
使用上面的方法,可以以可讀的十六進位制格式將位元組陣列打印出來。
13.6 正則表示式
使用正則表示式,就能夠以程式設計的方式,構造複雜的文字模式,並對輸入的字串進行搜尋。
正則表示式提供了一種完全通用的方式,能夠解決各種字串處理相關的問題:匹配、選擇、編輯以及驗證。
String 中提供了正則表示式工具
split()
將字串從正則表示式匹配的地方切開
replace()
只替換正則表示式第一個匹配物件
replaceAll()
替換正則表示式全部的匹配物件
?
可以用來描述一個要查詢的字串
+
一個或多個之前的表示式
\\
在正則表示式中插入一個普通的反斜線,因此\\d
可以表示一個數字,\\w
表示一個非單詞小寫字元,\\W
表示一個非單詞大寫字元
|
或操作
例:
-?\\d+
,表示“可能有一個負號,後面跟著一位或者多位的數字”。
(-|\\+)?
表示“可能以一個正號或者負號開頭的字串”
建立正則表示式
字元 | |
---|---|
B | 指定字元 B |
\xhh | 十六進位制值為 oxhh 的字元 |
\uhhhh | 十六進位制表示為 oxhhhh 的 Unicode 字元 |
\t | 製表符 Tab |
\n | 換行符 |
\r | 回車 |
\f | 換頁 |
\e | 轉義(Escape) |
字元類 | |
---|---|
. | 任意字元 |
[abc] | 包含 a、b 和 c 的任何字元(和 a|b|c 作用相同 |
[^abc] | 除了 a、b 和 c 之外的任何字元(否定) |
[a-zA-Z] | 從 a 到 z 或從 A 到 Z 的任何字元(範圍) |
[abc[hij]] | 任意 a、b、c、h、i 和 j 字元(與 a|b|c|h|i|j 作用相同)(合併) |
[a-z&&[hij]] | 任意 h、i 或 j(交集) |
\s | 空白符(空格、tab、換行、換頁和回車) |
\S | 非空白符([^\s]) |
\d | 數字[0-9] |
\D | 非數字[^0-9] |
\w | 詞字元[a-zA-Z0-9] |
\W | 非詞字元[^\w] |
邏輯操作符 | |
---|---|
XY | Y 跟在 X 後面 |
X|Y | X 或 Y |
(X) | 捕獲組(capturing group)。可以在表示式中用 \i 引用第 i 個捕獲組 |
邊界匹配符 | |
---|---|
^ | 一行的起始 |
$ | 一行的結束 |
\b | 詞的邊界 |
\B | 非詞的邊界 |
\G | 前一個匹配的結束 |
量詞
量詞描述了一個模式吸收輸入文字的方式
- 貪婪型:量詞總是貪婪的,除非有其他的選項被設定。貪婪表示式會為所有可能的模式發現儘可能多的匹配。
- 勉強型:用問號來指定,這個量詞匹配滿足模式所需的最少字元數。因此也可以視作“懶惰的、最少匹配的、非貪婪的、不貪婪的”。
- 佔有型:該量詞只在 Java 中可用。正常當正則表示式被應用於字串時,它會產生相當多的狀態,以便在匹配失敗時可以回溯。而“佔有型”量詞並不儲存這些中間狀態,因此他們可以用來防止回溯,這個特性常用於防止正則表示式失控,因此可以使正則表示式執行起來更有效。
貪婪型 | 勉強型 | 佔有型 | 如何匹配 |
---|---|---|---|
X? | X?? | X?+ | 一個或零個 X |
X* | X*? | X*+ | 零個或多個 X |
X+ | X+? | X++ | 一個或多個 X |
X{n} | X{n}? | X{n}+ | 恰好 n 次 X |
X{n,} | X{n,}? | X{n,}+ | 至少 n 次 x |
X{n,m} | X{n,m}? | X{n,m}+ | X 至少 n 次,且不超過 m 次 |
表示式 X 通常必須使用圓括號括起來以免造成不必要的歧義。
介面 CharSequence
從 CharBuffer
、String
、StringBuffer
、StringBuilder
類之中抽象出了字元序列的一般化定義。多數正則表示式操作都接受 CharSequence
型別的引數。
Pattern 和 Matcher
使用 static Pattern.compile()
方法來編譯正則表示式,它會根據 String
型別的正則表示式生成一個 Pattern
物件。
接下來可以把想要檢索的字串傳入 Pattern
物件的 matcher()
方法。該方法會生成一個 Matcher
物件,有很多種用法。
示例如下:
public class TestRegularExpression {
public static void main(String[] args) {
if (args.length < 2) {
print("Usage:\njava TestRegularExpression " +
"characterSequence regularExpression+");
System.exit(0);
}
print("Input: \"" + args[0] + "\"");
for (String arg : args) {
print("Regular expression: \"" + arg + "\"");
Pattern p = Pattern.compile(arg);
Matcher m = p.matcher(args[0]);
while (m.find()) {
print("Match \"" + m.group() + "\" at positions " +
m.start() + "-" + (m.end() - 1));
}
}
}
}
可以看到,Pattern
物件表示編譯後的正則表示式,利用該物件上的 matcher()
方法加上一個輸入字串,即可構造出 Matcher
物件,用來進行相應的匹配或其他操作。
Pattern
類還提供:
matches()
該方法完整為static boolean matches(String regex, CharSequence input)
,用以檢查 regex 是否匹配整個 CharSequence 型別的 input 引數。split()
該方法從匹配 regex 的地方分隔輸入字串,返回分割後的子字串 String 陣列。
Matcher
類提供:
boolean matches()
判斷整個輸入字串是否匹配正則表示式模式。boolean lookingAt()
用來判斷該字串(不必是整個字串)的始部分是否能匹配模式。boolean find()
用來在CharSequence
中查詢多個匹配。boolean find(int start)
組(Group)是用括號劃分的正則表示式。可以根據組的編號來引用整個組。組號為0表達整個表示式;組號為1表示被第一對括號括起的組,以此類推。
Matcher
類提供一系列方法用於獲取與組相關的資訊:
public int groupCount()
返回該匹配器的模式中的分組數目,第0組不包括在內。public String group()
返回前一次匹配操作(例如 find())的第0組(整個匹配)。public String group(int i)
返回前一次匹配操作期間指定的組號,如果匹配成功,但指定的組沒有匹配輸入字串的任何部分,則會返回 null。public int start(int group)
返回在前一次匹配操作中尋找到的組的起始索引。public int end(int group)
返回在前一次匹配操作中尋找到的組的最後一個字元索引加一的值。
Pattern 標記:Pattern
類的 compile()
方法還有另外一個版本,它接受一個標記引數,以調整匹配的行為。完整方法表達為:Pattern Pattern.compile(String regex, int flag)
。
其中 flag 來自以下 Pattern
類中的常量:
編譯標記 | 效果 |
---|---|
Pattern.CANON_EQ | 兩個字元當且僅當他們完全規範分解相匹配時,就認為他們是匹配的。在預設情況下匹配不考慮規範的等價性 |
Pattern.CASE_INSENSITIVE(?i) | 預設情況下,大小寫不敏感的匹配假定只有 US-ASCII 字符集中的字元才能進行。這個標記允許模式匹配不必考慮大小寫。通過指定 UNICODE_CASE 標記以及結合此標記,就可以開啟基於 Unicode 的大小寫不敏感匹配 |
Pattern.COMMENTS(?x) | 在這種模式下空格符會被忽略,並且以#開始直到行末的註釋也會被忽略。通過嵌入的標記表示式也可以開啟 Unix 的行模式 |
Pattern.DOTALL(?s) | 在 dotall 模式中,表示式 "." 匹配所有字元,包括行終結符。預設情況下 "."不匹配行終結符 |
Pattern.MULTILINE(?m) | 在多行模式下,表示式^和\(分別匹配一行的開始和結束。^還匹配輸入字串的開始,\)還匹配輸入字串的結尾。預設情況下,這些表示式只匹配輸入的完整字串的開始和結束 |
Pattern.UNICODE_CASE(?a) | 當指定這個標記,並且 開啟 CASE_INSENSITIVE 時,大小寫不敏感的匹配將按照與 Unicode 標準相一致的方式進行。預設情況下,大小寫不敏感的匹配假定只有 US-ASCII 字符集中的字元才能進行。 |
Pattern.UNIX_LINES(?d) | 這種模式下,在 ./^/$ 的行為中,只識別行終結符 \n |
示例:
Pattern p = Pattern.compile("^java",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
split()
方法將輸入字串斷開成字串物件陣列,斷開邊界有下列正則表示式確定:
String[] split(CharSequence input)
String[] split(CharSequence input, int limit)
限制了將輸入分割成字串的數量
替換操作
replaceFirst(String replacement)
以引數字串 replacement 替換掉第一個匹配成功的部分。replaceAll(String replacement)
以引數字串 replacement 替換所有匹配成功的部分。appendReplacement(StringBuffer sbuf, String replacement)
執行漸進式替換,它允許你呼叫其他方法來生成或處理 replacement,使你能夠以程式設計的方式將目標分割成組。appendTail(StringBuffer sbuf)
在執行一次或多次appendReplacement()
之後,呼叫此方法可以將輸入字串餘下的部分複製到 sbuf 中。
通過 reset()
方法可以將現有的 Matcher
物件應用於一個新的字元序列。使用不帶引數的 reset()
方法可以將 Matcher
物件重新設定到當前字元序列的起始位置。
13.7 掃描輸入
Java SE5中新增了 Scanner
類,可以用於掃描輸入工作。
Scanner
的構造器可以接受任何型別的輸入物件,包括 File
、InputStream
、String
或 Readable
等。Readable
介面表示”具有 read()
方法的某種東西“。
對於 Scanner
,所有的輸入、分詞以及翻譯操作都隱藏在不同型別的 next()
方法中,所有基本型別(除 char 之外)都有對應的 next()
方法。對於所有的 next()
方法,只有找到一個完整的分詞之後才會返回。Scanner
也有相應的 hasNext()
方法,用來判斷下一個輸入分詞是否為所需型別。
預設情況下,Scanner
根據空白字元對輸入進行分詞,但是也可以用正則表示式指定自己所需的定界符。
13.8 StringTokenier
在 Java 引入正則表示式(J2SE1.4)和 Scanner
類(Java SE5)之前,使用 StringTokenier
來進行分詞。
下面是兩者的比較:
public class ReplacingStringTokenizer {
public static void main(String[] args) {
String input = "But I'm not dead yet! I feel happy!";
StringTokenizer stoke = new StringTokenizer(input);
while (stoke.hasMoreElements())
System.out.print(stoke.nextToken() + " ");
System.out.println();
System.out.println(Arrays.toString(input.split(" ")));
Scanner scanner = new Scanner(input);
while (scanner.hasNext())
System.out.print(scanner.next() + " ");
}
}
輸出:
But I'm not dead yet! I feel happy!
[But, I'm, not, dead, yet!, I, feel, happy!]
But I'm not dead yet! I feel happy!
StringTokenier
已經基本廢棄了。