跳轉到內容

Java 之道/字串和其他

來自華夏公益教科書

字串和其他

[編輯 | 編輯原始碼]

在物件上呼叫方法

[編輯 | 編輯原始碼]

在圖形部分,我們使用 Graphics 物件在視窗中繪製圓圈,並且我使用了“在物件上呼叫方法”這個短語來指代類似這樣的語句

 g.drawOval (0, 0, width, height);

在本例中,drawOval 是在名為 g 的物件上呼叫的方法。當時我沒有提供物件的定義,現在也不能提供完整的定義,但現在該嘗試了。

在 Java 和其他面向物件的語言中,物件是包含一組相關資料的集合,這些資料附帶一組方法。這些方法作用於物件,執行計算,有時還會修改物件的資料。

到目前為止,我們只見過一個物件 g,因此這個定義可能還沒有什麼意義。另一個例子是字串。字串是物件(而整數和雙精度數不是)。根據物件的定義,你可能會問“字串物件中包含哪些資料?”以及“我們可以對字串物件呼叫哪些方法?”

字串物件中包含的資料是字串的字母。有很多方法可以操作字串,但我在這本書中只使用其中的幾個。其餘內容在Java 網站中有記錄。

我們將要看的第一個方法是 charAt,它允許你從字串中提取字母。為了儲存結果,我們需要一種可以儲存單個字母(而不是字串)的變數型別。單個字母稱為字元,儲存它們的變數型別稱為 char。

字元的工作方式與我們已經見過的其他型別一樣

    char fred = 'c';
    if (fred == 'c')
      System.out.println (fred);

字元值用單引號 ( 'c' ) 括起來。與字串值(用雙引號括起來)不同,字元值只能包含單個字母或符號。

以下是 charAt 方法的使用方法

    String fruit = "banana";
    char letter = fruit.charAt(1);
    System.out.println (letter);

語法 fruit.charAt 表示我正在對名為 fruit 的物件呼叫 charAt 方法。

我向此方法傳遞了引數 1,表示我想知道字串的第一個字母。結果是一個字元,儲存在名為 letter 的 char 中。當我列印 letter 的值時,我感到意外

a

“a”不是“banana”的第一個字母。除非你是計算機科學家。由於某種奇怪的原因,計算機科學家總是從零開始計數。“banana”的第 0 個字母(第零個)是“b”。第 1 個字母(第一個)是“a”,第 2 個字母(第二個)是“n”。

如果你想要字串的第 0 個字母,你需要傳遞 0 作為引數

    char letter = fruit.charAt(0);

我們將要看的第二個 String 方法是 length,它返回字串中的字元數。例如

    int length = fruit.length();

length 不接受任何引數,如 () 所示,它返回一個整數,在本例中為 6。請注意,使用與方法相同的名稱來命名變數是合法的(儘管這會讓人類讀者感到困惑)。

要找到字串的最後一個字母,你可能會嘗試以下方法

    int length = fruit.length();
    char last = fruit.charAt (length);       // WRONG!!

這行不通。原因是“banana”中沒有第 6 個字母。因為我們從 0 開始計數,所以 6 個字母的編號從 0 到 5。要獲取最後一個字元,你需要從 length 中減去 1。

對字串進行的操作通常是先從開頭開始,依次選擇每個字元,對其進行某種操作,然後繼續到結尾。這種處理模式稱為遍歷。用 while 語句編碼遍歷的一種自然方式是

    int index = 0;
    while (index < fruit.length())
      char letter = fruit.charAt (index);
      System.out.println (letter);
      index = index + 1;

此迴圈遍歷字串,並將每個字母分別列印在一行上。請注意,條件是 index < fruit.length(),這意味著當 index 等於字串的長度時,條件為假,迴圈體不會執行。我們訪問的最後一個字元是索引為 fruit.length()-1 的字元。

迴圈變數的名稱是 index。索引是一個變數或值,用於指定有序集合中的一個成員(在本例中是字串中的字元集合)。索引指示(因此得名)你想要哪一個。集合必須是有序的,以便每個字母都有一個索引,每個索引都對應一個字元。

作為練習,編寫一個方法,它接受一個字串作為引數,並按逆序列印字母,所有字母都放在一行上。

執行時錯誤

[編輯 | 編輯原始碼]

早在執行時部分,我就談到了執行時錯誤,即直到程式開始執行才會出現的錯誤。在 Java 中,執行時錯誤稱為異常。

到目前為止,你可能還沒有見過很多執行時錯誤,因為我們還沒有做過會導致執行時錯誤的事情。嗯,現在我們正在做。如果你使用 charAt 命令並提供一個負值或大於 length-1 的索引,你將得到一個異常:具體來說,是 StringIndexOutOfBoundsException。試試看,看看它的樣子。

如果你的程式導致異常,它會列印一條錯誤訊息,指示異常型別以及它在程式中的位置。然後程式終止。

閱讀文件

[編輯 | 編輯原始碼]

如果你訪問

http://java.sun.com/j2se/1.4/docs/api/java/lang/String.html

並點選 charAt,你會得到以下文件(或類似內容)

public char charAt(int index)

Returns the character at the specified index.
An index ranges from 0 to length() - 1.

Parameters: index - the index of the character.

Returns: the character at the specified index of this string.
        The first character is at index 0.

Throws: StringIndexOutOfBoundsException if the index is out of range.
verbatim

第一行是方法的原型(參見原型部分),它指示方法的名稱、引數的型別和返回型別。

下一行描述了方法的作用。接下來的兩行解釋了引數和返回值。在本例中,說明有點多餘,但文件應該符合標準格式。最後一行解釋了此方法可能導致哪些異常(如果有)。

indexOf 方法

[編輯 | 編輯原始碼]

在某種程度上,indexOf 與 charAt 相反。charAt 接受一個索引,並返回該索引處的字元。indexOf 接受一個字元,並找到該字元出現的索引。

如果索引超出範圍,charAt 將失敗並導致異常。如果字元未出現在字串中,indexOf 將失敗並返回 -1。

    String fruit = "banana";
    int index = fruit.indexOf('a');

這將找到字母 'a' 在字串中的索引。在本例中,字母出現了三次,因此 indexOf 應該做什麼並不明顯。根據文件,它返回第一個出現的索引。

為了找到後續的出現,indexOf 有一個備用版本(有關此類過載的解釋,請參見過載部分)。它接受第二個引數,指示在字串中的什麼位置開始查詢。如果我們呼叫

    int index = fruit.indexOf('a', 2);

它將從第二個字母(第一個 n)開始,並找到第二個 a,它位於索引 3 處。如果字母恰好出現在起始索引處,則起始索引就是答案。因此,

    int index = fruit.indexOf('a', 5);

返回 5。根據文件,要弄清楚起始索引超出範圍時會發生什麼情況有點棘手

indexOf 返回此物件表示的字元序列中大於或等於 fromIndex 的第一個出現的字元的索引,如果該字元不存在,則返回 -1。

弄清楚這意味著什麼的一種方法是嘗試幾個案例。以下是我實驗的結果

  • 如果起始索引大於或等於 length(),則結果為 -1,表示該字母不會出現在大於起始索引的任何索引處。
  • 如果起始索引為負數,則結果為 1,表示字母在起始索引之後的第一個出現位置。

如果你回顧一下文件,你會發現這種行為與定義一致,即使它並不立即顯而易見。現在我們對 indexOf 的工作原理有了更好的瞭解,我們可以將其用作程式的一部分。

迴圈和計數

[edit | edit source]

以下程式計算字母 'a' 在字串中出現的次數

    String fruit = "banana";
    int length = fruit.length();
    int count = 0;
    
    int index = 0;
    while (index < length)
      if (fruit.charAt(index) == 'a')
        count = count + 1;
      
      index = index + 1;
    
    System.out.println (count);

該程式演示了一種常見的習慣用法,稱為計數器。變數 count 初始化為零,然後每當我們找到一個 'a' 時就增加它(增加是指增加一個;它是遞減的相反,與糞便無關,糞便是一個名詞)。當我們退出迴圈時,count 包含結果:a 的總數。

作為練習,將此程式碼封裝在一個名為 countLetters 的方法中,並將其泛化,使其接受字串和字母作為引數。

作為第二個練習,重寫該方法,使其使用 indexOf 來定位 a,而不是逐個檢查字元。

遞增和遞減運算子

[edit | edit source]

遞增和遞減是如此常見的操作,以至於 Java 為它們提供了特殊的運算子。++ 運算子將 int 或 char 的當前值加一。-- 減一。這兩個運算子都不適用於 double、boolean 或 String。

從技術上講,在表示式中同時遞增變數是合法的。例如,你可能會看到類似的東西

    System.out.println (i++);

看看這個,不清楚遞增是在值被列印之前還是之後生效。因為像這樣的表示式容易造成混淆,我建議你不要使用它們。事實上,為了讓你更加反感,我不會告訴你結果是什麼。如果你真的想知道,你可以嘗試一下。

使用遞增運算子,我們可以重寫字母計數器

    int index = 0;
    while (index < length)
      if (fruit.charAt(index) == 'a')
        count++;

      index++;

常見的錯誤是寫下類似的東西

    index = index++;             // WRONG!!

不幸的是,這在語法上是合法的,因此編譯器不會警告你。該語句的效果是使 index 的值保持不變。這通常是一個難以追蹤的錯誤。

記住,你可以寫 index = index +1;,或者你可以寫 index++;,但你不應該將它們混合使用。

字元算術

[edit | edit source]

可能看起來很奇怪,但你可以對字元進行算術運算!表示式 'a' + 1 得出值 'b'。類似地,如果你有一個名為 letter 的變數包含一個字元,那麼 letter - 'a' 將告訴你它在字母表中的位置(記住 'a' 是字母表的第零個字母,'z' 是第 25 個字母)。

這種方法對於在包含數字的字元(如 '0'、'1' 和 '2')與相應的整數之間進行轉換很有用。它們不是一回事。例如,如果你嘗試這樣

    char letter = '3';
    int x = (int) letter;
    System.out.println (x);

你可能期望得到值 3,但根據你的環境,你可能會得到 51,它是用於表示字元 '3' 的 ASCII 程式碼,或者你可能會得到完全不同的東西。要將 '3' 轉換為相應的整數值,你可以減去 '0'

    int x = (int)(letter - '0');

從技術上講,在這兩個例子中,型別轉換 ((int)) 都是不必要的,因為 Java 會自動將型別 char 轉換為型別 int。我添加了型別轉換是為了強調型別之間的差異,因為我是一個堅持這種事情的人。

由於這種轉換可能有點難看,因此最好使用 Character 類中的 digit 方法。例如

    int x = Character.digit (letter, 10);

將 letter 轉換為相應的數字,將其解釋為十進位制數。

字元算術的另一個用途是按順序遍歷字母表中的字母。例如,在羅伯特·麥考斯基的書《給鴨子讓路》中,小鴨子的名字構成了一個字母順序的序列,類似於 Jack、Kack、Lack、Mack、Nack、Oack、Pack 和 Qack。以下是一個按順序列印這些名字的迴圈

    char letter = 'J';
    while (letter <= 'Q')
      System.out.println (letter + "ack");
      letter++;

注意,除了算術運算子外,我們還可以對字元使用條件運算子。該程式的輸出是

Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack

當然,這不太正確,因為我拼錯了 OuackQuack。作為練習,修改程式以更正此錯誤。

型別轉換(面向專家)

[edit | edit source]

這是一個難題:通常,語句 x++ 與 x = x + 1 完全等價。除非 x 是一個 char!在這種情況下,x++ 是合法的,但 x = x + 1 會導致錯誤。

嘗試一下看看錯誤訊息是什麼,然後看看你是否能弄清楚發生了什麼。

字串是不可變的

[edit | edit source]

當你檢視 String 方法的文件時,你可能會注意到 toUpperCase 和 toLowerCase。這些方法通常是混亂的來源,因為它們聽起來像是改變(或修改)現有字串。實際上,這些方法或任何其他方法都不能改變字串,因為字串是不可變的。

當你對 String 呼叫 toUpperCase 時,你會得到一個新的 String 作為返回值。例如

    String name = "Alan Turing";
    String upperName = name.toUpperCase ();

在第二行執行之後,upperName 包含值 "ALAN TURING",但 name 仍然包含 "Alan Turing"。

字串是不可比較的

[edit | edit source]

通常需要比較字串以檢視它們是否相同,或者檢視哪個在字母順序中排在前面。如果我們可以使用比較運算子(如 == 和 >)就好了,但我們不能。

為了比較字串,我們必須使用 equals 和 compareTo 方法。例如

    String name1 = "Alan Turing";
    String name2 = "Ada Lovelace";

    if (name1.equals (name2))
      System.out.println ("The names are the same.");


    int flag = name1.compareTo (name2);
    if (flag == 0)
      System.out.println ("The names are the same.");
     else if (flag < 0)
      System.out.println ("name1 comes before name2.");
     else if (flag > 0)
      System.out.println ("name2 comes before name1.");

這裡的語法有點奇怪。要比較兩個東西,你必須對其中一個呼叫一個方法,並將另一個作為引數傳遞。

equals 的返回值很簡單;如果字串包含相同的字元,則為真,否則為假。

compareTo 的返回值有點奇怪。它是字串中第一個不同的字元之間的差值。如果字串相等,則為 0。如果第一個字串(呼叫該方法的字串)在字母表中排在前面,則差值為負數。否則,差值為正數。在這種情況下,返回值為正數 8,因為 Ada 的第二個字母在字母表中比 Alan 的第二個字母提前 8 個字母。

使用 compareTo 通常很棘手,我總是記不住哪種方式是哪種方式,但我可以告訴你的是,這個介面對於比較許多型別的物件來說都是非常標準的,所以一旦你理解了它,你就可以一勞永逸了。

為了完整起見,我應該承認,使用 == 運算子操作字串是合法的,但很少是正確的。但這隻有在稍後才能理解,所以現在,不要這樣做。

詞彙表

[edit | edit source]
  • 物件 一個相關資料的集合,它附帶了一組對其進行操作的方法。到目前為止我們使用過的物件是系統提供的 Graphics 物件和 String。
  • 索引 一個變數或值,用於選擇一個有序集合中的一個成員,例如字串中的一個字元。
  • 遍歷 對一個集合中的所有元素進行迭代,對每個元素執行類似的操作。
  • 計數器 用於計數的變數,通常初始化為零,然後遞增。
  • 遞增 將變數的值增加一。Java 中的遞增運算子是 ++。
  • 遞減 將變數的值減少一。Java 中的遞減運算子是 --。
  • 異常 執行時錯誤。異常會導致程式的執行終止。
華夏公益教科書