第 6 章 字串

字串也許是您在 Java 中最常處理的物件,但也可能是最常被忽視的物件,從這個章節開始,無論如何請重視它,字串運用的好不好,足以影響您程式運行的效率,瞭解字串上可用的操作,則可以讓您省去不少程式撰寫與維護的功夫。

這個章節會將字串(String)分作兩個部份來討論:一個是字串本身的特性,您要瞭解這些特性,才知道如何對字串進行比較、如何將字串用的有效率;一個是介紹字串上可用的幾個操作與可用的 API 輔助,像是字串的分離、正則式(Regular expression)比對、Pattern 與 Matcher 的使用等,您也可以學到如何直接在啟動程式的同時下引數給程式來使用。


6.1 認識字串

字串的本質是字元(char)型態的陣列,在 Java 中則更將字串視為 String 類別的一個實例,也就是將其視為物件存在於程式之中,這讓字串處理在 Java 中變得簡單,這個小節就先從基本的字串特性開始認識。

6.1.1 String 類別

由字元所組成的一串文字符號被稱之為「字串」,例如 “Hello” 這個字串就是由 ‘H’、’e’、’l’、’l’、’o’ 這五個字元所組成,在某些程式語言中,字串是以字元陣列的方式存在:

字串的本質是字元陣列

圖 6.1 字串的本質是字元陣列

在 Java 中字串不僅僅是字元陣列,而是 String 類別的一個實例,可以使用 String 類別來建構,例如您可以使用以下的方式來宣告一個字串參考名稱,並指向一個字串實例:

  1. String text = "字串的使用";
  2. System.out.println(text);

注意字串的直接指定必須使用 “” 來包括您的文字,字串的每個字元是使用 Unicode 字元來建構,在建構一個字串物件之後,您可以直接在輸出串流(out)中指定字串物件的參考名稱來輸出字串。

字串的串接在 Java 中可以直接使用 ‘+’ 運算子,’+’ 本來是加法運算子,而它被重新定義(Override)為可以直接用於字串的串接,例如您可以如下將兩個字串接在一起輸出:

  1. String msg = "哈囉!";
  2. msg = msg + "Java程式設計!";
  3. System.out.println(msg);

這一段程式碼會在文字模式上顯示 “哈囉!Java 程式設計!”。
字串在 Java 中以 String 類別的一個實例存在,所以每個字串物件本身會擁有幾個可操作的方法,表 6.1 先介紹幾個常用的方法。

表 6.1 String 物件上的幾個方法

方法 說明
length() 取得字串的字元長度
equals() 判斷原字串中的字元是否相等於指定字串中的字元
toLowerCase() 轉換字串中的英文字元為小寫
toUpperCase() 轉換字串中的英文字元為大寫

範例 6.1 示範了以上的幾個字串操作方法的使用。

範例 6.1 StringDemo.java

  1. public class StringDemo {
  2. public static void main(String[] args) {
  3. String text = "hello";
  4. System.out.println("字串內容: " + text);
  5. System.out.println("字串長度: " + text.length());
  6. System.out.println("等於hello? " +
  7. text.equals("hello"));
  8. System.out.println("轉為大寫: " +
  9. text.toUpperCase());
  10. System.out.println("轉為小寫: " +
  11. text.toLowerCase());
  12. }
  13. }

執行結果:

  1. 字串內容: hello
  2. 字串長度: 5
  3. 等於hello? true
  4. 轉為大寫: HELLO
  5. 轉為小寫: hello

如果您要將輸入的字串轉換為整數、浮點數等資料型態,您可以使用表 6.2 中各類別所提供的各個靜態(static)剖析方法。

表 6.2 將字串剖析為數值型態

方法 說明
Byte.parseByte(字串) 將字串剖析為位元
Short.parseShort(字串) 將字串剖析為short整數
Integer.parseInt(字串) 將字串剖析為int整數
Long.parseLong(字串) 將字串剖析為long整數
Float.parseFloat(字串) 將字串剖析為float浮點數
Double.parseDouble(字串) 將字串剖析為double浮點數

注意如果指定的字串無法剖析為指定的資料型態數值,則會發生 NumberFormatException 例外。
之前宣告字串時,都是以這樣的樣子來宣告:

  1. String str = "caterpillar";

這樣的宣告方式看來像是基本資料型態宣告,但事實上 String 並不是 Java 的基本資料型態,String 是 java.lang 套件下所提供的類別,如果以配置物件的觀念來宣告字串,應該是這樣的:

  1. String str = new String("caterpillar");

不過事實上兩種宣告方式是有所差別的,這待會 6.1.2 再詳細說明,在這邊只要先記得這個差異性就可以了。

字串的本質是由字元陣列所組成,所以使用 String 類別宣告字串後,該字串會具有陣列索引的性質,表 6.3 介紹幾個與使用索引取得字元的相關方法。

表 6.3 取得字串中的字元之方法

方法 說明
char charAt(int index) 傳回指定索引處的字元
int indexOf(int ch) 傳回指定字元第一個找到的索引位置
int indexOf(String str) 傳回指定字串第一個找到的索引位置
int lastIndexOf(int ch) 傳回指定字元最後一個找到的索引位置
String substring(int beginIndex) 取出指定索引處至字串尾端的子字串
String substring(int beginIndex, int endIndex) 取出指定索引範圍子字串
char[] toCharArray() 將字串轉換為字元陣列

範例 6.2 為表 6.3 作了幾個簡單的示範。

範例 6.2 CharAtString.java

  1. public class CharAtString {
  2. public static void main(String[] args) {
  3. String text = "One's left brain has nothing right.\n"
  4. + "One's right brain has nothing left.\n";
  5. System.out.println("字串內容: ");
  6. for(int i = 0; i < text.length(); i++)
  7. System.out.print(text.charAt(i));
  8. System.out.println("\n第一個left: " +
  9. text.indexOf("left"));
  10. System.out.println("最後一個left: " +
  11. text.lastIndexOf("left"));
  12. char[] charArr = text.toCharArray();
  13. System.out.println("\n字元Array內容: ");
  14. for(int i = 0; i < charArr.length; i++)
  15. System.out.print(charArr[i]);
  16. }
  17. }

執行結果:

  1. 字串內容:
  2. One's left brain has nothing right.
  3. One's right brain has nothing left.
  4. 第一個left: 6
  5. 最後一個left: 66
  6. 字元Array內容:
  7. One's left brain has nothing right.
  8. One's right brain has nothing left.

在建構字串物件時,除了直接在 ‘=’ 後使用 “” 來指定字串常數之外,您也可以使用字元陣列來建構,例如使用字元陣列 name,建構出一個內容為 “caterpillar” 的字串:

  1. char[] name = {'c', 'a', 't', 'e', 'r', 'p', 'i', 'l', 'l', 'a', 'r'};
  2. String str = new String(name);

除了以上所介紹的幾個方法之外,您應該查詢 API 手冊,瞭解更多有關於 String 類別的方法,例如 String 上還有 endsWith() 方法可以判斷字串是不是以指定的文字作為結束,您可以使用這個方法來過濾檔案名稱,範例 6.3 就使用 endsWith() 來過濾出副檔名為 jpg 的檔案。

範例6.3 FileFilter.java

  1. public class FileFilter {
  2. public static void main(String[] args) {
  3. String[] filenames = {"caterpillar.jpg", "cater.gif",
  4. "bush.jpg", "wuwu.jpg", "clockman.gif"};
  5. System.out.print("過濾出jpg檔案: ");
  6. for(int i = 0; i < filenames.length; i++) {
  7. if(filenames[i].endsWith("jpg")) {
  8. System.out.print(filenames[i] + " ");
  9. }
  10. }
  11. System.out.println("");
  12. }
  13. }

執行結果:

  1. 過濾出jpg檔案: caterpillar.jpg bush.jpg wuwu.jpg

良葛格的話匣子 在宣告字串一併指定字串值時,如果字串長度過長,可以分作兩行來寫,這樣比較容易閱讀,例如:

  1. String text = "One's left brain has nothing right.\n"
  2. + "One's right brain has nothing left.\n";

6.1.2 不可變(immutable)字串

在 Java 中使用字串有一個非常重要的觀念必須記得,一個字串物件一旦被配置,它的內容就是固定不可變的(immutable),例如下面這個宣告:

  1. String str = "caterpillar";

這個宣告會配置一個長度為 11、內容為 “caterpillar” 的字串物件,您無法改變這個字串物件的內容;不要以為下面的陳述就是改變一個字串物件的內容:

  1. String str = "Just";
  2. str = "Justin";

事實上在這個程式片段中會有兩個字串物件,一個是 “Just” 字串物件,長度為 4,一個是 “Justin” 字串物件,長度為 6,兩個是不同的字串物件,您並不是在 “Just” 字串後加上 “in” 字串,而是讓 str 名稱參考至 “Justin” 字串物件,圖 6.2 展示出這個概念。

使用 '=' 作字串指定的意義

圖 6.2 使用 ‘=’ 作字串指定的意義

在 Java 中,使用 ‘=’ 將一個字串物件指定給一個參考名稱,其意義為改變該名稱所參考的物件,原來被參考的字串物件若沒有其它名稱來參考它,就會在適當的時機被 Java 的「垃圾回收」 (Garbage collection)機制回收,在 Java 中,程式設計人員通常不用關心無用物件的資源釋放問題,Java 會檢查物件是否不再被參考,如果沒有被任何名稱參考的物件將會被回收。

在 Java 執行時會維護一個String池(Pool),對於一些可以共享的字串物件,會先在 String 池中查找是否存在相同的 String 內容(字元相同),如果有就直接傳回,而不是直接創造一個新的 String 物件,以減少記憶體的耗用,如果您在程式中使用下頁的方式來宣告,則實際上是指向同一個字串物件:

  1. String str1 = "flyweight";
  2. String str2 = "flyweight";
  3. System.out.println(str1 == str2);

當您直接在程式中使用 “” 來包括一個字串時,該字串就會在 String 池中,上面的程式片段中的 “flyweight” 就是這樣的情況,”flyweight” 在程式中會有一個實例,而 str1 與 str2 同時參考至 “flyweight”,圖 6.3 表示了這個概念。

字串參考名稱 str1、str2 同時參考至 "flyweight"

圖 6.3 字串參考名稱str1、str2同時參考至”flyweight”

在 Java 中如果 ‘==’ 被使用於兩個參考名稱時,它是用於比較兩個參考名稱是否參考至同一物件,所以在圖 6.3 中當 str1==str2 比較時,程式的執行結果會顯示 true。

再來看看關於 String 的 intern() 方法,先看一下 API 說明的節錄(翻譯):

  1. intern() 方法被呼叫時,如果池(Pool)中已經包括了相同的 String 物件(相同與否由 equals() 方法決定),那麼會從池中返回該字串,否則的話原 String 物件會被加入池中,並返回這個 String 物件的參考。

這段話其實說明了「Flyweight模式」(請見本章後的網路索引) 的運作方式,直接使用範例 6.4 來說明會更清楚。

範例 6.4 InternString.java

  1. public class InternString {
  2. public static void main(String[] args) {
  3. String str1 = "fly";
  4. String str2 = "weight";
  5. String str3 = "flyweight";
  6. String str4 = null;
  7. str4 = str1 + str2;
  8. System.out.println(str3 == str4);
  9. str4 = (str1 + str2).intern();
  10. System.out.println(str3 == str4);
  11. }
  12. }

使用圖形來說明這個範例,在宣告 str1、str2、str3 後,String 池中的狀況如圖 6.4 所示。

字串池示意

圖 6.4 字串池示意

在 Java 中,使用 ‘+’ 串接字串的話會產生一個新的字串物件,所以在程式中第一次比較 str3 與 str4 物件是否為同一物件時,結果會是 false,如圖 6.5 所示。

字串加法會產生新的字串

圖 6.5 字串加法會產生新的字串

intern() 方法會先檢查 String 池中是否存在字元部份相同的字串物件,如果有的話就傳回,由於程式中之前已經有宣告 “flyweight” 字串物件,intern() 在 String 池中發現了它,所以直接傳回,這時再進行比較,str3 與 str4 所指向的其實會是同一物件,所以結果會是 true,如圖 6.6所示。

intern() 會返回 String 池中字串物件之參考

圖 6.6 intern() 會返回 String 池中字串物件之參考

由以上的說明也帶出一個觀念,’==’ 在 Java 中被用來比較兩個參考名稱是否參考至同一物件,所以您不可以用 ‘==’ 來比較兩個字串的字元內容是否相同,例如:

  1. String str1 = new String("caterpillar");
  2. String str2 = new String("caterpillar");
  3. System.out.println(str1 == str2);

雖然兩個字串物件的字元值完全相同,但實際上在這個程式片段中,您產生了兩個 String 的實例,str1 與 str2 分別參考至不同的實例,所以使用 ‘==’ 比較時結果會顯示 false,如果您要比較兩個字串物件的字元值是否相同,您要使用 equals() 方法,以下的寫法才會顯示 true 的結果:

  1. String str1 = new String("caterpillar");
  2. String str2 = new String("caterpillar");
  3. System.out.println(str1.equals(str2));

一個常見的問題是:上面的程式片段產生了幾個 String 的實例?很多人會回答 2 個,但答案是 3 個,因為 “caterpillar” 就是一個,它存在於字串池中,而您又使用 new 建構出兩個 String 物件,分別由 str1 與 str2 參考,所以總共會有 3 個 String 實例。

6.1.3 StringBuilder 類別

一個 String 物件的長度是固定的,您不能改變它的內容,或者是附加新的字元至 String 物件中,您也許會使用 ‘+’ 來串接字串以達到附加新字元或字串的目的,但 ‘+’ 會產生一個新的 String 實例,如果您的程式對這種附加字串的需求很頻繁,並不建議使用 ‘+’ 來進行字串的串接,在物件導向程式設計中,最好是能重複運用已生成的物件,物件的生成需要記憶體空間與時間,不斷的產生 String 實例是一件沒有效率的行為。

在 J2SE 5.0 開始提供 java.lang.StringBuilder 類別,使用這個類別所產生的物件預設會有 16 個字元的長度,您也可以自行指定初始長度,如果附加的字元超出可容納的長度,則 StringBuilder 物件會自動增加長度以容納被附加的字元,如果您有頻繁作字串附加的需求,使用 StringBuilder 會讓程式的效率大大提昇,來寫個簡單的測試程式就可以知道效能差距有多大。

範例 6.5 AppendStringTest.java

  1. public class AppendStringTest {
  2. public static void main(String[] args) {
  3. String text = "";
  4. long beginTime = System.currentTimeMillis();
  5. for(int i = 0; i < 10000; i++)
  6. text = text + i;
  7. long endTime = System.currentTimeMillis();
  8. System.out.println("執行時間:" + (endTime - beginTime));
  9. StringBuilder builder = new StringBuilder("");
  10. beginTime = System.currentTimeMillis();
  11. for(int i = 0; i < 10000; i++)
  12. builder.append(String.valueOf(i));
  13. endTime = System.currentTimeMillis();
  14. System.out.println("執行時間:" + (endTime - beginTime));
  15. }
  16. }

在範例 6.5 中首先使用 ‘+’ 來串接字串,您使用 System.currentTimeMillis() 取得 for 迴圈執行前、後的系統時間,如此就可以得知 for 迴圈執行了多久,以下是我的電腦上的測試數據:

  1. 執行時間:4641
  2. 執行時間:16

您可以看到執行的時間差距很大,這說明了使用 ‘+’ 串接字串所帶來負擔,如果您有經常作附加字串的需求,建議使用 StringBuilder,事實上就範例 6.5 來說,第二個 for 迴圈執行時間還可以更短,因為 append() 也可以接受基本資料型態,所以不必特地使用 String.valueOf() 方法從 int 取得 String,改為以下的方式,執行時間可以更大幅縮短:

  1. for(int i = 0; i < 10000; i++)
  2. builder.append(i);

使用 StringBuilder 最後若要輸出字串結果,可以呼叫其 toString() 方法,您可以使用 length() 方法得知目前物件中的字元長度,而 capacity() 可傳回該物件目前可容納的字元容量,另外 StringBuilder 還有像是 insert() 方法可以將字元插入指定的位置,如果該位置以後有字元,則將所有的字元往後移;deleteChar() 方法可以刪除指定位置的字元,而 reserve() 方法可以反轉字串,詳細的使用可以查詢看看 java.lang.StringBuilder 的 API 文件說明。

StringBuilder 是 J2SE 5.0 才新增的類別,在 J2SE 5.0 之前的版本若有相同的需求,是使用 java.lang.StringBuffer,事實上,StringBuilder 被設計為與 StringBuffer 具有相同的操作介面,在單機非「多執行緒」(Multithread)的情況下使用 StringBuilder 會有較好的效率,因為 StringBuilder 沒有處理「同步」(Synchronized)問題;StringBuffer 則會處理同步問題,如果您的 StringBuilder 會在多執行緒下被操作,則要改用 StringBuffer,讓物件自行管理同步問題,關於多執行緒的觀念,會在第 15 章詳細說明。

6.2 字串進階運用

在瞭解字串的基本特性之後,接下來看看如何操作字串,除了 String 類別上的幾個可用方法之外,您還可以使用一些輔助的類別,像是「正則表示式」(Regular expression)於字串比對上的應用。

6.2.1 命令列引數

在文字模式下執行程式時,通常您可以連帶指定一些引數給程式,例如 javac 本身就附帶了一堆引數可以使用,直接鍵入javac就可以顯示每個引數所代表的意義。

javac 可附帶的引數說明

圖 6.7 javac 可附帶的引數說明

在文字模式下啟動一個 Java 程式時,您也可以一併指定引數,以讓程式進行相對應的功能,也就是輸入命令列引數(Command line argument)給程式,在您撰寫主程式時,會在 main() 的參數列撰寫 String[] args,目的就是用來接受一個字串陣列,您只要取得 args 中的元素值,就可以取出 Java 程式運行時被指定的引數,範例 6.6 是個簡單的示範。

範例 6.6 CommandLineArg.java

  1. public class CommandLineArg {
  2. public static void main(String[] args) {
  3. System.out.print("讀入的引數: ");
  4. for(int i = 0; i < args.length; i++)
  5. System.out.print(args[i] + " ");
  6. System.out.println();
  7. }
  8. }

args 索引 0 的值是從程式名稱後第一個引數開始,以空白為區隔,依序地儲存在 args 陣列中,執行的方式與結果如下頁:

  1. java CommandLineArg -file student.dat
  2. 讀入的引數: -file student.dat

當然您也可以使用 J2SE 5.0 新增的 foreach 語法來改寫範例 6.6,以循序取出被輸入的引數,範例 6.7 的執行結果與範例 6.6 是相同的。

範例 6.7 CommandLineArg2.java

  1. public class CommandLineArg2 {
  2. public static void main(String[] args) {
  3. System.out.print("讀入的引數: ");
  4. for(String arg : args)
  5. System.out.print(arg + " ");
  6. System.out.println();
  7. }
  8. }

良葛格的話匣子 在寫 main() 方法中的參數列時,也可以寫成 String args[],這是陣列宣告取自於 C/C++ 的寫法,不過一般建議寫成 String[] args,比較符合 Java 的陣列宣告語法。
由於命令列引數會傳遞給 args 陣列,如果您要存取 args 陣列的話,記得要檢查陣列長度,否則使用者若沒提供引數的話,會發生 ArrayIndexOutOfBoundsException 例外,一個檢查使用者是否有輸入引數的方式是檢查 args 陣列的長度,例如:

  1. if(args.length == 0) {
  2. // 使用者沒有指定引數,顯示引數功能畫面
  3. }
  4. else {
  5. // 執行所指定引數的對應功能
  6. }

在第10章學到例外處理之後,您還可以使用try…catch語法來取代if判斷式。

6.2.2 分離字串

將字串依所設定的條件予以分離是很常見的操作,例如指令的分離、文字檔案的資料讀出等,以後者而言,當您在文字檔案中儲存以下的資料時,在讀入檔案後,將可以使用 String 的 split() 來協助每一格的資料分離。

  1. justin 64/5/26 0939002302 5433343
  2. momor 68/7/23 0939100391 5432343

範例 6.8是個簡單的示範,假設 fakeFileData 的資料就是檔案中讀入的文字資料。

範例 6.8 SplitStringDemo.java

  1. public class SplitStringDemo {
  2. public static void main(String args[]) {
  3. String[] fakeFileData = {
  4. "justin\t64/5/26\t0939002302\t5433343",
  5. "momor\t68/7/23\t0939100391\t5432343" };
  6. for(String data : fakeFileData) {
  7. String[] tokens = data.split("\t");
  8. for(String token : tokens) {
  9. System.out.print(token + "\t| ");
  10. }
  11. System.out.println();
  12. }
  13. }
  14. }

執行結果:

  1. justin | 64/5/26 | 0939002302 | 5433343 |
  2. momor | 68/7/23 | 0939100391 | 5432343 |

split() 依您所設定的分隔設定,將字串分為數個子字串並以 String 陣列傳回,這邊簡單的介紹了一下 split() 方法的使用,有些用過 Java 的人可能會想到 java.util.StringTokenizer,基本上 API 文件中明確的表示 StringTokenizer 已經是「遺產類別」(Legacy class)了,存在的原因是為了與舊版 Java 程式的相容性,不建議在您撰寫新的 Java 程式時使用,使用 split() 來代替會是個好的方案,而且您還可以進一步搭配正則表示式來進行字串分離。

6.2.3 使用正則表示式(Regular expression)

如果您查詢 J2SE 1.4 之後的 String 線上 API 手冊說明,您會發現有 matches()、replaceAll() 等方法,您所傳入的引數是「正則表示式」(Regular expression)的字串,正則表示式最早是由數學家 Stephen Kleene 于 1956 年提出,主要使用在字元字串的格式比對,後來在資訊領域廣為應用,現在已經成為 ISO(國際標準組織)的標準之一。

Java 在 J2SE 1.4 之後開始支援正則表示式,您可以在 API 文件的 java.util.regex.Pattern 類別中找到支援的正則表示式相關資訊;您可以將正則表示式應用於字串的比對、取代、分離等動作上,以下將介紹幾個簡單的正則表示式。

對於一些簡單的字元比對,例如 1 到 9、A-Z 等,您可以使用預先定義的符號來表示,表 6.4 列出幾個常用的字元比對符號。

表 6.4 字元比對符號

方法 說明
. 符合任一字元
\d 符合 0 到 9 任一個數字字元
\D 符合 0-9 以外的字元
\s 符合 ‘\t’、’\n’、’\x0B’、’\f’、’\r’ 等空白字元
\w 符合 a 到 z、A 到 Z、0 到 9 等字元,也就是數字或是字母都符合
\W 符合 a 到 z、A 到 Z、0 到 9 等之外的字元,也就是除數字與字母外都符合

舉例來說,如果有一字串 “abcdebcadxbc”,使用 “.bc” 來作比對的話,符合的子字串有 “abc”、”ebc”、”xbc” 三個;如果使用 “..cd”,則符合的子字串只有 “abcd”,範例 6.9證實這個說明。

範例6.9 RegularExpressionDemo.java

  1. public class RegularExpressionDemo {
  2. public static void main(String[] args) {
  3. String text = "abcdebcadxbc";
  4. String[] tokens = text.split(".bc");
  5. for(String token : tokens) {
  6. System.out.print(token + " ");
  7. }
  8. System.out.println();
  9. tokens = text.split("..cd");
  10. for(String token : tokens) {
  11. System.out.print(token + " ");
  12. }
  13. System.out.println();
  14. }
  15. }

執行結果:

  1. d ad
  2. ebcadxbc

使用 “.bc” 來作比對的話,由於符合的子字串有 “abc”、”ebc”、”xbc” 三個,所以 split() 方法會使用這三個字串為依據來作字串分離,傳回的自然就是不符合表示式 “.bc” 的 “d” 與 “ad”,同理如果表示式為 “..cd”,則使用 split() 傳回的就是不符合 “..cd” 的 “ebcadxbc”。

您也可以使用「字元類」(Character class)來比較一組字元範圍,表 6.5 示範了幾個字元類的設定方式。

表 6.5 字元類範例

範例 作用
[abc] 符合 a、b 或 c
[^abc] 符合「a 或 b 或 c」之外的字元
[a-zA-Z] 符合 a 到 z 或者是 A 到 Z 的字元
[a-d[m-p]] a 到 d 或者是m 到 p,也可以寫成 [a-dm-p]
[a-z&&[def]] a 到 z 並且是 d 或 e 或 f,結果就是 d 或 e 或 f 可以符合
[a-z&&[^bc]] a 到 z 並且不是 b 或 c
[a-z&&[^m-p]] a 到 z 並且不是 m 到 p

指定一個字元之外,您也可以加上「貪婪量詞」(Greedy quantifiers)來指定字元可能出現的次數,表 6.6 示範了幾個例子。

表 6.6 貪婪量詞範例

範例 作用
X? X 可出現一次或完全沒有
X* X 可出現零次或多次
X+ X 可出現一次或多次
X{n} X 可出現 n 次
X{n,} X 可出現至少n次
X{n, m} X 可出現至少 n 次,但不超過 m 次
X? X 可出現一次或完全沒有

另外還有 Reluctant quantifiers、Possessive quantifiers 等的指定,您可以自行參考 java.util.regex.Pattern 類別 API 文件中的說明。

在 String 類別中,matches() 方法可以讓您驗證字串是否符合指定的正則表示式,這通常用於驗證使用者輸入的字串資料是否正確,例如電話號碼格式;replaceAll() 方法可以將符合正則表示式的子字串置換為指定的字串;split() 方法可以讓您依指定的正則表示式,將符合的子字串排除,剩下的子字串分離出來並以字串陣列傳回,範例 6.9 已經示範了 split() 方法的使用,接下來在範例 6.10 中示範了 replaceAll() 與 matches() 方法的運用。

範例 6.10 UseRegularExpression.java

  1. import java.io.*;
  2. public class UseRegularExpression {
  3. public static void main(String args[])
  4. throws IOException {
  5. BufferedReader reader =
  6. new BufferedReader(
  7. new InputStreamReader(System.in));
  8. System.out.println("abcdefgabcabc".replaceAll(".bc", "###"));
  9. String phoneEL = "[0-9]{4}-[0-9]{6}";
  10. String urlEL = "<a.+href*=*['\"]?.*?['\"]?.*?>";
  11. String emailEL = "^[_a-z0-9-]+(.[_a-z0-9-]+)*" +
  12. "@[a-z0-9-]+([.][a-z0-9-]+)*$";
  13. System.out.print("輸入手機號碼: ");
  14. String input = reader.readLine();
  15. if(input.matches(phoneEL))
  16. System.out.println("格式正確");
  17. else
  18. System.out.println("格式錯誤");
  19. System.out.print("輸入href標籤: ");
  20. input = reader.readLine();
  21. // 驗證href標籤
  22. if(input.matches(urlEL))
  23. System.out.println("格式正確");
  24. else
  25. System.out.println("格式錯誤");
  26. System.out.print("輸入電子郵件: ");
  27. input = reader.readLine();
  28. // 驗證電子郵件格式
  29. if(input.matches(emailEL))
  30. System.out.println("格式正確");
  31. else
  32. System.out.println("格式錯誤");
  33. }
  34. }

執行結果:

  1. ###defg######
  2. 輸入手機號碼: 0939-100391
  3. 格式正確
  4. 輸入href標籤: <a href="http://caterpillar.onlyfun.net">
  5. 格式正確
  6. 輸入電子郵件: caterpillar.onlyfun@gmail.com
  7. 格式正確

良葛格的話匣子 正則表示式的設計是門學問,也有專書就是在介紹正則表示式的設計,沒有經常使用的話,很難設計出實用性高的正則表示式,這邊只能說大致介紹而已,如果真有需要某種正則表示式,建議可以使用搜尋引擎找看看有無現成或類似的表示式可以使用,要不然的話,就只有找專書研究研究如何設計了。

6.2.4 Pattern、Matcher

String 上可使用正則表示式的操作,實際上是利用了 java.util.regex.Pattern 與 java.util.regex.Matcher 的功能,當您呼叫 String 的 matches() 方法時,實際上是呼叫 Pattern 的靜態方法 matches(),這個方法會傳回 boolean 值,表示字串是否符合正則表示式。

如果您想要將正則表示式視為一個物件來重複使用,則您可以使用 Pattern 的靜態方法 compile() 進行編譯,compile() 方法會傳回一個 Pattern 的實例,這個實例代表您的正則表示式,之後您就可以重複使用 Pattern 實例的 matcher() 方法來傳回一個 Matcher 的實例,代表符合正則式的的實例,這個實例上有一些尋找符合正則式條件的方法可供操作,範例 6.11 直接作了示範。

範例 6.11 UsePatternMatcher.java

  1. import java.util.regex.*;
  2. public class UsePatternMatcher {
  3. public static void main(String[] args) {
  4. String phones1 =
  5. "Justin 的手機號碼:0939-100391\n" +
  6. "momor 的手機號碼:0939-666888\n";
  7. Pattern pattern = Pattern.compile(".*0939-\\d{6}");
  8. Matcher matcher = pattern.matcher(phones1);
  9. while(matcher.find()) {
  10. System.out.println(matcher.group());
  11. }
  12. String phones2 =
  13. "caterpillar 的手機號碼:0952-600391\n" +
  14. "bush 的手機號碼:0939-550391";
  15. matcher = pattern.matcher(phones2);
  16. while(matcher.find()) {
  17. System.out.println(matcher.group());
  18. }
  19. }
  20. }

範例 6.11 會尋找手機號碼為 0939 開頭的號碼,假設您的號碼來源不只一個(如 phones1、phones2),則您可以編譯好正則表示式並傳回一個 Pattern 物件,之後就可以重複使用這個 Pattern 物件,在比對時您使用 matcher() 傳回符合條件的 Matcher 實例,find() 方法表示是否有符合的字串,group() 方法則可以將符合的字串傳回,程式的執行結果如下:

  1. Justin 的手機號碼:0939-100391
  2. momor 的手機號碼:0939-666888
  3. bush 的手機號碼:0939-550391

來使用 Pattern 與 Matcher 改寫一下範例 6.9,讓程式可以傳回符合正則式的字串,而不是傳回不符合的字串。

範例 6.12 RegularExpressionDemo2.java

  1. import java.util.regex.*;
  2. public class RegularExpressionDemo2 {
  3. public static void main(String[] args) {
  4. String text = "abcdebcadxbc";
  5. Pattern pattern = Pattern.compile(".bc");
  6. Matcher matcher = pattern.matcher(text);
  7. while(matcher.find()) {
  8. System.out.println(matcher.group());
  9. }
  10. System.out.println();
  11. }
  12. }

執行結果:

  1. abc
  2. ebc
  3. xbc

良葛格的話匣子 在這邊想再嘮叨一下,在書中我有介紹的類別上之方法操作,都只是 API 中一小部份而已,目的只是讓您瞭解有這個功能的存在,並以實際的範例體會其功能,真正要瞭解每個方法的操作,「請多參考線上 API 文件」,我不想列出一長串的表格來說明每個方法,這只會讓篇幅增加,也只會模糊了您學習的焦點,而且請記得,學會查詢 API 文件,絕對是學好 Java 的必備功夫。

6.3 接下來的主題

每一個章節的內容由淺至深,初學者該掌握的深度要到哪呢?在這個章節中,對於初學者我建議至少掌握以下幾點內容:

  • 會使用 String 類別上的常用方法
  • 瞭解字串的不可變數變動(immutable)特性
  • 知道有字串池的存在
  • 瞭解使用 ‘==’ 與 equals() 方法比較兩個字串時的不同點
  • 瞭解 ‘+’ 使用於字串串接上的負擔

到目前為止,您僅止於使用物件的階段,接下來就要進入重頭戲了,從下一個章節開始,您將會逐步接觸 Java 中物件導向程式設計的領域,瞭解 Java 中的何種特性可以支援物件導向設計。