第 7 章 陣列 (Array)
本章提要 7-1 甚麼是陣列? 7-2 陣列的配置與初值設定 7-3 多維陣列 (Multi-Dimensional Array) 7-4 參照型別 (Reference Data Type) 7-5 命令列參數:argv 陣列 7-6 綜合演練
前言 假設您要撰寫一個程式, 計算 5 個學生的國文成績平均值, 那麼這個程式可能會是這樣:
前言 程式雖然很簡單, 但是卻有幾個問題存在:
前言 因為有 5 個學生, 所以在第 3 、4 行宣告了 5 個變數, 分別用來記錄個別學生的成績。如果學生人數變動的話, 所需要宣告的變數數量就會變多, 比如說 100 個學生, 那麼這 2 行就得宣告 100 個變數。不但程式寫起來很長, 光是要寫上 100 個變數的名稱就很容易寫錯。
前言 相同的問題也會出現在第 5 、6 行加總學生成績的地方。 最後, 在第 7 行計算平均成績的時候, 也會因為學生人數的變動, 而必須自行修改除數。
前言 以上這些問題都會影響撰寫程式時的效率, 也可能會因為疏忽, 造成程式的錯誤。舉例來說, 很可能在 3 、4 行的地方宣告了正確的變數數目與名稱, 但是在 5 、6 行加總時漏了某個變數;或者是在第 7 行計算平均時除錯了人數。
前言 如果能夠有一種比較好的資料紀錄與處理方式, 幫助我們解決以上的問題, 就可以避免許多不必要的錯誤發生了。 在這一章中, 就要介紹可以解決上述問題的新資料型別 -- 陣列。
7-1 甚麼是陣列? 要瞭解甚麼是陣列, 可以先回頭看看保管箱的概念。 在之前的章節中, 變數的使用都是只索取單一個保管箱來存放一項資料, 而這也正是前面提到計算平均成績時招致各種問題的根本原因。 我們所需要的是一種可以儲存多項資料的變數, 而且可以隨時知道所儲存資料的數量, 同時還能依據數量, 一一取出各項資料進行處理。
甚麼是陣列? 陣列就是上述問題的解決方案。 它相當於一次索取多個相同大小的保管箱, 就好像是一個由多個保管箱所組成的組合櫃一樣。 實際使用時, 可以將組合櫃中個別的保管箱當成獨立的變數, 我們稱個別的保管箱為該陣列的一個元素 (Element)。
甚麼是陣列?
甚麼是陣列? 更棒的是, 我們隨時可以知道這個組合櫃中包含有幾個保管箱, 而且每個獨立的保管箱都會依照順序編上序號, 只要出示某個保管箱序號的號碼牌, 就可以單獨使用指定的保管箱, 這個序號稱為是該保管箱的索引碼 (Index Number)。 有了這樣的組合櫃之後, 原本計算平均成績的程式, 就可以用以下的演算法來撰寫了:
甚麼是陣列?
陣列的宣告與配置 要使用陣列, 必須分成 2 個步驟, 第 1 個步驟是宣告陣列變數, 第 2 個步驟是配置陣列。宣告陣列變數的目的就是索取一個保管箱, 用來存放指向組合櫃的號碼牌, 而配置陣列的作用則是實際索取組合櫃, 並且把組合櫃的號碼牌放入陣列變數中。我們來看看實際的程式:
陣列的宣告與配置
陣列的宣告與配置
陣列變數的宣告 在 ArrayAverage.java 的第 3 行就宣告了一個陣列變數: 只要在型別名稱的後面加上一對中括號 ([ ]), 就表示要宣告一個指向可以放置該型別資料陣列的變數, 也就是說, students 這個變數會指到一個儲存 double 資料的陣列。
陣列變數的宣告 到這裡為止, 只宣告了陣列變數本身, 並沒有實際配置儲存資料的空間, 也沒有說明元素的個數。如果還是以保管箱來比擬的話, 等於只索取了用來放置組合櫃號碼牌的保管箱, 還沒有取得組合櫃。因此, 也還不能放入資料。
陣列變數的宣告 另外, 從陣列的宣告方式也可以看出來, 我們已經指定了資料型別, 所以, 接下來索取到的組合櫃中, 包含的都是一樣大小的保管箱, 只能放置相同型別的資料。 也就是說, 您不能在組合櫃的某個保管箱中放 int 型別的資料, 而在另一個保管箱中放 double 型別的資料。
另一種陣列變數的宣告方式 宣告陣列的時候, 您也可以將放在型別後面的那對中括號放在變數的後面, 變成這樣: 不過並不建議這樣做, 因為寫成 double[ ] 時, 可以直接讀成 double 陣列, 清楚明白的表示出該變數會指向一個 double 型別的陣列, 同時也可以將 double [ ] 當成是一種資料型別。
陣列的配置 宣告了陣列變數之後, 接下來就可以配置陣列了。在 ArrayAverage.java 的第 4 行中, 就是配置陣列的動作: 要配置陣列, 必須使用 new 運算子。new 運算子的運算元就表示了所需要的空間大小。
陣列的配置 以本例來說, 中括號裡頭的數字 5 就表示需要 5 個保管箱, 而中括號前面的 double 就表示了這 5 個保管箱都要用來放置 double 型別的資料。 換言之, 執行過這一行後, 就會配置一個組合櫃給 students, 其中擁有 5 個可以放置 double 型別資料的保管箱。
陣列的配置
陣列的配置 配置好空間後, 就可以將資料放入陣列中。 要特別注意的是配置空間時並不一定要使用字面常數來指定陣列的元素數量, 如果在撰寫程式的當下還無法決定陣列的大小, 那麼也可以在程式實際執行的時候, 以變數, 甚至於是運算式來指定要配置的元素個數。
陣列的使用 配置好空間之後, 程式在 5 ~ 9 行就將各個學生的成績放入個別的元素中。 指派的方式是在陣列變數的後面加上一對中括號, 並且在中括號內以數字來標示索引碼, 指定要放入哪一個元素中。 要特別注意的是, 第 1 個保管箱的索引碼是 0 、第 2 個保管箱是 1 、...、第 5 個保管箱是 4。
陣列的使用 這樣一來,就記錄好各個學生的成績了。
陣列的使用
陣列的使用 有了上面的內容之後, 就可以用非常簡單的方式來計算總和。陣列本身除了可以放置資料外, 還提供有許多項相關於該陣列的資訊, 稱為屬性 (Attributes)。 其中有一項屬性, 名稱為 length, 可以告訴我們該陣列中元素的數量。要取得這個屬性的內容, 只要在陣列變數的名稱之後, 加上 .length 即可。有了這項資訊, 再搭配迴圈, 就可以依序取出陣列中的資料, 進行加總了。
陣列的使用 這也就是 10 ~ 13 行所進行的工作: 其中, 第 11 行的 students.length 就是取得 students 所指陣列的元素個數。在第 12 行中, 就將每次迴圈所取出的元素加入 sum 變數中, 以計算加總值。
陣列的使用 最後, 再利用陣列的 length 屬性值, 去除剛剛得到的加總值, 以算出平均值: 這樣, 就使用了陣列改寫了原本使用多個變數撰寫的成績平均計算程式。
超過陣列個數的存取 在使用陣列時, 必須小心不要使用超過陣列最後一個元素的索引碼, 舉例來說, 以下這個程式就會發生錯誤:
超過陣列個數的存取
超過陣列個數的存取 由於陣列元素的索引碼由 0 起算, 因此在第 9 行想要設定 a[4] 的內容時就會出錯。 相同的道理, 第 12 行中, 迴圈結束的條件是 i <= a.length, 所以當 i 為 4 的時候, 迴圈仍然會執行, 但是 a 所指的陣列只有 4 個元素, 最後一個元素的索引碼為 3, 要存取 a [4] 的內容, 自然會出錯:
超過陣列個數的存取 對於還沒有習慣陣列的第 1 個元素索引碼為 0 的讀者來說, 這是很容易犯的錯, 請特別留意。
使用陣列的好處 瞭解了陣列的使用方法後, 就可以回過頭來看看, 使用陣列到底有甚麼好處? 我們先比較看看 ArrayAverage.java 與 Average.java 這 2 個程式, 從中發現 ArrayAverage.java 因為使用陣列而具有的優點。
使用陣列的好處 只需宣告一個陣列變數, 而不需要宣告和學生人數相同數量的變數。 當然, 在配置陣列空間的時候, 還是得依據學生人數指定大小, 但至少程式的行數仍然維持不變。 更重要的是, 假設學生的成績是從檔案中讀取, 學生人數在讀取資料後才能確定的話, 不使用陣列的方式根本就無法處理, 因為不但不知道該宣告多少個變數, 而且也無法透過索引碼的方式使用這些變數。
使用陣列的好處 指定陣列元素的內容時雖然看起來和使用變數時一樣的累贅, 不過如果資料是從檔案中循序讀入的話, 就可以使用索引碼依序放入陣列的元素中。 此時, 若是使用多個變數的方式, 就沒有辦法做到了。
使用陣列的好處 可以使用索引碼存取陣列元素, 這使得在進行陣列元素加總之類的循序處理時非常方便, 只要透過同樣的程式, 不論陣列多大, 都一樣適用, 而不需要去修改程式。
7-2 陣列的配置與初值設定 宣告同時配置 設定陣列的初值 配合陣列使用迴圈
宣告同時配置 雖然陣列在實際運作時會區分為宣告變數以及配置陣列兩段工作, 不過在撰寫程式時卻可以簡化, 將宣告以及配置的動作合在同一個指定運算式中。寫法很簡單, 就是將原本宣告的敘述和配置空間的 new 運算透過指定運算子串起來, 像是剛剛的 ArrayAverage.java 就可以改寫成這樣:
宣告同時配置
宣告同時配置 程式的執行完全和之前的程式一樣。 前面曾經提過, 配置陣列時, 並不是只能用字面常數來指定元素的個數, 也可以使用變數或是運算式, 例如:
宣告同時配置 要注意的是, 只有運算結果為整數值的運算式才能用來指定陣列的元素個數。
設定陣列的初值 如果您的陣列是在撰寫程式的當時就已經知道個別元素的初值, 那麼您還可以再把計算成績平均的程式簡化, 在宣告的同時, 直接列出個別元素的初值, 替代配置元素以及設定個別元素的動作。例如:
設定陣列的初值
設定陣列的初值 要直接設定陣列的初值, 必須使用一對大括號, 列出陣列中每一個元素的初值。 記得要在右大括號後面加上分號 (;), 表示整個宣告敘述的結束。
設定陣列的初值 實際上這段程式在執行的時候, 和原來的程式是一模一樣, 只是您在撰寫程式時可以比較便利而已。由於這樣的特性, 因此在宣告時直接設定陣列內容時, 也可以使用運算式, 而不單單僅能使用字面常數。舉例來說:
設定陣列的初值
配合陣列使用迴圈 從本章一開始的範例到目前為止, 我們經常會使用迴圈來依序處理陣列中的個別元素。 舉例來說, 在 ArrayAverage.java 中計算平均成績時, 就使用了迴圈依序取出個別學生的成績, 以便進行加總;而剛剛所介紹的 ArrayInitWithExpr.java 程式中, 也使用了迴圈依序顯示陣列中個別元素的內容。
配合陣列使用迴圈 要進行這些工作, 就必須自行撰寫迴圈, 並且宣告迴圈變數, 然後透過迴圈變數做為索引碼, 取出指定元素的值。 為了避免這樣的麻煩, Java 提供了另外一種 for 迴圈的語法, 方便陣列的使用。請看以下這個範例:
配合陣列使用迴圈
配合陣列使用迴圈 for(:) 的作用, 就是迴圈進行時, 每一輪就從 ":" 後面的陣列取出下一個元素, 放到冒號前面所指定的變數中, 然後執行迴圈本體的敘述。 “:” 前面的變數必須和陣列的型別一致, 否則編譯時就會發生錯誤。 如果拆解開來, 這個程式的第 5 ~ 7 行就相當於以下這樣:
配合陣列使用迴圈 for(:) 這種迴圈稱為 foreach 迴圈, 可以幫助我們撰寫出簡潔的迴圈敘述, 在第17 章介紹集合物件的時候, 還會看到它的用處。
foreach 迴圈的注意事項 實際上編譯器會把 foreach 迴圈拆解為一般的 for 迴圈, 就像剛剛所展示的一樣。因此, 使用 foreach 迴圈除了在撰寫程式時有所差異以外, 就和一般的 for 迴圈一模一樣, 執行時的速度也不會有差別。 您必須使用 J2SE 5.0 (含) 以上的 Java 編譯器才能編譯 foreach 迴圈, 否則編譯時會有錯誤。
7-3 多維陣列 (Multi-Dimensional Array) 由於陣列的每一個元素都可以看做是單獨的變數, 如果每一個元素自己本身也指向一個陣列, 就會形成一個指向陣列的陣列。 這種陣列, 稱為 2 維陣列 (2 - Dimensional Array), 而前面所介紹儲存一般資料的陣列, 就稱為1 維陣列 (1 - Dimensional Array )。
多維陣列 (Multi-Dimensional Array) 依此類推, 您也可以建立一個指向 2 維陣列的陣列, 其中每一個元素都指向一個 2 維陣列, 這時就稱這個陣列為 3 維陣列。 相同的道理, 您還可以建立 4 維陣列、5 維陣列、....等。 這些大於 1維的陣列, 統稱為多維陣列, 每多一層陣列, 就稱是多一個維度 (Dimension)。
多維陣列的宣告 要宣告多維陣列, 其實和宣告 1 維陣列沒有太大的差別, 只要將 1 維陣列的觀念延伸即可。 舉例來說, 要宣告一個個別元素都是指向 int 陣列的陣列時, 由於陣列中每一個元素都指向一個 int 陣列, 因此可以這樣宣告:
多維陣列的宣告 如果依循前面對於 1 維陣列宣告時的瞭解, 可以把 int[] 當成是一種新的資料型別, 表示一個用來儲存 int 型別資料的陣列。 這樣一來, 上述的宣告就可以解讀為
多維陣列的宣告 表示 a 是一個陣列, 它的每一個元素都指向一個儲存 int 型別資料的陣列。如果套用之前所提的保管箱概念, 這行宣告就等於是說 a 保管箱中放置了一個組合櫃的號碼牌, 而且組合櫃中每一個保管箱本身又放置了另一個組合櫃的號碼牌, 也可以說像是一面組合櫃了。
多維陣列的宣告 相同的道理,如果您要宣告一個3 維陣列,可以這樣宣告: 我們可以解讀成 b 是一個陣列, 它的每一個元素都指向 int[ ][ ] 型別的資料, 也就是一個 2 維的 int 陣列。
多維陣列的宣告 如果套用組合櫃的觀念, 這等於是一個立方體的組合櫃了。 當然啦, 在現實世界中, 不會有人這樣設計組合櫃, 否則被夾在中間的保管箱就沒法放入或是取出物品了。 依此類推, 您可以自行衍生宣告 4 維、5 維陣列的方式, 不過在實務上很少會使用到這麼多維的陣列。
多維陣列的配置 多維陣列的配置和 1 維陣列相似, 只要使用 new 運算子, 再指定各層的元素數量即可。例如:
多維陣列的配置
多維陣列的配置 第 4 行就是實際配置 2 維陣列的空間, 這就等於是先配置一個擁有 3 個元素的陣列, 其中每一個元素各指向一個擁有 4 個 int 型別資料的陣列。 由於 a 是指向一個擁有 3 個元素的陣列, 所以 a.length 的值是 3;而個別元素 a[0]、a[1]、與 a[2] 也各自是指向一個擁有 4 個元素的陣列, 因此 a[0].legnth、a[1].legnth 以及 a[2].legnth 的值都是 4。
多維陣列的配置
分層配置 由於 a 是指向一個有 3 個元素的陣列, 而每個元素各自指向一個陣列, 因此上述的程式也可以改寫成這樣:
分層配置
分層配置 第 4 行的意思就是先配置一個有 3 個元素的陣列, 其中每一個元素都是指向一個可以存放 int 型別資料的陣列, 這時尚未配置第 2 維的陣列。 第 6 ~ 8 行就透過迴圈, 幫剛剛配置的陣列中的每一個元素配置 int 型別資料的陣列。事實上, Java 在配置多維陣列的時候, 就是透過這樣的方式。
分層配置 如果使用這種配置方式, 請特別注意只有右邊維度的元素數目可以留空, 最左邊的維度一定要指明。不能撰寫以下的程式:
分層配置 但這樣就是可以的:
非矩形的多維陣列 由於 Java 是以剛剛所說明的方式配置多維陣列, 因此我們甚至於還可以建立一個奇特的多維陣列, 例如:
非矩形的多維陣列
非矩形的多維陣列 第 3 行宣告 a 指向一個擁有 3 個元素的陣列, 每個元素都指向 1 個可以放置 int 型別資料的陣列。 第 5 ~ 7 行, 分別為 a[0]、a[1]、與 a[2] 配置了不同大小的陣列。
非矩形的多維陣列 Java 允許建立這樣的多維陣列。如果以組合櫃的方式來描繪這種陣列, 就會發現這種陣列組合起來的組合櫃並不是矩形, 因此稱為非矩形陣列 (Non -Rectangular Array)。
非矩形的多維陣列
直接配置與設定多維陣列 多維陣列也和 1 維陣列一樣, 可以同時配置並且設定個別元素的內容。只要使用多層的大括號, 就可以對應到多維陣列的各個維度, 直接指定要配置的元素個數與元素內容。例如:
直接配置與設定多維陣列
直接配置與設定多維陣列
直接配置與設定多維陣列 在第4 行中, 就等於是宣告了陣列變數 a, 指向一個 2 維陣列, 其中每個元素都指向一個擁有 4 個整數的陣列, 同時還設定了個別元素的內容。 如果維度較多, 或者是元素數量較多時, 建議將大括號的配對依據陣列的維度排列, 例如:
直接配置與設定多維陣列
直接配置與設定多維陣列 要特別注意的是, 最後一個大括號的後面一定要加上分號 (;) , 表示該陣列宣告的敘述結束。
多維陣列的使用 多維陣列的使用也和 1 維陣列一樣, 只要在各維度指定索引碼, 就可以將指定的元素取出。舉例來說, a[2][3] 就是將 a 指向的多維陣列中第 3 排的組合櫃中的第 4 個保管箱的內容取出來。 如果是 a[2], 那就相當於是把 a 指向的多維陣列的第 3 排組合櫃取出來, 而您就可以將 a[2] 視為是指向一個 1 維陣列的變數來使用。
多維陣列的使用 多維陣列也可以和 foreach 迴圈搭配, 例如:
多維陣列的使用 第 5 行, 由於 a 指向一個 2 維陣列, 因此這個 foreach 迴圈取出的元素是 int[ ] 型別。
System.out.print() 注意到這裡使用了另外一個顯示資料的方法, 也就是 System.out.print(), 這個方法和System.out.println() 功能一模一樣, 唯一的差別是 System.out.print() 在顯示資料後不會換行。
7-4 參照型別 (Reference Data Type) 參照型別的特色 資源回收系統 (Garbage Collection System)
參照型別的特色 以這個陣列為例來說明參照型別的特性:
間接存取資料 參照型別的第一個特性是間接存取資料, 而不是直接使用變數的內容。舉例來說, 當執行以下這行程式時:
間接存取資料 實際上進行的動作如下: 先把變數 a 的內容取出來, 得到號碼牌 15。 到編號 15 的組合櫃中, 依據索引碼 2 打開組合櫃中第 3 個保管箱, 也就是號碼牌為 15-2 的保管箱。 最後, 才進行指定運算, 將 50 這個整數資料放入保管箱中。
間接存取資料
間接存取資料 也就是因為在存取資料時, 實際上是參照變數所記錄的位址 (號碼牌) 去存取真正的資料, 因此才稱之為是參照型別。由於必須透過間接的方式存取資料, 因此存取的時間一定會比使用直接存取資料的基本型別要久。 如果是多維陣列, 那麼維度越多, 間接參照的次數也就越多, 所花費的時間當然也就越久了。
指定運算不會複製資料 先來看看以下這個程式:
指定運算不會複製資料
指定運算不會複製資料 如果依照之前對於基本型別資料的觀念, 那麼對於執行的結果可能就會覺得奇怪。陣列 a 不是應該是 20 、30 、40, 而陣列 b 應該是 20 、30 、100 嗎?怎麼變成一樣的內容了呢?
指定運算不會複製資料 其實這正是參照型別最特別但也最需要注意的地方。 由於參照型別的變數所儲存的是存放真正資料的組合櫃的號碼牌, 因此在第 5 行的指定運算中, 等於是把變數 a 所儲存的號碼牌複製一個放到變數 b 中, 而不是把整個陣列的元素複製一份給變數 b。 這也就是說, 現在 b 和 a 都擁有同樣號碼的號碼牌, 也就等於是 b 和 a 都指向同一個組合櫃:
指定運算不會複製資料
指定運算不會複製資料 因此, 接下來透過 b 對陣列的操作, 就等於是對 a 所指陣列的操作, 而現在 a 和 b 根本就是指向同一個陣列, 所以更改的是同一個陣列。 事實上, 對於一個陣列變數來說, 也可以隨時變換整個陣列, 例如:
指定運算不會複製資料
指定運算不會複製資料
指定運算不會複製資料 其中第 8 ~ 10 行就重新幫 a 配置陣列, 這個新的陣列和一開始所配置的陣列大小也不一樣, 完全是新的陣列。事實上, 這並非重新配置陣列, 而只是捨棄了原本的陣列, 再配置一個新的陣列而已。
重新配置陣列的注意事項 在重新配置陣列的時候, 有一點要注意的是, 不能使用同時配置與設定元素內容的方式, 因為這種方式只能用在宣告陣列變數的時候。也就是說, 您不能撰寫如下的敘述: 因為這並不是一個宣告陣列變數的敘述。
資源回收系統 (Garbage Collection System) 瞭解了上述參照型別的特色後, 您可能已經想到了一個問題, 若不斷的配置陣列, 也就是不斷的索取組合櫃, 會不會有組合櫃全部被佔用, 沒有閒置的組合櫃可用的情況? 為了避免這樣的狀況, Java 設計了一個特別的機制, 可以將已經閒置不再需要使用的組合櫃回收, 以便供後續需要時使用。這個機制就稱為資源回收系統。
參照計數 (Reference Count) 為了讓資源回收系統能夠運作, 就必須要有一套監控組合櫃使用狀況的機制, 這樣才能在發現有閒置的組合櫃時, 自動將之回收。Java 的作法很簡單, 它會監控對應於每一個組合櫃的號碼牌個數, 以剛剛看過的ArrayAssignment.java 程式為例:
參照計數 (Reference Count)
參照計數 (Reference Count) 當程式執行完第 5 行後, 變數 a 和 b 都擁有對應同一個陣列的號碼牌, 也就是該陣列目前已經發出了 2 個號碼牌。這個數目稱為參照計數 (Reference Count), 因為它記錄了目前有多少變數還握有同一個組合櫃的號碼牌, 也就是還有多少變數可能會用到這個組合櫃。
參照計數 (Reference Count) 有了參照計數後, 資源回收系統就可以在某個組合櫃的參照計數為 0 時, 認定不再有需要使用該組合櫃的可能, 因而回收該組合櫃。那麼參照計數在甚麼狀況下才會減少呢?這可以分成 3 種狀況:
參照計數 (Reference Count) 參照型別的變數自行歸還號碼牌:只要將參照型別的變數指定為字面常數 null, 亦即: 就等於是告訴資源回收系統該變數不再需要使用所握有的號碼牌對應的組合櫃, 這時該組合櫃的參照計數就會減 1。
參照計數 (Reference Count) 給予參照型別變數其他組合櫃的號碼牌:參照型別變數只能握有一個號碼牌, 如果指派給它另一個組合櫃的號碼牌, 像是重新配置陣列, 它就必須歸還原本的號碼牌, 因此對應組合櫃的參照計數也會減 1 。例如:
參照計數 (Reference Count) 參照型別的變數離開有效範圍, 自動失效時。有關這一點, 會在下一章說明。 有了這樣的規則, 資源回收系統就可以知道哪些組合櫃還可能會再用到, 而哪一些組合櫃不可能會再用到了。
回收的時機 一旦發現有閒置的組合櫃之後, 資源回收系統並不會立即進行回收的動作, 而是先將這個組合櫃的號碼記錄下來。 這是因為回收組合櫃的工作並不單單只是將其收回, 可能還必須將組合櫃拆開, 或是與其他的組合櫃集中放置等搬移動作, 這些動作都必須耗費時間。 如果資源回收系統在發現閒置的組合櫃的同時便立即回收, 就可能會影響到程式正在執行的重要工作。
回收的時機 因此, 資源回收系統會先將要回收的組合櫃記錄下來, 等到發現您的程式似乎沒有在執行繁重的工作, 像是等待網路連線的對方回應時, 才進行回收的工作。 透過這樣的方式, 資源回收系統不但可以自動幫您將閒置的資源回收, 而且也不會影響到程式的執行效率。
7-5 命令列參數:argv 陣列 雖然到了這一章才介紹陣列, 但事實上之前所展示過的每一個程式, 都已經使用過陣列了。 如果您眼尖的話, 一定會注意到每一個程式中的 main( ) 方法在 main 之後都跟著一對小括號, 而在小括號中有 String[ ] argv 字樣:
命令列參數:argv 陣列 根據這一章所學, 這個 argv 無疑是指向陣列的變數, 而且其元素是用來儲存 String 型別的資料, 也就是字串。 但是這個 argv 有甚麼作用?又是從何而來呢?在這一節中就要為您詳細的說明。
argv 與 main( ) 方法 在第 2 章曾經說過, main( ) 方法是 Java 程式的起點, 當我們在命令提示符號下鍵入指令要求執行 Java 程式時, 例如: Java 虛擬機器就會載入 ShowArgv 程式, 並且從這個程式的 main( ) 方法開始執行。
argv 與 main( ) 方法 如果您的程式是用來顯示某個文字檔案的內容, 那麼可能就會需要將所要顯示檔案的檔名傳入, 此時就可以在命令提示符號下鍵入的指令後面加上額外的資訊, 例如:
argv 與 main( ) 方法 這樣一來, Java 虛擬機器就會配置一個字串陣列, 然後將程式名稱 (以此例來說就是 ShowArgv) 之後的字串, 以空白字元分隔切開成多個單字 (以此例來說, 就有 2 個單字, 一個是 test.html、另一個是 readme.txt) , 依序放入陣列中。 然後, 將這個陣列傳遞給 main() 方法, 而 argv 就會指向這個陣列。
argv 與 main( ) 方法 因此, 在您的程式中就可以透過 argv 取出使用者執行您的程式時附加在程式名稱之後的資訊。請看底下這個程式:
argv 與 main( ) 方法 程式中第 3、4 行就使用了一個 for 迴圈, 將 argv 所指向的字串陣列的元素依序顯示出來。如果您使用以下指令執行這個程式: 執行的結果就會顯示:
argv 與 main( ) 方法 如果要傳遞的資訊本身包含有空白, 可以使用一對雙引號 (“) 將整串字括起來, 例如: 其中的 this is a text 會被拆解為 4 個單字,如下:
argv 與 main( ) 方法 如果 “this is a text” 是單一項資訊, 就得使用一對雙引號括起來: 執行結果如下:
argv 與 main( ) 方法 其中 "this is a text" 就被當作是一項資訊, 而不會被拆成 this、is、a、text 分開的 4 項資訊了。
argv 陣列內容的處理 由於 argv 指向的是一個字串陣列, 因此不論您在指令行中鍵入甚麼資訊, 實際上在 argv 中都只是一串文字。 如果您希望傳遞的是數值資料, 那麼就必須自行將由數字構成的字串轉換成為數值, 這可以透過第 5 章使用過的 parseInt 來達成。
argv 陣列內容的處理 舉例來說, 如果您想要撰寫一個程式, 將使用者傳遞給main( ) 方法的整數數值算出階乘值後顯示出來, 像是這樣: 要得到 5 * 4 * 3 * 2 * 1 的值, 那麼就必須將傳入的字串 "5" 轉換成整數才行:
argv 陣列內容的處理
argv 陣列內容的處理 第 5 行就是將字串轉換成整數, 如果您要轉換的是浮點數, 可以改用以下的程式: 在第 7 ~ 9 行中, 就使用轉換出來的整數數值計算階乘, 並顯示出來。
7-6 綜合演練 將陣列運用在查表上 搜尋 (Search) 資料 找出最大與最小值 排序 (Sorting)
將陣列運用在查表上 陣列最常使用的場合就是在查表 (Table Lookup) 上, 以避免在程式中撰寫複雜的條件判斷敘述, 並且在條件數量有所變動時, 只需修改陣列的內容, 而不需修改程式的結構。 舉例來說, 假如某個停車場的費用是採計時制, 而且停的越久, 每一時段的停車費率就越高, 若停車費率如下:
將陣列運用在查表上
將陣列運用在查表上 那麼如果停車 5 小時, 停車費就是: 如果要撰寫程式來計算的話, 可以採用兩個方法, 一種是使用多層的條件敘述, 另一種則是使用陣列。 以下分別說明, 並比較其中的優劣。
使用多層的條件敘述 您可以使用多層的 if 或是 switch 敘述來撰寫這個程式, 以下是使用 if 的版本 (停車的時數是透過指令行傳入):
使用多層的條件敘述
使用多層的條件敘述
使用多層的條件敘述 第 7 行先將傳入的字串轉為整數, 代表停車的時數。 第 8 ~ 26 行利用了 4 個 if 敘述分別對應停車費率的 4 個時段, 依次累加停車費用。
使用多層的條件敘述 這個程式完全正 確, 但是有個問題 是, 如果停車費率 有所更改, 比如說 改成這樣: 那就得更改程式, 甚至需要移除或是新增 if 敘述, 平白增加寫錯程式的機會。如果善用陣列, 就可以避免這個缺點。
使用陣列 接著就示範如何使用陣列解決同樣的問題:
使用陣列
使用陣列 在第 4 、5 行中, 我們把停車費率的資訊放在 2 個陣列中。其中, hourTable 記錄了各個時段的分隔點, 而 feeTable 則記錄了各個時段的收費標準。
使用陣列 在第 11 ~ 16 中, 就依據停車時數在 hourTable 陣列中先找出所到達的最高費率時段。 第 18 ~ 22 行就是從找到的最高費率時段開始, 往下依次累加停車費用。 雖然程式看起來沒有使用 if 的版本簡單, 可是因為採用查表方式, 即便停車費率的時段或是價格異動, 也只要修改陣列中的資料, 程式的邏輯部分完全不需要更改。
使用陣列 舉例來說, 如果費率更改為如表 7-2, 那麼只要將第 4 、5 兩行的陣列初始資料改成這樣: 就可以計算新的費率了。
搜尋 (Search) 資料 在剛剛的程式中, 我們將停車費率的資訊放在陣列當中, 然後依據停車的時數到陣列中由最後一個元素依序往前搜尋, 找出符合的最高費率時段。 這種一個一個元素依序搜尋的方式稱為循序搜尋 (Sequential Search) 或是線性搜尋 (Linear Search), 在資料量多的時候, 最慘的狀況就是找遍了所有元素, 但卻沒有找到相符的項目。
搜尋 (Search) 資料 其實以停車費率的陣列這種已經排好順序 (Sorted) 的資料來說, 可以改用其他的搜尋方法, 會比需要循序比對的方式來的有效率。
二分搜尋法 (Binary Search) 在已排好順序的資料中找尋資料, 最常使用的一種方法稱為二分搜尋法, 可以一次捨棄剩餘資料的一半, 大幅提升搜尋的效率。 也正因為其每次過濾一半資料的特性, 所以稱為二分搜尋法。 舉例來說, 假設陣列中有以下 9 項已排序的資料:
二分搜尋法 (Binary Search) 如果要找出 15 在其中的位置, 可以: 先從最中間的元素找起。由於最中間的元素是 17, 而要找的是 15, 並且陣列中的元素已經是由小到大排列的順序, 所以可以推斷如果陣列中有 15 這個元素的話, 必定是在 17 的左邊, 因此可以先把 17 右邊的 4 個元素捨去不找。 從 17 左半邊的中間找起, 發現是 9, 因此 15 只可能在 9 的右邊。 再從 13 與 15 的中間找起, 為 13。 然後在 15 與 15 之間找到 15。
二分搜尋法 (Binary Search) 利用這樣的方式, 因為每次可以捨棄掉一半未看過的元素, 因此最多只要看過 4 個元素, 就可以確定要搜尋的資料在陣列中的位置。
二分搜尋法 (Binary Search) 我們將上述的方法寫成程式:
二分搜尋法 (Binary Search)
二分搜尋法 (Binary Search)
二分搜尋法 (Binary Search)
二分搜尋法 (Binary Search)
二分搜尋法 (Binary Search)
二分搜尋法 (Binary Search) 程式首先在 11 ~ 17 行要求使用者輸入所要搜尋的數值, 並且轉換為整數。 接著在 24 ~ 39 行利用一個 while 迴圈進行二分搜尋法。其中 low 與 high 都是陣列的索引碼, 切割出目前還需要搜尋的區間。 在第 26 行中, 每輪迴圈都使用 low 與 high 求出目前搜尋區間的中間元素索引碼, 指派給 middle。
二分搜尋法 (Binary Search) 在第 28 ~ 30 行中, 顯示了目前的搜尋區間以及中間元素。 第 32 ~ 38 行, 如果中間元素就是要搜尋的數值, 則跳出迴圈, 結束搜尋。否則, 如果要搜尋的數值大於中間元素, 就將 low 更改為 middle + 1, 將搜尋區間縮小為右半邊;否則就將 high 改為 middle - 1, 將搜尋區間縮小為左半邊。 最後顯示搜尋結果。
尋找小於某數值的最大元素 如果要將二分搜尋法套用在之前所展示的停車費用計算程式上, 就必須要做少許的修改。由於在停車費用計算時, 所要找的是停車時數適用的最高費率時段, 因此要找出的是陣列中比停車時數小的最大元素。 基本的想法是這樣的, 只要在每一輪把搜尋區間縮減成區間內的元素都不小於要搜尋的數值, 那麼最後迴圈結束時, low - 1 的索引所指的就是小於搜尋數值的最大元素了
尋找小於某數值的最大元素 只要將剛剛的 Binarysearch.java 改成以下這樣, 就可以了:
尋找小於某數值的最大元素
尋找小於某數值的最大元素
尋找小於某數值的最大元素
尋找小於某數值的最大元素
尋找小於某數值的最大元素 我們把測試中間值是否就是搜尋值的敘述去除, 並將第 32 行的條件測試改為小於等於, 如此一來, 最後就會讓搜尋區間內的元素都不小於搜尋值。 迴圈結束後, low 索引碼所指的就是不小於搜尋值的最小元素, 因此如果low 為 0, 就表示陣列中所有元素都不小於搜尋值;否則, low - 1 索引碼所指的元素就是小於搜尋值的最大元素。
找出最大與最小值 有些時候, 您所要搜尋的資料並沒有排好順序, 這時就無法套用二分搜尋法。舉例來說, 氣象局在各地都裝置有蒐集氣象資料, 像是氣溫、濕度等的儀器, 這些設備會每隔一段時間自動記錄數值。如果要從這些資料中找出最高與最低的數值, 就必須一一檢查每個元素的內容了。
找出最大與最小值
找出最大與最小值
找出最大與最小值 重點在於第 5 、6 行要將記錄最小值與最大值的變數預設為極大與極小的初始值, 這樣就可以一一比對陣列中的元素值, 找出最小與最大的值。 第 8 ~ 15 行就利用 foreach 迴圈, 一一比對陣列中的各個元素。
排序 (Sorting) 對於沒有排序的資料來說, 也可以先將之排好順序, 以方便後續的使用。 在這一節中, 我們要介紹一種最簡單的排序方法, 稱為氣泡排序法 (Bubble Sort)。這個方法的觀念是這樣的, 假設陣列中有 n 個元素, 氣泡排序法就依據以下步驟進行:
排序 (Sorting) 第 1 輪先從索引碼為 0 的元素開始, 往後兩兩相鄰元素相比, 如果前面的元素比後面的元素大, 就把兩個元素對調。這樣數值較大的元素會漸漸往後移, 一直比對到陣列最後, 索引碼為 n-1 的這個元素 (也就是陣列的最後一個元素) 必定是最大的元素。
排序 (Sorting) 重複上述的步驟, 依序將第 2 大、第 3 大、....、第 i 大的元素移到正確的位置。第 i 輪僅需比對到第 n-i 個元素即可, 因為後面的元素已經依序排好了。 以底下的資料為例:
排序 (Sorting) 排序的過程如下:
排序 (Sorting) 您可以從排序的過程中看到, 數值大的元素會漸漸地往右移動, 就好像氣泡不斷地往上浮一樣, 這也正是氣泡排序法的名稱由來。 以程式來表達就如下所示:
排序 (Sorting)
排序 (Sorting)
排序 (Sorting) 第 7 行的第 1 層迴圈控制了進行第幾輪, 而第 9 行的第 2 層迴圈進行相鄰元素兩兩比對的工作。 第 12 ~ 14 行進行元素交換的動作。 從執行結果您也可以看到, 後面幾輪的迴圈其實並不需要進行, 因為資料已經完全排好順序。其實氣泡排序法只是一種簡單的方法, 就效率來看, 還有其他更好的作法, 有興趣的讀者可以參考其他相關書籍。