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。試試看,看看它的樣子。
如果你的程式導致異常,它會列印一條錯誤訊息,指示異常型別以及它在程式中的位置。然後程式終止。
如果你訪問
並點選 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 與 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
當然,這不太正確,因為我拼錯了 Ouack 和 Quack。作為練習,修改程式以更正此錯誤。
型別轉換(面向專家)
[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 中的遞減運算子是 --。
- 異常 執行時錯誤。異常會導致程式的執行終止。