Java 的基本程式設計結構
現在, 假定已經成功地安裝了 JDK,並且能夠執行第 2 章中給出的示例程式。我們從現在開始將介紹 Java 應用程式設計。本章主要介紹程式設計的基本概念(如資料型別、分支以及迴圈)在 Java 中的實現方式。
非常遺憾, 需要告誡大家, 使用 Java 編寫 GUI 應用程式並不是一件很容易的事情, 程式設計者需要掌握很多相關的知識才能夠建立視窗、 新增文字框以及能響應的按鈕等。介紹基於 GUI 的 Java 應用程式設計技術與本章將要介紹的程式設計基本概念相差甚遠, 因此本章給出的所有示例都是為了說明一些相關概念而設計的“ 玩具式” 程式, 它們僅僅使用終端視窗提供輸入輸出。
最後需要說明, 對於一個有 C++ 程式設計經驗的程式設計師來說, 本章的內容只需要瀏覽一下, 應該重點閱讀散佈在正文中的 C/C++ 註釋。對於具有使用 Visual Basic 等其他程式設計背景的程式設計師來說, 可能會發現其中的絕大多數概念都很熟悉, 但是在語法上有比較大的差異,因此, 需要非常仔細地閱讀本章的內容。
3.1 一個簡單的Java應用程式
下面看一個最簡單的 Java 應用程式,它只傳送一條訊息到控制檯視窗中:
public class FirstSample { public static void main(String[] args) { System.out.println("We will not use 'Hello, World!"'); } }
這個程式雖然很簡單, 但所有的 Java 應用程式都具有這種結構, 還是值得花一些時間來研究。首先,Java 區分大小寫。 如果出現了大小寫拼寫錯誤(例如, 將 main 拼寫成 Main), 程式將無法執行。
下面逐行地檢視一下這段原始碼。關鍵字 public 稱為訪問修飾符(access modifier), 這 些修飾符用於控制程式的其他部分對這段程式碼的訪問級別。在第 5 章中將會更加詳細地介紹訪問修飾符的具體內容。關鍵字 class 表明 Java 程式中的全部內容都包含在類中。這裡, 只需要將類作為一個載入程式邏輯的容器,程式邏輯定義了應用程式的行為。在第 4 章中將會用大量的篇幅介紹 Java 類。正如第 1 章所述, 類是構建所有 Java 應用程式和 applet 的構建塊。Java 應用程式中的全部內容都必須放置在類中。
關鍵字 class 後面緊跟類名。 Java 中定義類名的規則很寬鬆。名字必須以字母開頭,後面可以跟字母和數字的任意組合。長度基本上沒有限制。但是不能使用 Java 保留字(例如, public 或 class) 作為類名(保留字列表請參看附錄 A)。
標準的命名規範為(類名 FirstSample 就遵循了這個規範):類名是以大寫字母開頭的名 詞。如果名字由多個單片語成,每個單詞的第一個字母都應該大寫(這種在一個單詞中間使用大寫字母的方式稱為駱駝命名法。以其自身為例, 應該寫成 CamelCase)。
原始碼的檔名必須與公共類的名字相同,並用 .java 作為副檔名。因此,儲存這段源代 碼的檔名必須為 FirstSample.java (再次提醒大家注意,大小寫是非常重要的, 千萬不能寫成 firstsample.java)。
如果已經正確地命名了這個檔案, 並且原始碼中沒有任何錄入錯誤,在編譯這段原始碼之後就會得到一個包含這個類位元組碼的檔案。Java 編譯器將位元組碼檔案自動地命名為 FirstSample. class, 並與原始檔儲存在同一個目錄下。最後, 使用下面這行命令執行這個程式:
java FirstSample
(請記住,不要新增 .class 副檔名。)程式執行之後,控制檯上將會顯示“ We will not use ‘ Hello,World,!”。
當使用
java ClassName
執行已編譯的程式時,Java 虛擬機器將從指定類中的 main 方法開始執行(這裡的“ 方法” 就是 Java 中所說的“ 函式”),因此為了程式碼能夠執行,在類的原始檔中必須包含一個 main 方法。當然,也可以將使用者自定義的方法新增到類中,並且在 main 方法中呼叫它們(第 4 章 將講述如何自定義方法)。
註釋:根據 Java 語言規範, main 方法必須宣告為 public ( Java 語言規範是描述 Java 語言 的官方文件。可以從網站 http://docs.oracle.com/javase/specs 上閱讀或下載)。
不過, 當 main 方法不是 public 時, 有些版本的 Java 直譯器也可以執行 Java 應用程式。 有個程式設計師報告了這個 bug 如果感興趣的話, 可以在網站 http://bugsjava.com/ bugdatabase/ indexjsp 上輸入 bug 號碼 4252539 檢視。這個 bug 被標明“ 關閉, 不予修 復。” Sun 公司的工程師解釋說: Java 虛擬機器規範(在 http://docs.orade.com/javase/specs/ jvms/se8/html) 並沒有要求 main 方法一定是 public, 並且“ 修復這個 bug 有可能帶來其 他的隱患”。好在,這個問題最終得到了解決。在 Java SE 1.4 及以後的版本中強制 main 方法是 public 的。
從上面這段話可以發現一個問題的兩個方面。 一方面讓質量保證工程師判斷在 bug報告中是否存在問題是一件很頭痛的事情, 這是因為其工作量很大,並且工程師對 Java 的所有細節也未必瞭解得很清楚。 另一方面, Sun 公司在 Java 開源很久以前就把 bug 報告及其解決方案放到網站上讓所有人監督檢查, 這是一種非常了不起的舉動某些情況 下, Sun 甚至允許程式設計師為他們最厭惡的 bug 投票, 並用投票結果來決定釋出的下一個 JDK 版本將修復哪些 bug。
需要注意原始碼中的括號{ }。在 Java 中,像在 C/C++ 中一樣,用大括號劃分程式的各個部分(通常稱為塊)。Java 中任何方法的程式碼都用“ {” 開始,用“}”結束。
大括號的使用風格曾經引發過許多無意義的爭論。我們的習慣是把匹配的大括號上下對齊。不過,由於空白符會被 Java 編譯器忽略,所以可以選用自己喜歡的大括號風格。在下面講述各種迴圈語句時, 我們還會詳細地介紹大括號的使用。
我們暫且不去理睬關鍵字 static void, 而僅把它們當作編譯 Java 應用程式必要的部分就行了。在學習完第 4 章後,這些內容的作用就會揭曉。現在需要記住:每個 Java 應用程式都 必須有一個 main 方法,其宣告格式如下所示:
public class ClassName { public static void main(String[] args) { program statements } }
C++ 註釋: 作為一名 C++ 程式設計師, 一定知道類的概念。Java 的類與 C++ 的類很相似, 但還是有些差異會使人感到困惑。 例如, Java 中的所有函式都屬於某個類的方法(標準術語將其稱為方法, 而不是成員函式)。因此,Java 中的 main 方法必須有一個外殼類。 讀者有可能對 C++ 中的靜態成員函式( static member functions) 十分熟悉。這些成員函式定義在類的內部, 並且不對物件進行操作。Java 中的 main 方法必須是靜態的。 最後, 與 C/C++ —樣, 關鍵字 void 表示這個方法沒有返回值, 所不同的是 main 方法沒有為作業系統返回“ 退出程式碼” 。如果 main 方法正常退出, 那麼 Java 應用程式的退出程式碼為 0, 表示成功地執行了程式。如果希望在終止程式時返回其他的程式碼, 那就需要呼叫 System.exit 方法。
接下來,研究一下這段程式碼:
{ Systein.out.println("We will not use 'Hello, World!'"); }
一對大括號表示方法體的開始與結束, 在這個方法中只包含一條語句。與大多數程式設計語言一樣, 可以將 Java 語句看成是這種語言的句子。在 Java 中,每個句子必須用分號結束。 特別需要說明,回車不是語句的結束標誌,因此,如果需要可以將一條語句寫在多行上。
在上面這個 main 方法體中只包含了一條語句,其功能是:將一個文字行輸出到控制檯上。
在這裡,使用了 System.out 物件並呼叫了它的 println 方法。注意, 點號( • )用於呼叫方法。Java 使用的通用語法是
object,method(parameters)
這等價於函式呼叫。
在這個示例中,呼叫了 println 方法並傳遞給它一個字串引數。 這個方法將傳遞給它的字串引數顯示在控制檯上。然後,終止這個輸出行,使得每次呼叫 println 都會在新的一行 上顯示輸出。需要注意一點,Java 與 C/C++ —樣,都採用雙引號分隔字串。(本章稍後將 會詳細地講解有關字串的知識)。
與其他程式設計語言中的函式一樣,在 Java 的方法中,可以沒有引數, 也可以有一個或 多個引數(有的程式設計師把引數叫做實參。) 對於一個方法, 即使沒有引數也需要使用空括號。 例如, 不帶引數的 println 方法只列印一個空行。使用下面的語句來呼叫:
System,out.printlnO;
註釋: System.out 還有一個 print 方法, 它在輸出之後不換行。例如, System.out.print ( “ Hello”)列印“ Hello” 之後不換行, 後面的輸出緊跟在字母“o”之後。
3.2 註釋
與大多數程式設計語言一樣,Java 中的註釋也不會出現在可執行程式中。因此, 可以在源程式中根據需要新增任意多的註釋,而不必擔心可執行程式碼會膨脹。在 Java 中,有 3 種標記註釋的方式。最常用的方式是使用 // 其註釋內容從 // 開始到本行結尾。
System.out.println("We will not use 'Hello, World!’");// is this too cute?
當需要長篇的註釋時, 既可以在每行的註釋前面標記 // 也可以使用 /* 和 */ 將一段比較長的註釋括起來。
最後,第 3 種註釋可以用來自動地生成文件。這種註釋以 / ** 開始, 以 */ 結束。請參見 程式清單 3-1。 有關這種註釋的詳細內容和自動生成文件的具體方法請參見第 4 章。
警告: 在 Java 中,/* */ 註釋不能巢狀 „ 也就是說, 不能簡單地把程式碼用 /* 和 */ 括起來 作為註釋, 因為這段程式碼本身可能也包含一個 */。
3.3 資料型別
Java 是 一種強型別語言。這就意味著必須為每一個變數宣告一種型別: 在 Java 中, -共有 8 種基本型別( primitive type ), 其中有 4 種整型、2 種浮點型別、 1 種用於表示 Unicode 編碼的字元單元的字元型別 char (請參見論述 char 型別的章節) 和 1 種用於表示真值的 boolean 型別。
註釋: Java 有一個能夠表示任意精度的算術包, 通常稱為“ 大數值”( bignumber。) 雖然 被稱為大數值,但它並不是一種新的 Java 型別,而是一個 Java 物件。 本章稍後將會詳細地介紹它的用法
3.3.1 整型
整型用於表示沒有小數部分的數值, 它允許是負數。Java 提供了 4 種整型,具體內容如 表 3-1 所示。
在通常情況下,int 型別最常用。但如果表示星球上的居住人數, 就需要使用 long 型別 了。byte 和 short 型別主要用於特定的應用場合,例如,底層的檔案處理或者需要控制佔用儲存空間量的大陣列。
在 Java 中, 整型的範圍與執行 Java 程式碼的機器無關。這就解決了軟體從一個平臺移植到 另一個平臺,或者在同一個平臺中的不同作業系統之間進行移植給程式設計師帶來的諸多問題。與此相反,C 和 C++ 程式需要針對不同的處理器選擇最為高效的整型, 這樣就有可能造成一個在 32 位處理器上執行很好的 C 程式在 16 位系統上執行卻發生整數溢位。由於 Java 程式必須保證 在所有機器 L都能夠得到相同的執行結果, 所以各種資料型別的取值範圍必須固定。
長整型數值有一個字尾 L 或 1 ( 如 4000000000L。) 十六進位制數值有一個字首 Ox 或 0X (如 OxCAFEL 八進位制有一個字首 0 , 例如, 010 對應八進位制中的 8。 很顯然, 八進位制表示法比較容易混淆, 所以建議最好不要使用八進位制常數。
從 Java 7 開始, 加上字首 0b 或 0B 就可以寫二進位制數。例如,0b1001就是 9。 另外,同樣是 從 Java 7 開始,還可以為數字字面量加下劃線,如用 1_000_000(或0b1111_0100_0010_0100_0000 ) 表示一百萬。這些下劃線只是為丫讓人更易讀。Java 編譯器會去除這些下劃線。
C++ 註釋: 在 C 和 C++ 中, int 和 long 等型別的大小與目標平臺相關。在 8086 這樣的 16 位處理器上整型數值佔 2 位元組;不過, 在 32 位處理器(比如 Pentium 或 SPARC) 上, 整型數值則為 4 位元組。 類似地, 在 32 位處理器上 long 值為 4 位元組, 在 64 位處理器上則 為 8 位元組。由於存在這些差別, 這對編寫跨平臺程式帶來了很大難度。 在 Java 中, 所有 的數值型別所佔據的位元組數量與平臺無關。
注意, Java 沒有任何無符號(unsigned) 形式的 int、 long、short 或 byte 型別。
3.3.2 浮點型別
浮點型別用於表示有小數部分的數值。在 Java 中有兩種浮點型別,具體內容如表 3-2 所示。
double 表示這種型別的數值精度是 float 型別的兩倍(有人稱之為雙精度數值)。絕大部分應用程式都採用 double 型別。在很多情況下,float 型別的精度很難滿足需求。實際上,只 有很少的情況適合使用 float 型別,例如,需要單精度資料的庫, 或者需要儲存大量資料。
float 型別的數值有一個字尾 F 或 f (例如,3.14F) 。沒有字尾 F 的浮點數值(如 3.14 ) 預設為 double 型別。當然,也可以在浮點數值後面新增字尾 D 或 d (例如,3.14D) 。
註釋:可以使用十六進位制表示浮點數值。例如,0.125=2的負3次方 可以表示成 0xl.0p-3。在十六 進製表示法中, 使用 p 表示指數, 而不是 e。 注意, 尾數採用十六進位制,指數採用十進位制。指數的基數是 2,而不是 10。
所有的浮點數值計算都遵循 IEEE 754 規範。具體來說,下面是用於表示溢位和出錯情況 的三個特殊的浮點數值:
•正無窮大
•負無窮大
•NaN (不是一個數字)
例如, 一 正整數除以 0 的結果為正無窮大。計算 0/0 或者負數的平方根結果為 NaN。
註釋: 常量 Double_POSITIVE_INFINITY、 Double.NEGATIVEJNFINITY 和 Double.NaN ( 以及相應的 Float 型別的常量) 分別表示這三個特殊的值, 但在實際應用中很少遇到。 特別要說明的是, 不能這樣檢測一個特定值是否等於 Double.NaN:
if (x = Double.NaN)// is never true
所有“ 非數值” 的值都認為是不相同的。然而,可以使用 Double.isNaN 方法:
if (Double.isNaN(x)) // check whether x is "not a number"
警告: 浮點數值不適用於無法接受舍入誤差的金融計算中。 例如,命令 System.out.println ( 2.0-1.1 ) 將列印出 0.8999999999999999, 而不是人們想象的 0.9。這種舍入誤差的主要原因是浮點數值採用二進位制系統表示, 而在二進位制系統中無法精確地表示分數 1/10。這就好像十進位制無法精確地表示分數 1/3 —樣。如果在數值計算中不允許有任何舍入誤差, 就應該使用 BigDecimal類, 本章稍後將介紹這個類。
3.3.3 char型別
char 型別原本用於表示單個字元。不過,現在情況已經有所變化。 如今,有些 Unicode 字元可以用一個 char值描述,另外一些 Unicode 字元則需要兩個 char 值。有關的詳細資訊請 閱讀下一節。
char 型別的字面量值要用單引號括起來。例如:‘A’是編碼值為 65 所對應的字元常量。 它與 "A" 不同,"A" 是包含一個字元 A 的字串, char 型別的值可以表示為十六進位制值,其 範圍從 \u0000 到 \Uffff。例如:W2122 表示註冊符號 ( ), \u03C0 表示希臘字母 it。
除了轉義序列 \u 之外, 還有一些用於表示特殊字元的轉義序列, 請參看表 3-3。所有這些轉義序列都可以出現在加引號的字元字面量或字串中。例如,’ \u2122' 或 "Hello\n”。轉義序列 \u還可以出現在加引號的字元常量或字串之外(而其他所有轉義序列不可以)。例 如:
public static void main(String\u005B\ u00SD args)
就完全符合語法規則, \u005B 和 \u005D 是 [ 和 ] 的編碼。
警告: Unicode 轉義序列會在解析程式碼之前得到處理。 例如,"\u0022+\u0022 ” 並不是一 個由引號(U+0022) 包圍加號構成的字串。 實際上, \u0022 會在解析之前轉換為 ", 這 會得到 ""+"",也就是一個空串。
更隱祕地, 一定要當心註釋中的 \u。註釋
// \u00A0 is a newline
會產生一個語法錯誤, 因為讀程式時 \u00A0 會替換為一個換行符類似地,下面這個註釋
// Look inside c:\users
也會產生一個語法錯誤, 因為 \u 後面並未跟著 4 個十六進位制數, ,
3.3.4 Unicode和char型別
要想弄清 char 型別, 就必須瞭解 Unicode 編碼機制。Unicode 打破了傳統字元編碼機制的限制。 在 Unicode 出現之前, 已經有許多種不同的標準:美國的 ASCII、 西歐語言中的 ISO 8859-1 、俄羅斯的 KOI-8、 中國的 GB 18030 和 BIG-5 等。這樣就產生了下面兩個問題: 一個是對於任意給定的程式碼值,在不同的編碼方案下有可能對應不同的字母;二是採用大字符集的語言其編碼長度有可能不同。例如,有些常用的字元采用單位元組編碼, 而另一些字元則需要兩個或更多個位元組。
設計 Unicode 編碼的目的就是要解決這些問題。在 20 世紀 80 年代開始啟動設計工作時, 人們認為兩個位元組的程式碼寬度足以對世界上各種語言的所有字元進行編碼, 並有足夠的空間留給未來的擴充套件。在 1991 年釋出了 Unicode 1.0, 當時僅佔用 65 536 個程式碼值中不到一半的 部分。在設計 Java 時決定採用 16 位的 Unicode 字符集,這樣會比使用 8 位字符集的程式設計語言有很大的改進。
十分遺憾, 經過一段時間, 不可避免的事情發生了。Unicode 字元超過了 65 536 個,其主要原因是增加了大量的漢語、 日語和韓語中的表意文字。現在,16 位的 char 型別已經不能滿足描述所有 Unicode 字元的需要了。
下面利用一些專用術語解釋一下 Java 語言解決這個問題的基本方法。從 Java SE 5.0 開 始。碼點( code point) 是指與一個編碼表中的某個字元對應的程式碼值。在 Unicode 標準中, 碼點採用十六進位制書寫,並加上字首 U+, 例如 U+0041 就是拉丁字母 A 的碼點。Unicode 的 碼點可以分成 17 個程式碼級別( codeplane)。第一個程式碼級別稱為基本的多語言級別( basic multilingual plane ), 碼點從 U+0000 到 U+FFFF, 其中包括經典的 Unicode 程式碼;其餘的 16 個級別碼點從 U+10000 到 U+10FFFF , 其中包括一些輔助字元(supplementary character)。
UTF-16 編碼採用不同長度的編碼表示所有 Unicode 碼點。在基本的多語言級別中,每個 字元用 16 位表示,通常被稱為程式碼單元( code unit); 而輔助字元采用一對連續的程式碼單元 進行編碼。這樣構成的編碼值落入基本的多語言級別中空閒的 2048 位元組內, 通常被稱為替代區域(surrogate area) [ U+D800 ~ U+DBFF 用於第一個程式碼單元,U+DC00 ~ U+DFFF 用 於第二個程式碼單元 ]。這樣設計十分巧妙,我們可以從中迅速地知道一個程式碼單元是一個字元的編碼,還是一個輔助字元的第一或第二部分。例如,是八元數集(http://math.ucr.edu/ home/baez/octonions) 的一個數學符號,碼點為 U+1D546, 編碼為兩個程式碼單元 U+D835 和 U+DD46。(關於編碼演算法的具體描述見 http://en.wikipedia.org/wiki/UTF-l6 ) 。
在 Java 中,char 型別描述了 UTF-16 編碼中的一個程式碼單元。
我們強烈建議不要在程式中使用 char 型別,除非確實需要處理 UTF-16 程式碼單元。最好將字串作為抽象資料型別處理(有關這方面的內容將在 3.6 節討論)。
3.3.4 boolean型別
boolean (布林)型別有兩個值:false 和 true, 用來判定邏輯條件整型值和布林值之間不能進行相互轉換。
C++ 註釋: 在 C++ 中, 數值甚至指標可以代替 boolean 值。值 0 相當於布林值 false, 非 0 值相當於布林值 true, 在 Java 中則不是這樣,, 因此, Java 程式設計師不會遇到下述麻煩:
if (x = 0) // oops... meant x = 0
在 C++ 中這個測試可以編譯執行, 其結果總是 false: 而在 Java 中, 這個測試將不 能通過編譯, 其原因是整數表示式 x = 0 不能轉換為布林值。
3.4 變數
在 Java 中,每個變數都有一個型別( type)。在宣告變數時,變數的型別位於變數名之前。這裡列舉一些宣告變數的示例:
double salary; int vacationDays; long earthPopulation; boolean done;
可以看到,每個宣告以分號結束。由於宣告是一條完整的 Java語句,所以必須以分號結束。
變數名必須是一個以字母開頭並由字母或數字構成的序列。需要注意,與大多數程式設計語言相比,Java 中“ 字母” 和“ 數字” 的範圍更大。字母包括 ’ A’ ~ ’ Z’、 ’ a 1 ~ ’ z 1 、 或在某種語言中表示字母的任何 Unicode 字元。例如, 德國的使用者可以在變數名中使用字母 ;希臘人可以用π。同樣, 數字包括 '0' ~ '9 ’ 和在某種語言中表示數字的任何 Unicode 字 符。但 ‘+’ 和 '©’ 這樣的符號不能出現在變數名中,空格也不行。變數名中所有的字元都是有意義的,並且大小寫敏感。變數名的長度基本上沒有限制。
提示:如果想要知道哪些 Unicode 字元屬於 Java 中的“ 字母”, 可以使用 Character 類的 isJavaldentifierStart 和 isJavaldentifierPart 方法來檢查。
提示:儘管 $ 是一個合法的 Java 字元, 但不要在你自己的程式碼中使用這個字元。它只用 在 Java 編譯器或其他工具生成的名字中。
另外, 不能使用 Java 保留字作為變數名(請參看附錄 A 中的保留字列表。)
可以在一行中宣告多個變數:
int i , j; // both are integers
不過,不提倡使用這種風格。逐一宣告每一個變數可以提高程式的可讀性。
註釋:如前所述, 變數名對大小寫敏感, 例如,hireday 和 hireDay 是兩個不同的變數名。在對兩個不同的變數進行命名時, 最好不要只存在大小寫上的差異。 不過,在有些時候, 確實很難給變數取一個好的名字。於是,許多程式設計師將變數名命名為型別名, 例如:
Box box; // "Box" is the type and "box" is the variable name
還有一些程式設計師更加喜歡在變數名前加上字首“ a”:
Box aBox;
3.4.1 變數初始化
宣告一個變數之後,必須用賦值語句對變數進行顯式初始化, 千萬不要使用未初始化的 變數。例如, Java 編譯器認為下面的語句序列是錯誤的:
int vacationDays; System.out.println(vacationDays): // ERROR variable not initialized
要想對一個已經宣告過的變數進行賦值, 就需要將變數名放在等號(=) 左側, 相應取值 的 Java 表示式放在等號的右側。
int vacationDays; vacationDays = 12;
也可以將變數的宣告和初始化放在同一行中。例如:
int vacationDays = 12;
最後,在 Java 中可以將宣告放在程式碼中的任何地方。例如,下列程式碼的書寫形式在 Java 中是完全合法的:
double salary = 65000.0; System,out.println(salary); int vacationDays = 12; // OK to declare a variable here
在 Java 中, 變數的宣告儘可能地靠近變數第一次使用的地方, 這是一種良好的程式編寫風格。
C++ 註釋:C 和 C++ 區分變數的宣告與定義。例如:
int i = 10;
是一個定義, 而
extern int i;
是一個宣告。在 Java 中, 不區分變數的宣告與定義。
3.4.2 常量
在 Java 中, 利用關鍵字 final 指示常量。例如:
public class Constants { public static void main(String[] args) { final double CM_PER_INCH = 2.54; double paperWidth = 8.5; double paperHeight = 11; System,out.println("Paper size in centimeters: " + paperWidth * CM PER INCH + " by " + paperHeight * CM.PER.INCH); } }
關鍵字 final 表示這個變數只能被賦值一次。一旦被賦值之後,就不能夠再更改了。習慣上, 常量名使用全大寫。
在 Java 中,經常希望某個常量可以在一個類中的多個方法中使用,通常將這些常量稱為 類常量。可以使用關鍵字 static final設定一個類常量。 下面是使用類常量的示例:
public cl ass Constants〗 { public static final double CM_PER_INCH = 2.54; public static void main(Stringn args) { double paperWidth = 8.5; double paperHeight = 11; System.out.println("Paper size in centimeters: " + paperWidth * CMJERJNCH + by " + paperHeight * CM_PER_INCH) ; } }
需要注意, 類常量的定義位於 main方法的外部。因此,在同一個類的其他方法中也可以使用這個常量。而且,如果一個常量被宣告為 public,那麼其他類的方法也可以使用這個常量。 在這個示例中,Constants2.CM_PER-INCH 就是這樣一個常量。
C++ 註釋:const 是 Java 保留的關鍵字,但目前並沒有使用。在 Java 中, 必須使用 final 定義常量。
3.5 運算子
在 Java 中,使用算術運算子 + 、-、 * 、/ 表示加、減、 乘、除運算。 當參與 / 運算的兩個 運算元都是整數時, 表示整數除法;否則, 表示浮點除法。 整數的求餘操作(有時稱為取模) 用 % 表示。例如,15/2 等於7 ,15%2 等於 1 , 15.0/2 等於 7.5。
需要注意, 整數被 0 除將會產生一個異常, 而浮點數被 0 除將會得到無窮大或 NaN 結果。
註釋: 可移植性是 Java 語言的設計目標之一 , 無論在哪個虛擬機器上執行, 同一運算應該 得到同樣的結果3 對於浮點數的算術運算, 實現這樣的可移植性是相當困難的。double 型別使用 64 位儲存一個數值, 而有些處理器使用 80 位浮點暫存器這些暫存器增加了中間過程的計算精度. 例如, 以下運算:
double w = x * y / z;
很多 Intel 處理器計算 x * y,並且將結果儲存在 80 位的暫存器中, 再除以 z 並將結果截斷為 64 位„ 這樣可以得到一個更加精確的計算結果,並且還能夠避免產生指數溢位。但是, 這個結果可能與始終在 64 位機器上計算的結果不一樣。 因此,Java 虛擬機器的最初規範規定所有的中間計算都必須進行截斷。這種行為遭到了數值計算團體的反對。 截斷計算不僅可能導致溢位, 而且由於截斷操作需要消耗時間, 所以在計算速度上實際上要比精確計算慢。 為此,Java 程式設計語言承認了最優效能與理想結果之間存在的衝突,並給予了改進。在預設情況下, 虛擬機器設計者允許對中間計算結果採用擴充套件的精度。 但是, 對於使用 strictfp 關鍵字標記的方法必須使用嚴格的浮點計算來生成可再生的結果。例如,可以把 main 方法標記為
public static strictfp void main(String[] args)
於是,在 main 方法中的所有指令都將使用嚴格的浮點計算。如果將一個類標記為 strictfp, 這個類中的所有方法都要使用嚴格的浮點計算。
實際的計算方式將取決於 Intel 處理器的行為。在預設情況下,中間結果允許使用擴充套件的指數, 但不允許使用擴充套件的尾數(Intel 晶片在截斷尾數時並不損失效能)。因此,這兩種方式的區別僅僅在於採用預設的方式不會產生溢位, 而採用嚴格的計算有可能產生溢位。
如果沒有仔細閱讀這個註釋, 也沒有什麼關係。 對大多數程式來說, 浮點溢位不屬於大問題。在本書中, 將不使用 strictfp 關鍵字。
3.5.1 數學函式與常量
在 Math類中,包含了各種各樣的數學函式。在編寫不同類別的程式時,可能需要的函 數也不同。
要想計算一個數值的平方根, 可以使用 sqrt 方法:
double x = 4; double y = Math.sqrt(x); System.out.println(y); // prints 2.0
註釋: println 方法和 sqrt 方法存在微小的差異。println 方法處理 System.out 物件。但是, Math 類中的 sqrt 方法處理的不是物件,這樣的方法被稱為靜態方法。有關靜態方法的詳細內容請參看第 4 章。
在 Java中,沒有冪運算, 因此需要藉助於 Math 類的 pow 方法。語句:
double y = Math.pow(x, a);
將 y 的值設定為 x 的 a 次冪( xa)。pow 方法有兩個 double 型別的引數, 其返回結果也為 double 型別。
floorMod 方法的目的是解決一個長期存在的有關整數餘數的問題。考慮表示式 n % 2。 所有人都知道, 如果 n 是偶數, 這個表示式為 0 ; 如果 n 是奇數, 表示式則為 1。當然, 除 非 n 是負數 如果 n 為負,這個表示式則為 -1。為什麼呢? 設計最早的計算機時,必須有人制定規則,明確整數除法和求餘對負數運算元該如何處理。數學家們幾百年來都知道這樣一 個最優(或“ 歐幾里德”)規則:餘數總是要>=0。不過, 最早制定規則的人並沒有翻開數學書好好研究,而是提出了一些看似合理但實際上很不方便的規則。
下面考慮這樣一個問題: 計算一個時鐘時針的位置。這裡要做一個時間調整, 而且要歸 一化為一個 0 ~ 11 之間的數。 這很簡單: position + adjustment) % 12。不過, 如果這個調整為負會怎麼樣呢? 你可能會得到一個負數。所以要引入一個分支, 或者使用 (position + adjustment) % 12 + 12) % 12。不管怎樣, 總之都很麻煩。
floorMod 方法就讓這個問題變得容易了:floorMod(position + adjustment, 12) 總會得到一個 0 ~ 11 之間的數。(遺憾的是,對於負除數,floorMod 會得到負數結果,不過這種情況在實際中很少出現。)
Math 類提供了一些常用的三角函式:
Math,sin
Math.cos
Math.tan
Math.atan
Math.atan2
還有指數函式以及它的反函式--自然對數以及以 10 為底的對數:
Math.exp
Math.log
Math.logl0
最後,Java 還提供了兩個用於表示 π 和 e 常量的近似值:
Math.PI
Math.E
提示:不必在數學方法名和常量名前新增字首“ Math”, 只要在原始檔的頂部加上下面 這行程式碼就可以了。
import static java.1ang.Math.*; 例如: System.out.println("The square root of \u03C0 is " + sqrt(PI));
在第 4 章中將討論靜態匯入。
註釋: 在 Math 類中, 為了達到最快的效能, 所有的方法都使用計算機浮點單元中的例程.. 如果得到一個完全可預測的結果比執行速度更重要的話, 那麼就應該使用 StrictMath 類,, 它使用“ 自由釋出的 Math 庫”(fdlibm) 實現演算法, 以確保在所有平臺上得到相同的結果。 有關這些演算法的原始碼請參看 www.netlib.org/fdlibm ( 當 fdlibm 為一個函式提供了 多個定義時, StrictMath 類就會遵循 IEEE 754 版本,它的名字將以“ e” 開頭)
3.5.2 數值型別之間的轉換
經常需要將一種數值型別轉換為另一種數值型別。圖 3-1 給出了數值型別之間的合法 轉換。
在圖 3-1 中有 6 個實心箭頭,表示無資訊丟失的轉換;有 3 個虛箭頭, 表示可能有精度 損失的轉換。 例如,123 456 789 是一個大整數, 它所包含的位數比 float 型別所能夠表達的位數多。 當將這個整型數值轉換為 float 型別時, 將會得到同樣大小的結果,但卻失去了一定 的精度。
int n = 123456789; float f = n; // f is 1.23456792E8
當使用上面兩個數值進行二元操作時(例如 n + f,n 是整數, f 是浮點數,) 先要將兩個運算元轉換為同一種型別,然後再進行計算。
•如果兩個運算元中有一個是 double 型別, 另一個運算元就會轉換為 double 型別。
•否則,如果其中一個運算元是 float 型別,另一個運算元將會轉換為 float 型別。
•否則, 如果其中一個運算元是 long 型別, 另一個運算元將會轉換為 long 型別。
•否則, 兩個運算元都將被轉換為 int 型別。
3.5.3 強制型別轉換
在上一小節中看到, 在必要的時候, int 型別的值將會自動地轉換為 double 型別。但另 一方面,有時也需要將 double 轉換成 int。 在 Java 中, 允許進行這種數值之間的型別轉換。 當然, 有可能會丟失一些資訊。在這種情況下,需要通過強制型別轉換( cast) 實現這個操 作。強制型別轉換的語法格式是在圓括號中給出想要轉換的目標型別,後面緊跟待轉換的變數名。例如:
double x * 9.997; int nx = (int) x;
這樣, 變數 nx 的值為 9。強制型別轉換通過截斷小數部分將浮點值轉換為整型。
如果想對浮點數進行舍入運算, 以便得到最接近的整數(在很多情況下, 這種操作更有用,) 那就需要使用 Math_ round 方法:
double x z 9.997; int nx = (int) Math.round(x);
現在, 變數 nx 的值為 10。 當呼叫 round 的時候, 仍然需要使用強制型別轉換( int。) 其原因 是 round 方法返回的結果為 long 型別,由於存在資訊丟失的可能性,所以只有使用顯式的強 制型別轉換才能夠將 long 型別轉換成 int 型別。
警告: 如果試圖將一個數值從一種型別強制轉換為另一種型別, 而又超出了目標型別的 表示範圍,結果就會截斷成一個完全不同的值。例如,(byte ) 300 的實際值為 44。
C++ 註釋:不要在 boolean 型別與任何數值型別之間進行強制型別轉換, 這樣可以防止發生錯誤。只有極少數的情況才需要將布林型別轉換為數值型別,這時可以使用條件表 達式 b ? 1:0。
3.5.4 結合賦值和運算子
可以在賦值中使用二元運算子,這是一種很方便的簡寫形式。例如,x+=4等價於 x=x+4;(一般地, 要把運算子放在 = 號左邊,如 *= 或 %=)。
註釋: 如果運算子得到一個值, 其型別與左側運算元的型別不同, 就會發生強制型別轉換。 例如,如果 X 是一個 int, 則以下語句 x += 3.5; 是合法的, 將把 X 設定為(int)(x + 3.5)。
3.5.5 自增與自減運算子
當然, 程式設計師都知道加 1、 減 1 是數值變數最常見的操作。在 Java 中, 借鑑了 C 和 C++ 的做法,也提供了自增、 自減運算子: n++ 將變數 n 的當前值加 1, n-- 則將 n 的值減 1。例 如, 以下程式碼:
int n = 12; n++;
將 n 的值改為 13。由於這些運算子會改變變數的值,所以它們的運算元不能是數值。例如, 4++ 就不是一個合法的語句。
實際上, 這些運算子有兩種形式;上面介紹的是運算子放在運算元後面的“ 字尾” 形式。 還有一種“ 字首” 形式:++n。字尾和字首形式都會使變數值加 1 或減 1。但用在表示式中時, 二者就有區別了。字首形式會先完成加 1; 而字尾形式會使用變數原來的值。
int m = 7; int n = 7; int a = 2 * ++m; // now a is 16, m is 8 int b = 2 * n++; // now b is 14, n is 8
建議不要在表示式中使用 ++, 因為這樣的程式碼很容易讓人閒惑,而且會帶來煩人的 bug。
3.5.6 關係和boolean運算子
Java 包含豐富的關係運算子:要檢測相等性,可以使用兩個等號 = 。例如,3==7的值為false。另外可以使用!= 檢測不相等。例如,3!=7的值為true。
最後, 還有經常使用的 < (小於、) > (大於) 、<=(小於等於)和 >= (大於等於)運算子。
Java 沿用了 C++ 的做法,使用 && 表示邏輯“ 與” 運算子,使用丨| 表示邏輯“ 或” 運算子。從 != 運算子可以想到,感嘆號!就是邏輯非運算子。&& 和丨| 運算子是按照“ 短路” 方 式來求值的: 如果第一個運算元已經能夠確定表示式的值,第二個運算元就不必計算了。如 果用 && 運算子合並兩個表示式,
expression1 && expression2
而且已經計算得到第一個表示式的真值為 false, 那麼結果就不可能為 true。因此, 第二個表示式就不必計算了。可以利用這一點來避免錯誤。例如, 在下面的表示式中:
x != 0&&1 / x > x + y // no division by 0
如果 x 等於 0, 那麼第二部分就不會計算。因此,如果 x 為 0, 也就不會計算 1 / x , 除 以 0 的錯誤就不會出現。
類似地, 如果第一個表示式為 true, expression1 || expression2的值就自動為 true, 而無需 計算第二個表示式。
最後一點,Java 支援三元操作符?:,這個操作符有時很有用。如果條件為 true, 下面的 表示式
condition ? expression1: expression2
就為第一個表示式的值,否則計算為第二個表示式的值。例如,x < y ? x : y會返回 x 和 y 中較小的一個。
3.5.7 位運算子
處理整型型別時,可以直接對組成整型數值的各個位完成操作。這意味著可以使用掩碼技術得到整數中的各個位。位運算子包括:
& ("and") | ("or") A ("XOr") ~ ("not")
這些運算子按位模式處理。例如, 如果 n 是一個整數變數,而且用二進位制表示的 n 從右邊數第 4 位為 1,則
int fourthBitFromRight = (n & OblOOO) / OblOOO;
會返回 1,否則返回 0。利用 & 並結合使用適當的 2 的冪, 可以把其他位掩掉, 而只保留其 中的某一位。
註釋:應用在布林值上時, & 和丨運算子也會得到一個布林值。這些運算子與 && 和 ||運 算符很類似,不過 & 和丨運算子不採用“ 短路” 方式來求值, 也就是說,得到計算結果之前兩個運算元都需要計算。
另外,還有>>和 <<運算子將位模式左移或右移。需要建立位模式來完成位掩碼時, 這 兩個運算子會很方便:
int fourthBitFromRight = (n & (1« 3)) » 3;
最後,>>> 運算子會用 0 填充高位,這與>>不同,它會用符號位填充高位。不存在<<<運算子。
警告: 移位運算子的右運算元要完成模 32 的運算(除非左運算元是 long 型別, 在這種情 況下需要對右運算元模 64 )。 例如, 1<<35 的值等同於 1 <<3 或 8。
C++ 註釋: 在 C/C++ 中,不能保證>> 是完成算術移位(擴充套件符號位)還是邏輯移位(填 充 0。) 實現者可以選擇其中更高效的任何一種做法。 這意味著 C/C++ >>運算子對於負數生成的結果可能會依賴於具體的實現。Java 則消除了這種不確定性。
3.5.8 括號與運算子級別
表 3-4 給出了運算子的優先順序。 如果不使用圓括號, 就按照給出的運算子優先順序次序進行計算。同一個級別的運算子按照從左到右的次序進行計算(除了表中給出的右結合運算子外。)例如,由於 && 的優先順序比 || 的優先順序高, 所以表示式
a && b | c
等價於
(a M b) 11 c
有因為+=是右結合運算子,所以表示式
a += b += c
等價於
a += (b += c)
也就是將 b += c 的結果(加上 c 之後的 b) 加到 a 上。
C++ 註釋:與 C 或 C++ 不同,Java 不使用逗號運算子。不過, 可以在 for語 句 的 第 1 和 第 3 部分中使用逗號分隔表示式列表。
3.5.9 列舉型別
有時候,變數的取值只在一個有限的集合內。例如: 銷售的服裝或比薩餅只有小、中、 大和超大這四種尺寸。當然, 可以將這些尺寸分別編碼為 1、2、3、4 或 S、 M、 L、X。但 這樣存在著一定的隱患。在變數中很可能儲存的是一個錯誤的值(如 0 或 m)。
針對這種情況, 可以自定義列舉型別。列舉型別包括有限個命名的值。 例如,
enum Size { SMALL, MEDIUM, LARGE, EXTRA.LARCE };
現在,可以宣告這種型別的變數:
Size s = Size.MEDIUM;
Size 型別的變數只能儲存這個型別宣告中給定的某個列舉值,或者 null 值,null 表示這 個變數沒有設定任何值。
有關列舉型別的詳細內容將在第 5 章介紹。
3.6 字串
從概念上講, Java 字串就是 Unicode 字元序列。 例如, 串“ Java\u2122” 由 5 個 Unicode 字元 J、a、 v、a 和™。Java 沒有內建的字串型別, 而是在標準 Java 類庫中提供了 一個預定義類,很自然地叫做 String。每個用雙引號括起來的字串都是 String類的一個例項:
String e = ""; // an empty string
String greeting = "Hello";
3.6.1 子串
String 類的 substring 方法可以從一個較大的字串提取出一個子串。例如:
String greeting = "Hello";
String s = greeting.substring(0, 3);
建立了一個由字元“ Hel” 組成的字串。
substring 方法的第二個引數是不想複製的第一個位置。這裡要複製位置為 0、 1 和 2 (從 0 到 2, 包括 0 和 2 ) 的字元。在 substring 中從 0 開始計數,直到 3 為止, 但不包含 3。 substring 的工作方式有一個優點:容易計運算元串的長度。字串 s.substring(a, b) 的長度 為 b-a。例如, 子串“ Hel ” 的長度為 3-0=3。
3.6.2 拼接
與絕大多數的程式設計語言一樣,Java語言允許使用 + 號連線(拼接)兩個字串。
String expletive = "Expletive"; String PC13 = "deleted"; String message = expletive + PC13;
上述程式碼將“ Expletivedeleted” 賦給變數 message (注意, 單詞之間沒有空格, + 號按照 給定的次序將兩個字串拼接起來)。
當將一個字串與一個非字串的值進行拼接時,後者被轉換成字串(在第 5 章中可以看到,任何一個 Java 物件都可以轉換成字串)。例如:
int age = 13; String rating = "PC" + age;
rating 設定為“ PG13”。 這種特性通常用在輸出語句中。例如:
System.out.println("The answer is " + answer);
這是一條合法的語句, 並且將會列印出所希望的結果(因為單詞 is 後面加了一個空格, 輸出 時也會加上這個空格)。
如果需要把多個字串放在一起, 用一個定界符分隔,可以使用靜態 join 方法:
String all = String.join(" / ", "S", "M","L", "XL"); // all is the string "S / H / L / XL"
3.6.3 不可變字串
String 類沒有提供用於修改字串的方法。如果希望將 greeting 的內容修改為“ Help!”, 不能直接地將 greeting 的最後兩個位置的字元修改為‘ p ’ 和‘ ‘!’。 這對於 C 程式設計師來說, 將會感到無從下手。如何修改這個字串呢? 在 Java中實現這項操作非常容易。首先提取需要的字元, 然後再拼接上替換的字串:
greeting = greeting.substring(0, 3) + "p!";
上面這條語句將 greeting 當前值修改為“ Help ! ”。
由於不能修改 Java 字串中的字元, 所以在 Java 文件中將 String 類物件稱為不可變字串, 如同數字 3 永遠是數字 3 —樣,字串“ Hello” 永遠包含字元 H、 e、1、 1 和 o 的代 碼單元序列, 而不能修改其中的任何一個字元。當然, 可以修改字串變數 greeting, 讓它 引用另外一個字串, 這就如同可以將存放 3 的數值變數改成存放 4 一樣。
這樣做是否會降低執行效率呢? 看起來好像修改一個程式碼單元要比建立一個新字串更 加簡潔。答案是:也對,也不對。的確, 通過拼接“ Hel ” 和“ p! ” 來建立一個新字串的 效率確實不高。但是,不可變字串卻有一個優點:編譯器可以讓字串共享。
為了弄清具體的工作方式,可以想象將各種字串存放在公共的儲存池中。字串變數 指向儲存池中相應的位置。如果複製一個字串變數, 原始字串與複製的字串共享相同的字元。
總而言之,Java 的設計者認為共享帶來的高效率遠遠勝過於提取、 拼接字串所帶來的低效率。檢視一下程式會發現:很少需要修改字串, 而是往往需要對字串進行比較(有 一種例外情況,將來自於檔案或鍵盤的單個字元或較短的字串彙整合字串。為此, Java 提供了一個獨立的類,在 3.6.9 節中將詳細介紹)。
C++ 註釋: 在 C 程式設計師第一次接觸 Java 字串的時候, 常常會感到迷惑, 因為他們總將 字串認為是字元型陣列:
char greeting[] = "Hello";
這種認識是錯誤的, Java 字串大致類似於 char* 指標,
char* greeting = "Hello";
當採用另一個字串替換 greeting 的時候, Java 程式碼大致進行下列操作:
char* temp = malloc(6);
stmcpy(temp, greeting, 3);
strncpy(temp + 3, "p! " , 3);
greeting = temp;
的確, 現在 greeting 指向字串“ Help!”。 即使一名最頑固的 C 程式設計師也得承認 Java 語法要比一連串的 stmcpy 呼叫舒適得多。然而,如果將 greeting 斌予另一個值又會怎樣呢?
greeting = "Howdy";
這樣做會不會產生記憶體遺漏呢? 畢競, 原始字串放置在堆中。十分幸運,Java 將 自動地進行垃圾回收。 如果一塊記憶體不再使用了, 系統最終會將其回收。
對於一名使用 ANSI C++ 定義的 string 類的 C++ 程式設計師, 會感覺使用 Java 的 String 型別更為舒適。C++ string 物件也自動地進行記憶體的分配與回收。記憶體管理是通過構造器、 賦值操作和析構器顯式執行的。然而,C++ 字串是可修改的, 也就是說,可以修改字串中的單個字元。
3.6.4 檢測字串是否相等
可以使用 equals 方法檢測兩個字串是否相等。對於表示式:
s.equals(t)
如果字串 s 與字串 t 相等, 則返回 true ; 否則, 返回 false。需要注意,s與 t 可以是字串變數, 也可以是字串字面量。 例如, 下列表示式是合法的:
"Hello".equals(greeting)
要想檢測兩個字串是否相等,而不區分大小寫, 可以使用 equalsIgnoreCase 方法。
"Hello".equalsIgnoreCase("hel1o")
一定不要使用 = 運算子檢測兩個字串是否相等! 這個運算子只能夠確定兩個字串是否放置在同一個位置上。當然, 如果字串放置在同一個位置上, 它們必然相等。但是, 完全有可能將內容相同的多個字串的拷貝放置在不同的位置上。
String greeting = "Hello"; //initialize greeting to a string if (greeting == "Hello") . // probably true if (greeting.substring(0, 3) == "Hel") . . . // probably false
如果虛擬機器始終將相同的字串共享, 就可以使用=運算子檢測是否相等。但實際上 只有字串常量是共享的,而 + 或 substring 等操作產生的結果並不是共享的。因此,千萬不 要使甩== 運算子測試字串的相等性, 以免在程式中出現糟糕的 bug。從表面上看, 這種 bug 很像隨機產生的間歇性錯誤。
C++ 註釋: 對於習慣使用 C++ 的 string 類的人來說, 在進行相等性檢測的時候一定要特別小心。C++ 的 string 類過載了 == 運算子以便檢測字串內容的相等性。 可惜 Java 沒 有采用這種方式, 它的字串“ 看起來、 感覺起來” 與數值一樣, 但進行相等性測試時, 其操作方式又類似於指標。語言的設計者本應該像對 + 那樣也進行特殊處理, 即重定義 =運算子。 當然,每一種語言都會存在一些不太一致的地方。
C 程式設計師從不使用 =對字串進行比較, 而使用 strcmp 函式。Java 的 compareTo 方法與 strcmp 完全類似, 因此,可以這樣使用:
if (greeting.compareTo("Hel1oH) == 0} . . .
不過, 使用 equals 看起來更為清晰。
3.6.5 空串與Null串
空串 "" 是長度為 0 的字串。可以呼叫以下程式碼檢查一個字串是否為空:
if (str.length() = 0) 或 if (str.equals(""))
空串是一個 Java 物件, 有自己的串長度( 0 ) 和內容(空)。不過, String 變數還可以存放一個特殊的值, 名為 null, 這表示目前沒有任何物件與該變數關聯(關於 null 的更多資訊 請參見第 4 章)。要檢查一個字串是否為 null, 要使用以下條件:
if (str == null)
有時要檢查一個字串既不是 null 也不為空串,這種情況下就需要使用以下條件:
if (str != null && str.lengthO != 0)
首先要檢查 str 不為 null。在第 4 章會看到, 如果在一個 mill 值上呼叫方法, 會出現錯誤。
3.6.6 碼點與程式碼單元
Java 字串由 char 值序列組成。從 3.3.3 節“ char 型別” 已經看到, char 資料型別是一個採用 UTF-16 編碼表示 Unicode 碼點的程式碼單元。大多數的常用 Unicode 字元使用一個程式碼單元就可以表示,而輔助字元需要一對程式碼單元表示。
length 方法將返回採用 UTF-16 編碼表示的給定字串所需要的程式碼單元數量。例如:
String greeting = "Hello"; int n = greeting.length。; // is 5 .
要想得到實際的長度,即碼點數量,可以呼叫:
int cpCount = greeting.codePointCount(0, greeting.lengthQ);
呼叫 s.charAt(n) 將返回位置 n 的程式碼單元,n 介於 0 ~ s.length()-l 之間。例如:
char first = greeting.charAt(0); // first is 'H' char last = greeting.charAt(4); // last is ’o’
要想得到第 i 個碼點,應該使用下列語句
int index = greeting.offsetByCodePoints(0, i); int cp = greeting.codePointAt(index);
註釋: 類似於 C 和 C++, Java 對字串中的程式碼單元和碼點從 0 開始計數。 為什麼會對程式碼單元如此大驚小怪? 請考慮下列語句:
is the set of octonions 使用 UTF-16 編碼表示字元(U+1D546) 需要兩個程式碼單元。呼叫 char ch = sentence.charAt(1) 返回的不是一個空格,而是的第二個程式碼單元。為了避免這個問題, 不要使用 char 型別。 這太底層了。
如果想要遍歷一個字串,並且依次査看每一個碼點, 可以使用下列語句:
int cp = sentence.codePointAt(i); if (Character.isSupplementaryCodePoint(cp)) i+= 2; else i++; 可以使用下列語句實現回退操作: i ; if (CharacterssSurrogate(sentence.charAt(i))) i ; int cp = sentence.codePointAt(i);
顯然, 這很麻煩。更容易的辦法是使用 codePoints 方法, 它會生成一個 int 值的“ 流”, 每個 int 值對應一個碼點。(流將在卷 II 的第 2 章中討論〉 可以將它轉換為一個陣列(見 3.10 節,) 再完成遍歷。
int[] codePoints = str.codePointsO.toArrayO;
反之,要把一個碼點陣列轉換為一個字串, 可以使用建構函式(我們將在第 4 章詳細 討論建構函式和 new 操作符 )。
String str = new String(codePoints, 0, codePoints.length);
3.6.7 String API
Java 中的 String類包含了 50 多個方法。令人驚訝的是絕大多數都很有用, 可以設想使用的頻繁非常高。下面的 API 註釋彙總了一部分最常用的方法。
註釋: 可以發現,本書中給出的 API 註釋會有助於理解 Java 應用程式程式設計介面( API )。 每一個 API 的註釋都以形如 java.lang.String 的類名開始。(java.lang 包的重要性將在第 4 章給出解釋。) 類名之後是一個或多個方法的名字、 解釋和引數描述。
在這裡, 一般不列出某個類的所有方法, 而是選擇一些最常用的方法, 並以簡潔的方式給予描述。 完整的方法列表請參看聯機文件(請參看 3.6.8 節)。
這裡還列出了所給類的版本號。如果某個方法是在這個版本之後新增的, 就會給出 一個單獨的版本號。
API java.lang.string 1.0
• char charAt (int index) 返回給定位置的程式碼單元。除非對底層的程式碼單元感興趣, 否則不需要呼叫這個方法。
• int codePointAt(int Index) 5.0 返回從給定位置開始的碼點。
• int offsetByCodePoints(int startlndex, int cpCount) 5.0 返回從 startlndex 程式碼點開始,位移 cpCount 後的碼點索引。
• int compareTo(String other) 按照字典順序,如果字串位於 other 之前, 返回一個負數;如果字串位於 other 之 後,返回一個正數;如果兩個字串相等,返回 0。
• IntStream codePoints() 8 將這個字串的碼點作為一個流返回。呼叫 toArray 將它們放在一個陣列中。
• new String(int[] codePoints, int offset, int count) 5.0 用陣列中從 offset 開始的 count 個碼點構造一個字串。
• boolean equals(0bject other) 如果字串與 other 相等, 返回 true。
•boolean equalsIgnoreCase(String other ) 如果字串與 other 相等 ( 忽略大小寫,) 返回 tme。
•boolean startsWith(String prefix )
•boolean endsWith(String suffix )
如果字串以 suffix 開頭或結尾, 則返回 true。
•int indexOf(String str)
•int indexOf(String str, int fromlndex )
•int indexOf(int cp) •int indexOf(int cp, int fromlndex )
返回與字串 str 或程式碼點 cp 匹配的第一個子串的開始位置。這個位置從索引 0 或 fromlndex 開始計算。 如果在原始串中不存在 str, 返回 -1。 •int lastIndexOf(String str)
•Int lastIndexOf(String str, int fromlndex )
•int lastindexOf(int cp)
•int lastindexOf(int cp, int fromlndex )
返回與字串 str 或程式碼點 cp 匹配的最後一個子串的開始位置。 這個位置從原始串尾端或 fromlndex 開始計算。
•int length( ) 返回字串的長度。
•int codePointCount(int startlndex , int endlndex ) 5.0 返回 startlndex 和 endludex-1之間的程式碼點數量。沒有配成對的代用字元將計入程式碼點。 參
•String replace( CharSequence oldString,CharSequence newString) 返回一個新字串。這個字串用 newString 代替原始字串中所有的 oldString。可 以用 String 或 StringBuilder 物件作為 CharSequence 引數。
• String substring(int beginlndex )
• String substring(int beginlndex, int endlndex )
返回一個新字串。這個字串包含原始字串中從 beginlndex 到串尾或 endlndex-1的所有程式碼單元。
• String toLowerCase( )
• String toUpperCase( )
返回一個新字串。 這個字串將原始字串中的大寫字母改為小寫,或者將原始字 符串中的所有小寫字母改成了大寫字母。
• String trim( ) 返回一個新字串。這個字串將刪除了原始字串頭部和尾部的空格。
• String join(CharSequence delimiter, CharSequence ... elements ) 8 返回一個新字串, 用給定的定界符連線所有元素。
註釋:在 API 註釋中, 有一些 CharSequence 型別的引數這是一種介面型別, 所有字串都屬於這個介面。第 6 章將介紹更多有關介面型別的內容。現在只需要知道只要看到 一個 CharSequence 形參, 完全可以傳入 String 型別的實參。
3.6.8 閱讀聯機 API文件
正如前面所看到的, String 類包含許多方法。 而且, 在標準庫中有幾苄個類, 方法數tt 更加驚人。要想記住所有的類和方法是一件不太不可能的事情。 因此,學會使用線上 API 文件十分重要,從中可以查閱到標準類庫中的所有類和方法。API文件是 JDK 的一部分, 它是 HTML 格式的。 讓瀏覽器指向安裝 roK 的 docs/api/index.html 子目錄, 就可以看到所示的螢幕。(圖略)
3.6.9 構建字串
有些時候, 需要由較短的字串構建字串, 例如, 按鍵或來自檔案中的單詞。採用字串連線的方式達到此目的效率比較低。每次連線字串, 都會構建一個新的 String 物件, 既耗時, 又浪費空間。使用 StringBuilder類就可以避免這個問題的發生。
如果需要用許多小段的字串構建一個字串, 那麼應該按照下列步驟進行。 首先, 構 建一個空的字串構建器:
StringBuilder builder = new StringBuilderO;
當每次需要新增一部分內容時, 就呼叫 append 方法。
builder.append(ch); // appends a single character bui1der.append(str); // appends a string
在需要構建字串時就凋用 toString 方法, 將可以得到一個 String 物件, 其中包含了構建器 中的字元序列。
String completedString = builder.toStringO;
註釋: 在 JDK5.0 中引入 StringBuilder 類。 這個類的前身是 StringBuffer, 其效率稍有些低, 但允許採用多執行緒的方式執行新增或刪除字元的操作。如果所有字串在一個單線 程中編輯 (通常都是這樣) , 則應該用 StringBuilder 替代它。 這兩個類的 API是相同的。
下面的 API 註釋包含了 StringBuilder 類中的重要方法。
API java.lang.StringBuilder 5.0
• StringBuilder() 構造一個空的字串構建器。
• int length() 返回構建器或緩衝器中的程式碼單元數量。
• StringBuilder appencl(String str) 追加一個字串並返回 this。
• StringBuilder append(char c) 追加一個程式碼單元並返回 this。
• StringBuilder appendCodePoint(int cp) 追加一個程式碼點,並將其轉換為一個或兩個程式碼單元並返回 this。
• void setCharAt(int i ,char c) 將第 i 個程式碼單元設定為 c。
• StringBuilder insert(int offset,String str) 在 offset 位置插入一個字串並返回 this。
•StringBuilder insert(int offset,Char c) 在 offset 位置插入一個程式碼單元並返回 this。
• StringBuilder delete(1 nt startindex,int endlndex) 刪除偏移量從 startindex 到 -endlndex-1 的程式碼單元並返回 this
• String toString() 返回一個與構建器或緩衝器內容相同的字串.。
3.7 輸入與輸出
為了增加後面示例程式的趣味性,需要程式能夠接收輸入,並以適當的格式輸出。當 然, 現代的程式都使用 GUI 收集使用者的輸人, 然而,編寫這種介面的程式需要使用較多的工具與技術,目前還不具備這些條件。主要原因是需要熟悉 Java 程式設計語言,因此只要有簡單的用於輸入輸出的控制檯就可以了。第 10 章 ~ 第 12 章將詳細地介紹 GUI 程式設計。
3.7.1 讀取輸入
前面已經看到,列印輸出到“ 標準輸出流”(即控制檯視窗)是一件非常容易的事情,只要 呼叫 System.out.println 即可。然而,讀取“ 標準輸入流” System.in 就沒有那麼簡單了。要想通過控制檯進行輸入,首先需要構造一個 Scanner 物件,並與“ 標準輸入流” System.in 關聯。
Scanner in = new Scanner(System.in);
現在,就可以使用 Scanner 類的各種方法實現輸入操作了。例如, nextLine 方法將輸入 一行。
System.out.print("How old are you? "); int age = in.nextInt();
與此類似,要想讀取下一個浮點數, 就呼叫 nextDouble 方法。
在程式清單 3-2 的程式中,詢問使用者姓名和年齡, 然後列印一條如下格式的訊息:
Hello, Cay. Next year, you'll be 57
最後,在程式的最開始新增上一行:
import java.util.*;
Scanner 類定義在java.util 包中。 當使用的類不是定義在基本java.lang 包中時,一定要使用 import 指示字將相應的包載入進來。有關包與 import 指示字的詳細描述請參看第 4 章。
import java.util.*;
public class InputTest { public static void main(String口 args) { Scanner in = new Scanner(System.in); // get first input System,out.print("What is your name? "); String name = in.nextLine(); // get second input System,out.print("How old are you? "); int age = in.nextlntO; //display output on console System.out.println("Hello, " + name + Next year, you'll be " + (age + 1)); } }
註釋: 因為輸入是可見的, 所以 Scanner 類不適用於從控制檯讀取密碼。Java SE 6 特別 引入了 Console 類實現這個目的。要想讀取一個密碼, 可以採用下列程式碼:
Console cons = System.console(); String username = cons.readLine("User name: "); char [] passwd = cons.readPassword("Password:");
為了安全起見, 返回的密碼存放在一維字元陣列中, 而不是字串中。在對密碼進行處理之後,應該馬上用一個填充值覆蓋陣列元素(陣列處理將在 3.10 節介紹)。
採用 Console 物件處理輸入不如採用 Scanner 方便。每次只能讀取一行輸入, 而沒有能夠讀取一個單詞或一個數值的方法。
API java.util.Scanner 5.0
• Scanner (InputStream in) 用給定的輸入流建立一個 Scanner 物件。
• String nextLine( ) 讀取輸入的下一行內容。
• String next( ) 讀取輸入的下一個單詞(以空格作為分隔符。)
• int nextlnt( )
• double nextDouble( )
讀取並轉換下一個表示整數或浮點數的字元序列。
• boolean hasNext( ) 檢測輸入中是否還有其他單詞。
• boolean hasNextInt( )
• boolean hasNextDouble( )
檢測是否還有表示整數或浮點數的下一個字元序列。
API java.Iang.System 1.0
• static Console console( ) 6
如果有可能進行互動操作, 就通過控制檯視窗為互動的使用者返回一個 Console 物件, 否則返回 null。對於任何一個通過控制檯視窗啟動的程式, 都可使用 Console 物件。 否則, 其可用性將與所使用的系統有關。
API java.io.Console 6
• static char[] readPassword(String prompt, Object...args)
• static String readLine(String prompt, Object...args)
顯示字串 prompt 並且讀取使用者輸入,直到輸入行結束。args 引數可以用來提供輸人 格式。有關這部分內容將在下一節中介紹。
3.7.2 格式化輸出
可以使用 SyStem.out.print(x) 將數值 x 輸出到控制檯上。這條命令將以 x 對應的資料型別所允許的最大非 0 數字位數列印輸出 X。 例如:
double x = 10000.0 / 3.0; System.out.print(x);
列印 3333.333333333335
如果希望顯示美元、美分等符號, 則有可能會出現問題。
在早期的 Java 版本中,格式化數值曾引起過一些爭議。慶幸的是,Java SE 5.0 沿用了 C 語言庫函式中的 printf方法。例如,呼叫
System.out.printf("%8.2f", x);
可以用 8 個字元的寬度和小數點後兩個字元的精度列印 x。也就是說,列印輸出一個空格和 7 個字元, 如下所示:
3333.33
在 printf中,可以使用多個引數, 例如:
System.out.printf("Hello, %s. Next year, you'll be %d", name, age);
每一個以 % 字元開始的格式說明符都用相應的引數替換。 格式說明符尾部的轉換符將指示被格式化的數值型別:f 表示浮點數,s 表示字串,d 表示十進位制整數。表 3-5列出了所有轉換符。
另外,還可以給出控制格式化輸出的各種標誌。表 3-6 列出了所有的標誌。例如,逗號 標誌增加了分組的分隔符。 即
Systen.out.printf("%,.2f", 10000.0 / 3.0);
可以使用多個標誌,例如,“ %,( .2f” 使用分組的分隔符並將負數括在括號內。
註釋:可以使用 s 轉換符格式化任意的物件。 對於任意實現了 Formattable 介面的物件都將呼叫 formatTo 方法;否則將呼叫 toString 方法, 它可以將物件轉換為字串。在第 5 章中將討論 toString 方法, 在第 6 章中將討論介面。
可以使用靜態的 String.format 方法建立一個格式化的字串, 而不列印輸出:
String message = String.format("Hello, %s. Next year, you'll be %d", name , age);
基於完整性的考慮, 下面簡略地介紹 printf方法中日期與時間的格式化選項。在新程式碼中, 應當使用卷 II 第 6 章中介紹的 java.time 包的方法。 不過你可能會在遺留程式碼中看到 Date 類和相關的格式化選項。格式包括兩個字母, 以 t 開始, 以表 3-7 中的任意字母結束。 例如,
System.out.printf("%tc", new Date());
這條語句將用下面的格式列印當前的日期和時間:
Mon Feb 09 18:05:19 PST 2015
從表 3-7 可以看到, 某些格式只給出了指定日期的部分資訊 。例如, 只有 日期 或 月份 如果需要多次對口期操作才能實現對每一部分進行格式化的目的就太笨拙了。為此, 可以採用一 個格式化的字串指出要被格式化的引數索引。索引必須緊跟在 % 後面, 並以 $ 終止。 例如
System.out.printf("%1$s %2$tB %2$te, %2$tY", "Due date:", new Date());
列印 Due date: February 9, 2015
還可以選擇使用 < 標誌。它指示前面格式說明中的引數將被再次使用。也就是說, 下列語句將產生與前面語句同樣的輸出結果:
System.out .printf("%s %tB %<te, %<tY", "Due date:", new DateO);
提示: 參 教 索 引 值 從 1 開 始, 而 不 是 從 0 開 始, 對 第 1個 參 數 格 式 化 這 就 避 免 了 與 0 標 志 混 淆。
現在,已經瞭解了 printf 方法的所有特性。圖 3-6 給出了格式說明符的語法圖。
註釋:許多格式化規則是本地環境特有的。例如,在德國,組分隔符是句號而不是逗號, Monday 被格式化為 Montag,, 在 卷 II 第 5 章中將介紹如何控制應用的國際化行為。
3.7.3 檔案輸入與輸出
要想對檔案進行讀取,就需要一個用 File 物件構造一個 Scanner 物件,如下所示:
Scanner in = new Scanner(Paths.get("niyflle.txt"), "UTF-8");
如果檔名中包含反斜槓符號,就要記住在每個反斜槓之前再加一個額外的反斜槓:“ c:\\mydirectory\\myfile.txt ” 。
註釋: 在這裡指定了 UTF-8 字元編碼, 這對於網際網路上的檔案很常見(不過並不是普遍適用)。讀取一個文字檔案時,要知道它的字元編碼---更多資訊參見卷 II 第 2 章。如果省略字元編碼, 則會使用執行這個 Java 程式的機器的“ 預設編碼”。 這不是一個好主意, 如果在不同的機器上執行這個程式, 可能會有不同的表現。
現在,就可以利用前面介紹的任何一個 Scanner 方法對檔案進行讀取。
要想寫入檔案, 就需要構造一個 PrintWriter 物件。在構造器中,只需要提供檔名:
PrintWriter out = new PrintWriter('myfile.txt", "UTF-8");
如果檔案不存在,建立該檔案。 可以像輸出到 System.out —樣使用 print、 println 以及 printf 命令。
警告: 可以構造一個帶有字串引數的 Scanner, 但 這 個 Scanner 將字串解釋為資料, 而不是檔名。例如, 如果呼叫:
Scanner in = new Scanner("myfile.txt"); // ERROR?
這個 scanner 會將引數作為包含 10 個字元的資料:‘ m ’,‘ y ’,‘ f’ 等。在這個示例中所顯示的並不是人們所期望的效果。
註釋: 當指定一個相對檔名時, 例如,“ myfile.txt”,“ mydirectory/myfile.txt” 或“ ../myfile.txt",檔案位於 Java 虛擬機器啟動路徑的相對位置 , 如果在命令列方式下用下列命令啟動程式:
java MyProg
啟動路徑就是命令直譯器的當前路徑。 然而,如果使用整合開發環境, 那麼啟動路徑將由 IDE 控制。 可以使用下面的呼叫方式找到路徑的位置:
String dir = System.getProperty("user.dir"):
如果覺得定位檔案比較煩惱, 則可以考慮使用絕對路徑, 例如:“ c:\\mydirectory\\ myfile.txt ” 或者“/home/me/mydirectory/myfile.txt” 。
正如讀者所看到的,訪問檔案與使用 System.in 和 System.out —樣容易。要記住一點:如果 用一個不存在的檔案構造一個 Scanner, 或者用一個不能被建立的檔名構造一個 PrintWriter, 那麼就會發生異常。Java 編譯器認為這些異常比“ 被零除” 異常更嚴重。在第 7 章中,將會 學習各種處理異常的方式。現在,應該告知編譯器: 已經知道有可能出現“ 輸入 / 輸出” 異 常。這需要在 main 方法中用 throws 子句標記,如下所示:
public static void main(String[] args) throws IOException { Scanner in = new Scanner(Paths.get("myfi1e.txt"), "UTF-8"); ... }
現在讀者已經學習瞭如何讀寫包含文字資料的檔案。對於更加高階的技術,例如,處理不同的字元編碼、 處理二進位制資料、 讀取目錄以及寫壓縮檔案,請參看卷 II 第 2 章。
註釋:當採用命令列方式啟動一個程式時, 可以利用 Shell 的重定向語法將任意檔案關聯 到 System.in 和 System.out:
java MyProg < myfile.txt > output.txt
這樣,就不必擔心處理 IOException 異常了。
API java.util.Scanner 5.0
•Scanner(File f) 構造一個從給定檔案讀取資料的 Scanner。
•Scanner(String data) 構造一個從給定字串讀取資料的 Scanner。
API java.io.PrintWriter 1.1
• PrintWriter(String fileName) 構造一個將資料寫入檔案的 PrintWriter。 檔名由引數指定。
API Java.nio.file.Paths 7
• static Path get(String pathname) 根據給定的路徑名構造一個 Path。
3.8 控制流程
與任何程式設計語言一樣, Java 使用條件語句和迴圈結構確定控制流程。本節先討論條件語句, 然後討論迴圈語句,最後介紹看似有些笨重的 switch 語句,當需要對某個表示式的多個值進行檢測時, 可以使用 switch 語句。
C++ 註釋:Java 的控制流程結構與 C 和 C++ 的控制流程結構一樣, 只有很少的例外情 況。沒有 goto 語句,但 break 語句可以帶標籤, 可以利用它實現從內層迴圈跳出的目的 (這種情況 C 語言採用 goto 語句實現。) 另外,還有一種變形的 for 迴圈, 在 C 或 C++ 中 沒有這類迴圈。它有點類似於 C# 中的 foreach 迴圈。
3.8.1 塊作用域
在深入學習控制結構之前, 需要了解塊(block) 的概念。
塊(即複合語句)是指由一對大括號括起來的若干條簡單的 Java 語句。塊確定了變數的作用域。一個塊可以巢狀在另一個塊中。下面就是在 main方法塊中巢狀另一個語句塊的示例。
public static void main(String口 args) { int n; ... { int k; ... } // k is only defined up to here }
但是,不能在巢狀的兩個塊中宣告同名的變數。例如,下面的程式碼就有錯誤,而無法通過編譯:
public static void main(String口 args) { int n; ... { int k;
int n ;//Error can't redefine n in inner block ... } }
C++ 註釋:在 C++ 中, 可以在巢狀的塊中重定義一個變數。在內層定義的變數會覆蓋在外層定義的變數。這樣,有可能會導致程式設計錯誤, 因此在 Java 中不允許這樣做。
3.8.2 條件語句
在 Java 中,條件語句的格式為:if (condition) statement 這裡的條件必須用括號括起來。
與絕大多數程式設計語言一樣, Java 常常希望在某個條件為真時執行多條語句。在這種情況下, 應該使用塊語句 (block statement), 形 式 為
{ statement1 statement2
... } 例如: if (yourSales >= target) { performance = "Satisfactory"; bonus = 100; }
當 yourSales 大於或等於 target 時, 將執行括號中的所有語句(請參看圖 3-7 ) 。
註釋: 使用塊 ( 有時稱為複合語句)可以在 Java 程式結構中原本只能放置一條 ( 簡單)語句的地方放置多條語句。
在 Java 中, 更一般的條件語句格式如下所示 (請參看圖 3-8 ):
if (condition) statement1 else statement2
例如: if (yourSales >= target) { performance = "Satisfactory"; bonus = 100 + 0.01 *(yourSales - target); } else { performance = "Unsatisfactory"; bonus = 0; }
其中 else 部分是可選的。else 子句與最鄰近的 if 構成一組。因此,在語句
if (x <= 0) if (x == 0) sign = 0; else sign = -1;
中 else 與第 2 個 if 配對。當然, 用一對括號將會使這段程式碼更加清晰:
if (x <= 0) { if (x == 0) sign = 0; else sign = -1; }
重複地交替出現 if...else if... 是一種很常見的情況(請參看圖 3-9 )。例如:
if (yourSales >= 2 * target) { performance = "Excellent"; bonus = 1000; } else if (yourSales >= 1.5 * target) { performance = "Fine"; bonus = 500; } else if (yourSales >= target) { performance = "Satisfactory"; bonus = 100; } else { System.out.println("You're fired"); }
3.8.3 迴圈
當條件為 true 時,while 迴圈執行一條語句(也可以是一個語句塊)。一般格式為while (condition ) statement
如果開始迴圈條件的值就為 false, 則 while 迴圈體一次也不執行 (請參看圖 3-10 )。
程式清單 3-3 中的程式將計算需要多長時間才能夠儲存一定數量的退休金,假定每年存 人相同數量的金額,而且利率是固定的。(此處略過)
while 迴圈語句首先檢測迴圈條件。因此, 迴圈體中的程式碼有可能不被執行。如果希望 迴圈體至少執行一次, 則應該將檢測條件放在最後。 使用 do/while 迴圈語句可以實現這種操作方式。它的語法格式為:
do statement while (condition);
這種迴圈語句先執行語句 (通常是一個語句塊,) 再檢測迴圈條件;然後重複語句,再檢測迴圈條件, 以此類推: 在程式清單 3-4中, 首先計算退休賬戶中的餘額,然後再詢問是否打算退休:
do { balance += payment; double interest = balance * interestRate / 100; balance += interest; year++; // print current balance ... // ask if ready to retire and get input ... } while (input. equals ("N"));
3.8.4 確定迴圈
for 迴圈語句是支援迭代的一種通用結構, 利用每次迭代之後更新的計數器或類似的變數 來控制迭代次數。 如圖 3-12 所示, 下面的程式將數字 1 ~ 10 輸出到螢幕上。
for (int i = 1; i <= 10; i++) System.out.println(i);
for 語句的第 1 部分通常用於對計數器初始化;第 2 部分給出每次新一輪迴圈執行前要檢 測的迴圈條件;第 3 部分指示如何更新計數器。
與 C++ —樣, 儘管 Java 允許在 for 迴圈的各個部分放置任何表示式,但有一條不成文的 規則:for 語句的 3 個部分應該對同一個計數器變數進行初始化、 檢測和更新。若不遵守這一 規則,編寫的迴圈常常晦澀難懂。
即使遵守了這條規則,也還有可能出現很多問題。例如,下面這個倒計數的迴圈:
for (int i = 10; i > 0; i ) System.out.println("Counting down . . . " + i); System.out.println("B1astoff!");
警告: 在迴圈中,檢測兩個浮點數是否相等需要格外小心。下面的 for 迴圈
for (double x = 0; x != 10; x += 0.1) . . .
可能永遠不會結束。 由於舍入的誤差, 最終可能得不到精確值。 例如, 在上面的 迴圈中, 因為 0.1 無法精確地用二進位制表示, 所以,x 將從 9.999 999 999 999 98 跳到10.099 999 999 999 98。
當在 for 語句的第 1 部分中宣告瞭一個變數之後,這個變數的作用域就為 for 迴圈的整個 迴圈體。
for (int i = 1; i <= 10; i++) { ... }
// i no longer defined here
特別指出,如果在 for 語句內部定義一個變數, 這個變數就不能在迴圈體之外使用。因 此, 如果希望在 for 迴圈體之外使用迴圈計數器的最終值,就要確保這個變數在迴圈語句的 前面且在外部宣告!
int i ; for (i = 1; i <= 10; i++) { ... } // i is still defined here //另一方面,可以在各自獨立的不同 for 迴圈中定義同名的變數: for (int i = 1; i <= 10; i++) { ... } for (int i = 11; i <= 20; i ++) // OK to define another variable named i { ... }
for 迴圈語句只不過是 while 迴圈的一種簡化形式例如,
for (int i = 10; i > 0; i-- ) System.out.println("Counting down . . . " + i); 可以重寫為: int i = 10; while (i > 0) { System.out.println("Counting down . . . " + i); i--; }
註釋:3.10.1 節將會介紹“ 通用 for 迴圈”( 又稱為 for each 迴圈 ), 這是 Java SE 5.0 新增 加的一種迴圈結構。
3.8.5 多重選擇:switch語句
在處理多個選項時, 使用 if/else 結構顯得有些笨拙。 Java 有一個與 C/C++ 完全一樣的 switch 語句。
例如, 如果建立一個如圖 3-13 所示的包含 4 個選項的選單系統, 可以使用下列程式碼:
Scanner in = new Scanner(System.in); System.out.print("Select an option (1, 2, 3, 4)"); int choice = in.nextlnt(); switch (choice) { case 1:
... break; case 2:
... break; case 3:
... break; case 4:
... break; default: // bad input
...
break; }
switch語句將從與選項值相匹配的 case 標籤處開始執行直到遇到 break 語句,或者執行到 switch 語句的結束處為止。如果沒有相匹配的 case 標籤, 而有 default 子句, 就執行這個子句。
警告: 有可能觸發多個 case 分支。 如果在 case 分支語句的末尾沒有 break 語句, 那麼就會接著執行下一個 case 分支語句。這種情況相當危險, 常常會引發錯誤。 為此,我們在 程式中從不使用 switch 語句。
如果你比我們更喜歡 switch 語句, 編譯程式碼時可以考慮加上 -Xlint:fallthrough 選項, 如下所示:
javac -Xlint:fallthrough Test.java
這樣一來, 如果某個分支最後缺少一個 break 語句, 編譯器就會給出一個警告訊息。
如果你確實正是想使用這種“ 直通式”(fallthrough) 行為, 可以為其外圍方法加一個 標註@SuppressWamings("fallthrough")。 這樣就不會對這個方法生成警告了 。 (標註是為編譯器或處理 Java 原始檔或類檔案的工具提供資訊的一種機制。我們將在卷 II 的 第 8 章 詳細討論標註。)
case 標籤可以是:
•型別為 char、byte、 short 或 int 的常量表示式。
•列舉常量。
•從 Java SE 7開始, case 標籤還可以是字串字面量。
例如:
String input = ...; switch(input.toLowerCase()) { case "yes";//ok since java SE 7 ... break;
... }
當在 switch 語句中使用列舉常量時,不必在每個標籤中指明列舉名,可以由 switch 的表示式值確定。例如:
Size sz = . . .; switch (sz) { case SMALL: // no need to use Size.SMALL ... break; ... }
3.8.6 中斷控制流程語句
儘管 Java 的設計者將 goto 作為保留字,但實際上並沒有打算在語言中使用它。通常, 使用 goto 語句被認為是一種拙劣的程式設計風格。 當然,也有一些程式設計師認為反對 goto 的 呼聲似乎有些過分(例如,Donald Knuth 就曾編著過一篇名為《 Structured Programming with goto statements》的著名文章。) 這篇文章說:無限制地使用 goto 語句確實是導致錯誤的根源, 但在有些情況下,偶爾使用 goto 跳出迴圈還是有益處的。Java 設計者同意這種看法,甚至在 Java語言中增加了一條帶標籤的 break, 以此來支援這種程式設計風格。
下面首先看一下不帶標籤的 break 語句。與用於退出 switch 語句的 break 語句一樣,它 也可以用於退出迴圈語句。 例如,
while (years <= 100) { balance += payment; double interest = balance * interestRate / 100; balance += interest; if (balance >= goal ) break; years++ ; }
在迴圈開始時, 如果 years > 100, 或者在迴圈體中 balance 多 goal , 則退出迴圈語句。 當然,也可以在不使用 break 的情況下計算 years 的值,如下所示:
while (years <= 100 && balance < goal ) { balance += payment ; double interest = balance * interestRate / 100; balance +- interest; if (balance < goal) years++; }
但是需要注意, 在這個版本中, 檢測了兩次 balance < goal。為了避免重複檢測,有些程式設計師更加偏愛使用 break語句。
與 C++ 不同,Java 還提供了一種帶標籤的 break語句,用於跳出多重巢狀的迴圈語句。 有時候,在巢狀很深的迴圈語句中會發生一些不可預料的事情。此時可能更加希望跳到巢狀 的所有迴圈語句之外。通過新增一些額外的條件判斷實現各層迴圈的檢測很不方便。
這裡有一個示例說明了 break 語句的工作狀態。請注意,標籤必須放在希望跳出的最外層迴圈之前, 並且必須緊跟一個冒號。
Scanner in = new Scanner(System.in); int n; read_data: while (. . .) // this loop statement is tagged with the label
{
... for (. . .) // this inner loop is not labeled { Systen.out.print("Enter a number >= 0: "); n = in.nextInt(); if (n < 0) // should never happen-can’t go on break read.data; // break out of readjata loop
...
}
} // this statement is executed immediately after the labeled break if (n < 0) // check for bad situation { // deal with bad situation } else { // carry out normal processing }
如果輸入有誤,通過執行帶標籤的 break 跳轉到帶標籤的語句塊末尾。對於任何使用 break語句的程式碼都需要檢測迴圈是正常結束, 還是由 break 跳出。
註釋: 事實上,可以將標籤應用到任何語句中, 甚至可以應用到 if語句或者塊語句中, 如下所示:
label: { if (condition) break label; // exits block
...
} // jumps here when the break statement executes
因此,如果希望使用一條 goto 語句, 並將一個標籤放在想要跳到的語句塊之前, 就 可以使用 break 語句! 當然,並不提倡使用這種方式。另外需要注意, 只能跳出語句塊, 而不能跳入語句塊。
最後,還有一個 continue 語句。與 break 語句一樣, 它將中斷正常的控制流程。continue 語句將控制轉移到最內層迴圈的首部。例如:
Scanner in = new Scanner(System.in); while (sum < goal ) { System.out.print("Enter a number: "); n = in.nextlntO; if (n < 0) continue; sum += n; // not executed if n < 0 }
如果 n<0, 則 continue語句越過了當前迴圈體的剩餘部分, 立刻跳到迴圈首部。
如果將 continue 語句用於 for 迴圈中, 就可以跳到 for 迴圈的“ 更新” 部分。例如, 下面 這個迴圈:
for (count = 1; count <= 100; count++) { System.out.print("Enter a number, -1 to quit: "); n = in.nextlntO; if (n < 0) continue; sum += n; // not executed if n < 0 }
如果 n<0, 則 continue 語句跳到 count++ 語句。還有一種帶標籤的 continue 語句,將跳到與標籤匹配的迴圈首部。
3.9 大數值
如果基本的整數和浮點數精度不能夠滿足需求, 那麼可以使用java.math 包中的兩個 很有用的類:Biglnteger 和 BigDecimal 這兩個類可以處理包含任意長度數字序列的數值。 Biglnteger 類實現了任意精度的整數運算, BigDecimal 實現了任意精度的浮點數運算。
使用靜態的 valueOf方法可以將普通的數值轉換為大數值:
Biglnteger a = Biglnteger.valueOf(100);
遺憾的是,不能使用人們熟悉的算術運算子(如:+ 和 *) 處理大數值。 而需要使用大數值類中的 add 和 multiply 方法。
Biglnteger c = a.add(b); // c = a + b Biglnteger d = c.multiply(b.add(Biglnteger.valueOf(2))); // d = c * (b + 2)
C++ 註釋: 與 C++ 不同, Java 沒有提供運算子過載功能。 程式設計師無法重定義 + 和 * 運算 符, 使其應用於 BigInteger 類的 add 和 multiply 運算。Java 語言的設計者確實為字串 的連線過載了 + 運算子,但沒有過載其他的運算子,也沒有給 Java 程式設計師在自己的類中過載運算子的機會 ,
lotteryOdds = lotteryOdds *(n - i + 1) / i; 如果使用大數值, 則相應的語句為: lotteryOdds = lotteryOdds.multiply(BigInteger.valueOf(n - i + 1)).divide(Biglnteger.valueOf(i));
API java.math.Biglnteger 1.1
• Biglnteger add(Biglnteger other)
• Biglnteger subtract(Biglnteger other)
• Biglnteger multiply(Biginteger other)
• Biglnteger divide(Biglnteger other)
• Biglnteger mod(Biglnteger other)
返冋這個大整數和另一個大整數 other的和、 差、 積、 商以及餘數。
• int compareTo(Biglnteger other)
如果這個大整數與另一個大整數 other 相等, 返回 0; 如果這個大整數小於另一個大整 數 other, 返回負數; 否則, 返回正數。
• static Biglnteger valueOf(long x)
返回值等於 x 的大整數。
API java.math.Biglnteger 1.1
• BigDecimal add(BigDecimal other)
• BigDecimal subtract(BigDecimal other)
• BigDecimal multipiy(BigDecimal other)
• BigDecimal divide(BigDecimal other RoundingMode mode) 5.0
返回這個大實數與另一個大實數 other 的和、 差、 積、 商。要想計算商, 必須給出舍 入方式 ( rounding mode。) RoundingMode.HALF UP 是在學校中學習的四捨五入方式 ( BP , 數值 0 到 4 捨去, 數值 5 到 9 進位)。它適用於常規的計算。有關其他的舍入方 式請參看 Apr文件。
• int compareTo(BigDecimal other) 如果這個大實數與另一個大實數相等, 返回 0 ; 如果這個大實數小於另一個大實數, 返回負數;否則,返回正數。
• static BigDecimal valueOf(1 ong x)
• static BigDecimal valueOf(1 ong x ,int scale)
返回值為 X 或 x / 10的scale方 的一個大實數。
3.10 陣列
陣列是一種資料結構, 用來儲存同一型別值的集合。通過一個整型下標可以訪問陣列中的每一個值。例如, 如果 a 是一個整型陣列, a[i] 就是陣列中下標為 i 的整數。
在宣告陣列變數時, 需要指出陣列型別 ( 資料元素型別緊跟 []) 和陣列變數的名字。下 面宣告瞭整型陣列 a:int[] a;
不過, 這條語句只宣告瞭變數 a, 並沒有將 a 初始化為一個真正的陣列。應該使用 new 運算 符建立陣列。
int[] a = new int[100];
這條語句建立了一個可以儲存 100 個整數的陣列。陣列長度不要求是常量: newint[n] 會建立 一個長度為 n 的陣列。
註釋:可以使用下面兩種形式宣告陣列
int[] a; 或 int a[];
大多數 Java 應用程式設計師喜歡使用第一種風格, 因為它將型別 int[] ( 整型陣列)與變數名分開了。
這個陣列的下標從 0 ~ 99 (不是 1 ~ 100 )。一旦建立了陣列,就可以給陣列元素賦值。 例如,使用一個迴圈:
intp a = new int[100]; for (int i = 0; i < 100; i++) a[i] = i ; // fills the array with numbers 0 to 99
建立一個數字陣列時, 所有元素都初始化為 0。boolean 陣列的元素會初始化為 false。 物件陣列的元素則初始化為一個特殊值 null, 這表示這些元素(還)未存放任何物件。初學者 對此可能有些不解。例如,
String[] names = new String[10];
會建立一個包含 10 個字串的陣列, 所有字串都為 null。 如果希望這個陣列包含空 串, 可以為元素指定空串:
for (int i = 0; i < 10; i++) names[i]="";
警告: 如果建立了一個 100 個元素的陣列, 並且試圖訪問元素 a[100] (或任何在 0 ~ 99 之外的下標,) 程式就會引發“ array index out of bounds ” 異常而終止執行。
要想獲得陣列中的元素個數,可以使用 array.length。例如,
for (int i = 0; i < a.length; i ++) System.out.println(a[i]);
一旦建立了陣列, 就不能再改變它的大小(儘管可以改變每一個陣列元素)。如果經常需要在執行過程中擴充套件陣列的大小, 就應該使用另一種資料結構—陣列列表( array list ) 有關陣列列表的詳細內容請參看第 5 章。
3.10.1 for each迴圈
Java 有一種功能很強的迴圈結構, 可以用來依次處理陣列中的每個元素(其他型別的元素集合亦可)而不必為指定下標值而分心。 這種增強的 for 迴圈的語句格式為: for (variable : collection) statement
定義一個變數用於暫存集合中的每一個元素, 並執行相應的語句(當然,也可以是語句塊)。 collection 這一集合表示式必須是一個陣列或者是一個實現了 Iterable 介面的類物件(例如 ArrayList)。有關陣列列表的內容將在第 5 章中討論, 有關 Iterable 介面的內容將在第 9 章中 討論。
例如,
for (int element : a) System.out.println(element):
列印陣列 a 的每一個元素,一個元素佔一行。
這個迴圈應該讀作“ 迴圈 a 中的每一個元素”(for each element in a )。Java 語言的設計者認為應該使用諸如 foreach、 in 這樣的關鍵字,但這種迴圈語句並不是最初就包含在 Java 語 言中的,而是後來新增進去的, 並且沒有人打算廢除已經包含同名(例如 System.ii ) 方法或 變數的舊程式碼。
當然,使用傳統的 for 迴圈也可以獲得同樣的效果:
for (int i = 0; i < a.length; i++) System,out.println(a[i]);
但是,for each 迴圈語句顯得更加簡潔、 更不易出錯(不必為下標的起始值和終止值而操心。
註釋:foreach 迴圈語句的迴圈變數將會遍歷陣列中的每個元素, 而不需要使用下標值。 如果需要處理一個集合中的所有元素, for each 迴圈語句對傳統迴圈語句所進行的改進 更是叫人稱讚不已。然而, 在很多場合下, 還是需要使用傳統的 for 迴圈。例如,如果不希望遍歷集合中的每個元素, 或者在迴圈內部需要使用下標值等。
提示:有個更加簡單的方式列印陣列中的所有值, 即利用 Arrays 類的 toString 方法。 調 用 Arrays.toString(a), 返回一個包含陣列元素的字串,這些元素被放置在括號內, 並 用逗號分隔, 例如,“ [2,3,5,7,11,13] ” 、 要想列印陣列,可以呼叫
System.out.println(Arrays.toString(a));
3.10.2 陣列初始化以及匿名陣列
在 Java中, 提供了一種建立陣列物件並同時賦予初始值的簡化書寫形式。下面是一 例子:
int[] smallPrimes = { 2, 3, 5, 7, 11, 13 };
請注意, 在使用這種語句時,不需要呼叫 new。
甚至還可以初始化一個匿名的陣列:
new int[] { 17, 19, 23, 29, 31, 37 }
這種表示法將建立一個新陣列並利用括號中提供的值進行初始化,陣列的大小就是初始值的 個數。 使用這種語法形式可以在不建立新變數的情況下重新初始化一個陣列。例如:
smallPrimes = new int[] { 17, 19, 23, 29, 31, 37 };
這是下列語句的簡寫形式:
int[] anonymous = { 17, 19, 23, 29, 31, 37 };
smallPrimes = anonymous;
註釋: 在 Java 中, 允許陣列長度為 0。在編寫一個結果為陣列的方法時, 如果碰巧結果為空, 則這種語法形式就顯得非常有用。此時可以建立一個長度為 0 的陣列:
new elementType[0]
注意, 陣列長度為 0 與 null 不同。
3.10.3 陣列拷貝
在 Java 中,允許將一個陣列變數拷貝給另一個陣列變數。這時, 兩個變數將引用同 一個陣列:
int[] luckyNumbers = smallPrimes; 1uckyNumbers[S] = 12; // now smallPrimes[S] is also 12
如果希望將 一個陣列的所有值拷貝到一個新的陣列中去, 就要使用 Arrays 類的 copyOf方法:
int[] copiedLuckyNumbers = Arrays.copyOf(luckyNumbers , luckyNumbers.length);
第 2 個引數是新陣列的長度。這個方法通常用來增加陣列的大小:
luckyNumbers = Arrays.copyOf(luckyNumbers , 2 * luckyNumbers.length);
如果陣列元素是數值型,那麼多餘的元素將被賦值為 0 ; 如果陣列元素是布林型,則將賦值 為 false。相反,如果長度小於原始陣列的長度,則只拷貝最前面的資料元素。
C++ 註釋:Java 陣列與 C++ 陣列在堆疊上有很大不同, 但基本上與分配在堆(heap) 上 的陣列指標一樣。也就是說,
int[] a = new int[100]; // Java 不同於 int a[100]; // C++ 而等同於 int* a = new int[100]; // C++
Java 中的 [ ] 運算子被預定義為檢查陣列邊界,而且沒有指標運算, 即不能通過 a 加 1 得到陣列的下一個元素。
3.10.4 命令列引數
前面已經看到多個使用 Java 陣列的示例。 每一個 Java 應用程式都有一個帶 String arg[]引數的 main 方法。這個參數列明 main 方法將接收一個字串陣列, 也就是命令列引數。
例如, 看一看下面這個程式:
public class Message { public static void main(String[] args) { if (args.length = 0 11 args[0].equals("_h")) System.out.print("Hello,"); else if (args[0].equa1s("-g")) System.out.print("Goodbye ,"); // print the other command-line arguments for (int i = 1; i < args.length; i ++) System.out.print(" " + args[i]); System•out.println("!"); } }
如果使用下面這種形式執行這個程式:
java Message -g cruel world
args 陣列將包含下列內容:
args[0]:"-g" args[1]:"cruel" args[2]:"world"
這個程式將顯示下列資訊:
Goodbye, cruel world!
C++ 註釋: 在 Java 應用程式的 main 方法中, 程式名並沒有儲存在 args 陣列中。 例如, 當使用下列命令執行程式時
java Message -h world
args[0] 是“ -h”, 而不是“ Message” 或“ java”。
3.10.5 陣列排序
要想對數值型陣列進行排序, 可以使用 Arrays 類中的 sort 方法:
int[] a = new int[10000]; ... Arrays.sort(a)
這個方法使用了優化的快速排序演算法。快速排序演算法對於大多數資料集合來說都是效率比較高的。Arrays 類還提供了幾個使用很便捷的方法, 在稍後的 API 註釋中將介紹它們。
程式清單 3-7中的程式用到了陣列,它產生一個抽彩遊戲中的隨機數值組合。 假如抽彩 是從 49 個數值中抽取 6 個,那麼程式可能的輸出結果為:
Bet the following combination. It'll make you rich! 4 7 8 19 30 44
要想選擇這樣一個隨機的數值集合,就要首先將數值 1, 2, …,n 存入陣列 numbers 中:
int[] numbers = new int[n]; for(int i = 0;i < numbers.length;i++) numbers[i] = i+ 1;
而用第二個陣列存放抽取出來的數值:
int[] result = new int[k];
現在,就可以開始抽取 k 個數值了。Math.random 方法將返回一個 0 到 1 之間(包含 0、 不包含 1 ) 的隨機浮點數。用 乘以這個浮點數, 就可以得到從 0 到 n-l 之間的一個隨機數。
int r = (int) (Math.random() * n);
下面將 result 的第 i 個元素設定為 munbers[r] 存放的數值, 最初是 r+1。但正如所看到 的,numbers 陣列的內容在每一次抽取之後都會發生變化。
result[i] = numbers[r];
現在,必須確保不會再次抽取到那個數值,因為所有抽彩的數值必須不相同。因此,這裡用陣列中的最後一個數值改寫 mimber[r],並將 n 減 1。
numbers[r] = numbers[n - 1];
n--;
關鍵在於每次抽取的都是下標, 而不是實際的值。下標指向包含尚未抽取過的陣列元素。 在抽取了 k 個數值之後, 就可以對 result 陣列進行排序了, 這樣可以讓輸出效果更加清晰:
Arrays.sort(result); for (int r : result) System.out.println(r);
API Java.util.Arrays 1.2
•static String toString(type[] a) 5 . 0
返回包含 a 中資料元素的字串, 這些資料元素被放在括號內, 並用逗號分隔。 引數: a 型別為 int、long、short、 char、 byte、boolean、float 或 double 的陣列。
• static type copyOf(type[]a, int length)6
• static type copyOfRange(type[]a , int start, int end)6
返回與 a 型別相同的一個陣列, 其長度為 length 或者 end-start, 陣列元素為 a 的值。
引數:a 型別為 int、 long、 short、 char、 byte、boolean、 float 或 double 的陣列。
start 起始下標(包含這個值)0
end 終止下標(不包含這個值)。這個值可能大於 a.length。 在這種情況 下,結果為 0 或 false。
length 拷貝的資料元素長度。 如果 length 值大於 a.length, 結果為 0 或 false ; 否則, 陣列中只有前面 length 個資料元素的拷貝 值。 參 • static void sort(type [ ] a)
採用優化的快速排序演算法對陣列進行排序。 引數:a 型別為 int、long、short、char、byte、boolean、float 或 double 的陣列。
•static int binarySearch(type[]a ,type v)
• static int binarySearch(type[]a, int start, int end, type v) 6
採用二分搜尋演算法查詢值 v。如果查詢成功, 則返回相應的下標值; 否則, 返回一個 負數值 r。 -r-1 是為保持 a 有序 v 應插入的位置。
引數:a 型別為 int、 long、 short、 char、 byte、 boolean 、 float 或 double 的有 序陣列。
start 起始下標(包含這個值)。
end 終止下標(不包含這個值)。
v 同 a 的資料元素型別相同的值。
• static void fi11(type[]a , type v) 將陣列的所有資料元素值設定為 V。
引數:a 型別為 int、 long、short、 char、byte、boolean、float 或 double 的陣列。
v 與 a 資料元素型別相同的一個值。
• static boolean equals(type[]a, type[]b)
如果兩個陣列大小相同, 並且下標相同的元素都對應相等, 返回 true。
引數:a、 b 型別為 int、long、short、char、byte、boolean、float 或 double 的兩個陣列。
3.10.6 多維陣列
多維陣列將使用多個下標訪問陣列元素, 它適用於表示表格或更加複雜的排列形式。這 一節的內容可以先跳過, 等到需要使用這種儲存機制時再返四來學習。
假設需要建立一個數值表, 用來顯示在不同利率下投資 $10,000 會增長多少,利息每年 兌現, 而且又被用於投資(見表 3-8) 。
可以使用一個二維陣列(也稱為矩陣) 儲存這些資訊。這個陣列被命名為 balances。
在 Java 中, 宣告一個二維陣列相當簡單。例如:double[][] balances;
與一維陣列一樣, 在呼叫 new 對多維陣列進行初始化之前不能使用它。 在這裡可以這樣 初始化:
balances = new double[NYEARS] [NRATES]:
另外, 如果知道陣列元素, 就可以不呼叫 new, 而直接使用簡化的書寫形式對多維陣列 進行初始化。例如:
int[][] magicSquare = { {16, 3, 2, 13},
{5, 10, 11, 8}, (9, 6, 7, 12}, {4, 15, 14, 1} };
一旦陣列被初始化, 就可以利用兩個方括號訪問每個元素, 例如, balances[i][j]。
在示例程式中用到了一個儲存利率的一維陣列 interest 與一個儲存餘額的二維陣列 balances。一維用於表示年, 另一維用於表示利率, 最初使用初始餘額來初始化這個陣列的 第一行:
for (int j = 0; j < balances[0].length; j++) balances [0][j] = 10000;
然後, 按照下列方式計算其他行:
for (int i = 1; i < balances.length; i++) { for (int j = 0; j < balances[i].length; j++) { double oldBalance = balances[i- 1][j]: double interest = . . .; balances[i][j] = oldBalance + interest; } }
程式清單 3-8 給出了完整的程式。
註釋: for each 迴圈語句不能自動處理二維陣列的每一個元素。它是按照行, 也就是一維陣列處理的要想訪問二維教組 a 的所有元素, 需要使用兩個巢狀的迴圈, 如下所示:
for (double[] row : a) for (double value : row) do something with value
提示: 要想快速地列印一個二維陣列的資料元素列表, 可以呼叫:
System.out.println(Arrays.deepToString(a));
輸出格式為:
[[16, B, 2, 13], [5, 10, 11, 8], [9, 6, 7, 12], [4, 15, 14, 1]]
3.10.7 不規則陣列
到目前為止,讀者所看到的陣列與其他程式設計語言中提供的陣列沒有多大區別。但實際存在著一些細微的差異, 而這正是 Java 的優勢所在:Java 實際上沒有多維陣列,只有一維陣列。多維陣列被解釋為“ 陣列的陣列。”
例如, 在前面的示例中, balances 陣列實際上是一個包含 10 個元素的陣列,而每個元素 又是一個由 6 個浮點陣列成的陣列(請參看圖 3-15 )。
表示式 balances[i]引用第 i 個子陣列, 也就是二維表的第 i 行。它本身也是一個陣列, balances[i][j] 引用這個陣列的第 j 項。
由於可以單獨地存取陣列的某一行, 所以可以讓兩行交換。
double[] temp = balances[i]: balances[i] = balances[i + 1]; balances[i + 1] = temp;
還可以方便地構造一個“ 不規則” 陣列, 即陣列的每一行有不同的長度。下面是一個典型的示例。在這個示例中,建立一個陣列, 第 i 行第 j 列將存放“ 從 i 個數值中抽取 j 個數值” 產生的結果,
1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 1 0 1 0 5 1 1 6 1 5 2 0 1 5 6 1
由於 j 不可能大於 i, 所以矩陣是三角形的。第 i 行有 i + 1 個元素(允許抽取 0 個元素, 也是一種選擇) 。要想建立一個不規則的陣列, 首先需要分配一個具有所含行數的陣列。
int[][] odds = new int[NMAX + 1] [] ;
接下來, 分配這些行。
for (int n = 0; n <= NMAX ; n++) odds [n] = new int[n + 1];
在分配了陣列之後, 假定沒有超出邊界, 就可以採用通常的方式訪問其中的元素了。
for (int n = 0; n < odds.length; n++) for (int k = 0; k < odds [n] .length; k++) { // compute lotteryOdds
...
odds [n] [k] = lotteryOdds; }
現在, 已經看到了 Java 語言的基本程式結構, 下一章將介紹 Java 中的物件導向的程式設計。