陣列是一種有用的資料型別,用於管理在連續記憶體位置中建模最好的集合元素。下面是如何有效地使用它們。
有使用 C 或者 FORTRAN 語言程式設計經驗的人會對陣列的概念很熟悉。它們基本上是一個連續的記憶體塊,其中每個位置都是某種資料型別:整型、浮點型或者諸如此類的資料型別。
Java 的情況與此類似,但是有一些額外的問題。
讓我們在 Java 中建立一個長度為 10 的整型陣列:
int[] ia = new int[10];
上面的程式碼片段會發生什麼?從左到右依次是:
int[]
將變數的型別宣告為 int
陣列(由 []
表示)。ia
。=
告訴我們,左側定義的變數賦值為右側的內容。=
的右側,我們看到了 new
,它在 Java 中表示一個物件正在被初始化中,這意味著已為其分配儲存空間並呼叫了其建構函式(請參見此處以獲取更多資訊)。int[10]
,它告訴我們正在初始化的這個物件是包含 10 個整型的陣列。因為 Java 是強型別的,所以變數 ia
的型別必須跟 =
右側表示式的型別相容。
讓我們把這個簡單的陣列放在一段程式碼中,並嘗試執行一下。將以下內容儲存到一個名為 Test1.java
的檔案中,使用 javac
編譯,使用 java
執行(當然是在終端中):
import java.lang.*;public class Test1 { public static void main(String[] args) { int[] ia = new int[10]; // 見下文註 1 System.out.println("ia is " + ia.getClass()); // 見下文註 2 for (int i = 0; i < ia.length; i++) // 見下文註 3 System.out.println("ia[" + i + "] = " + ia[i]); // 見下文註 4 }}
讓我們來看看最重要的部分。
ia
,這顯而易見。ia.getClass()
。沒錯,ia
是屬於一個類的物件,這行程式碼將告訴我們是哪個類。for (int i = 0; i < ia.length; i++)
,它定義了一個迴圈索引變數 i
,該變數遍歷了從 0 到比 ia.length
小 1 的序列,這個表示式告訴我們在陣列 ia
中定義了多少個元素。ia
的每個元素的值。當這個程式編譯和執行時,它產生以下結果:
me@mydesktop:~/Java$ javac Test1.javame@mydesktop:~/Java$ java Test1ia is class [Iia[0] = 0ia[1] = 0ia[2] = 0ia[3] = 0ia[4] = 0ia[5] = 0ia[6] = 0ia[7] = 0ia[8] = 0ia[9] = 0me@mydesktop:~/Java$
ia.getClass()
的輸出的字串表示形式是 [I
,它是“整數陣列”的簡寫。與 C 語言類似,Java 陣列以第 0 個元素開始,擴充套件到第 <陣列大小> - 1
個元素。如上所見,我們可以看到陣列 ia
的每個元素都(似乎由陣列建構函式)設定為零。
所以,就這些嗎?宣告型別,使用適當的初始化器,就完成了嗎?
好吧,並沒有。在 Java 中有許多其它方法來初始化陣列。
像所有好的問題一樣,這個問題的答案是“視情況而定”。在這種情況下,答案取決于初始化後我們希望對陣列做什麼。
在某些情況下,陣列自然會作為一種累加器出現。例如,假設我們正在程式設計實現計算小型辦公室中一組電話分機接收和撥打的電話數量。一共有 8 個分機,編號為 1 到 8,加上話務員的分機,編號為 0。 因此,我們可以宣告兩個陣列:
int[] callsMade;int[] callsReceived;
然後,每當我們開始一個新的累計呼叫統計資料的週期時,我們就將每個陣列初始化為:
callsMade = new int[9];callsReceived = new int[9];
在每個累計通話統計資料的最後階段,我們可以列印出統計資料。粗略地說,我們可能會看到:
import java.lang.*;import java.io.*;public class Test2 { public static void main(String[] args) { int[] callsMade; int[] callsReceived; // 初始化呼叫計數器 callsMade = new int[9]; callsReceived = new int[9]; // 處理呼叫…… // 分機撥打電話:callsMade[ext]++ // 分機接聽電話:callsReceived[ext]++ // 彙總通話統計 System.out.printf("%3s%25s%25s\n", "ext", " calls made", "calls received"); for (int ext = 0; ext < callsMade.length; ext++) { System.out.printf("%3d%25d%25d\n", ext, callsMade[ext], callsReceived[ext]); } }}
這會產生這樣的輸出:
me@mydesktop:~/Java$ javac Test2.javame@mydesktop:~/Java$ java Test2ext calls made calls received 0 0 0 1 0 0 2 0 0 3 0 0 4 0 0 5 0 0 6 0 0 7 0 0 8 0 0me@mydesktop:~/Java$
看來這一天呼叫中心不是很忙。
在上面的累加器範例中,我們看到由陣列初始化程式設定的零起始值可以滿足我們的需求。但是在其它情況下,這個起始值可能不是正確的選擇。
例如,在某些幾何計算中,我們可能需要將二維陣列初始化為單位矩陣(除沿主對角線———左上角到右下角——以外所有全是零)。我們可以選擇這樣做:
double[][] m = new double[3][3];for (int d = 0; d < 3; d++) { m[d][d] = 1.0;}
在這種情況下,我們依靠陣列初始化器 new double[3][3]
將陣列設定為零,然後使用迴圈將主對角線上的元素設定為 1。在這種簡單情況下,我們可以使用 Java 提供的快捷方式:
double[][] m = { {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}};
這種可視結構特別適用於這種應用程式,在這種應用程式中,它便於複查陣列的實際布局。但是在這種情況下,行數和列數只在執行時確定時,我們可能會看到這樣的東西:
int nrc;// 一些程式碼確定行數和列數 = nrcdouble[][] m = new double[nrc][nrc];for (int d = 0; d < nrc; d++) { m[d][d] = 1.0;}
值得一提的是,Java 中的二維陣列實際上是陣列的陣列,沒有什麼能阻止無畏的程式設計師讓這些第二層陣列中的每個陣列的長度都不同。也就是說,下面這樣的事情是完全合法的:
int [][] differentLengthRows = { {1, 2, 3, 4, 5}, {6, 7, 8, 9}, {10, 11, 12}, {13, 14}, {15}};
在涉及不規則形狀矩陣的各種線性代數應用中,可以應用這種型別的結構(有關更多資訊,請參見此 Wikipedia 文章)。除此之外,既然我們了解到二維陣列實際上是陣列的陣列,那麼以下內容也就不足為奇了:
differentLengthRows.length
可以告訴我們二維陣列 differentLengthRows
的行數,並且:
differentLengthRows[i].length
告訴我們 differentLengthRows
第 i
行的列數。
考慮到在執行時確定陣列大小的想法,我們看到陣列在範例化之前仍需要我們知道該大小。但是,如果在處理完所有資料之前我們不知道大小怎麼辦?這是否意味著我們必須先處理一次以找出陣列的大小,然後再次處理?這可能很難做到,尤其是如果我們只有一次機會使用資料時。
Java 集合框架很好地解決了這個問題。提供的其中一項是 ArrayList
類,它類似於陣列,但可以動態擴充套件。為了演示 ArrayList
的工作原理,讓我們建立一個 ArrayList
物件並將其初始化為前 20 個斐波那契數位:
import java.lang.*;import java.util.*;public class Test3 { public static void main(String[] args) { ArrayList<Integer> fibos = new ArrayList<Integer>(); fibos.add(0); fibos.add(1); for (int i = 2; i < 20; i++) { fibos.add(fibos.get(i - 1) + fibos.get(i - 2)); } for (int i = 0; i < fibos.size(); i++) { System.out.println("fibonacci " + i + " = " + fibos.get(i)); } }}
上面的程式碼中,我們看到:
Integer
的 ArrayList
的宣告和範例化。add()
附加到 ArrayList
範例。get()
通過索引號檢索元素。size()
來確定 ArrayList
範例中已經有多少個元素。這裡沒有展示 put()
方法,它的作用是將一個值放在給定的索引號上。
該程式的輸出為:
fibonacci 0 = 0fibonacci 1 = 1fibonacci 2 = 1fibonacci 3 = 2fibonacci 4 = 3fibonacci 5 = 5fibonacci 6 = 8fibonacci 7 = 13fibonacci 8 = 21fibonacci 9 = 34fibonacci 10 = 55fibonacci 11 = 89fibonacci 12 = 144fibonacci 13 = 233fibonacci 14 = 377fibonacci 15 = 610fibonacci 16 = 987fibonacci 17 = 1597fibonacci 18 = 2584fibonacci 19 = 4181
ArrayList
範例也可以通過其它方式初始化。例如,可以給 ArrayList
構造器提供一個陣列,或者在編譯過程中知道初始元素時也可以使用 List.of()
和 array.aslist()
方法。我發現自己並不經常使用這些方式,因為我對 ArrayList
的主要用途是當我只想讀取一次資料時。
此外,對於那些喜歡在載入資料後使用陣列的人,可以使用 ArrayList
的 toArray()
方法將其範例轉換為陣列;或者,在初始化 ArrayList
範例之後,返回到當前陣列本身。
Java 集合框架提供了另一種類似陣列的資料結構,稱為 Map
(對映)。我所說的“類似陣列”是指 Map
定義了一個物件集合,它的值可以通過一個鍵來設定或檢索,但與陣列(或 ArrayList
)不同,這個鍵不需要是整型數;它可以是 String
或任何其它複雜物件。
例如,我們可以建立一個 Map
,其鍵為 String
,其值為 Integer
型別,如下:
Map<String, Integer> stoi = new Map<String, Integer>();
然後我們可以對這個 Map
進行如下初始化:
stoi.set("one",1);stoi.set("two",2);stoi.set("three",3);
等類似操作。稍後,當我們想要知道 "three"
的數值時,我們可以通過下面的方式將其檢索出來:
stoi.get("three");
在我的認知中,Map
對於將第三方資料集中出現的字串轉換為我的資料集中的一致程式碼值非常有用。作為資料轉換管道的一部分,我經常會構建一個小型的獨立程式,用作在處理資料之前清理資料;為此,我幾乎總是會使用一個或多個 Map
。
值得一提的是,ArrayList
的 ArrayList
和 Map
的 Map
是很可能的,有時也是合理的。例如,假設我們在看樹,我們對按樹種和年齡範圍累計樹的數目感興趣。假設年齡範圍定義是一組字串值(“young”、“mid”、“mature” 和 “old”),物種是 “Douglas fir”、“western red cedar” 等字串值,那麼我們可以將這個 Map
中的 Map
定義為:
Map<String, Map<String, Integer>> counter = new Map<String, Map<String, Integer>>();
這裡需要注意的一件事是,以上內容僅為 Map
的行建立儲存。因此,我們的累加程式碼可能類似於:
// 假設我們已經知道了物種和年齡範圍if (!counter.containsKey(species)) { counter.put(species,new Map<String, Integer>());}if (!counter.get(species).containsKey(ageRange)) { counter.get(species).put(ageRange,0);}
此時,我們可以這樣開始累加:
counter.get(species).put(ageRange, counter.get(species).get(ageRange) + 1);
最後,值得一提的是(Java 8 中的新特性)Streams 還可以用來初始化陣列、ArrayList
範例和 Map
範例。關於此特性的詳細討論可以在此處和此處中找到。