1. 程式人生 > >【Java程式設計思想】13.字串

【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() 方法。該方法可用於 PrintStreamPrintWriter 物件。

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 通常必須使用圓括號括起來以免造成不必要的歧義。
介面 CharSequenceCharBufferStringStringBufferStringBuilder 類之中抽象出了字元序列的一般化定義。多數正則表示式操作都接受 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 的構造器可以接受任何型別的輸入物件,包括 FileInputStreamStringReadable 等。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 已經基本廢棄了。