Learning Objectives
- 用 new 或初始值清單建立陣列,說明各種型別的預設值,用索引與 length 存取元素
- 用迴圈走訪陣列,完成加總、平均、找最大值這類常見工作,並避開索引越界
- 說明陣列變數存的是參照:指派只複製參照,傳給方法後方法可以改陣列的內容
- 使用 Arrays.toString、sort、fill、copyOf、equals,分辨 == 與 Arrays.equals
- 建立與走訪二維陣列(包含每列長度不同的不規則陣列)
為什麼需要陣列
一個班有 40 位學生,要記錄每個人的分數。宣告 score1、score2……score40 四十個變數,不但寫起來累,也沒辦法用迴圈一次處理。
陣列(array)把多個同型別的值放在一起,用一個名稱加上索引(index)存取:
int[] scores = new int[40]; // 一次準備 40 個 int 的位置
scores[0] = 85; // 第 1 個元素的索引是 0
scores[39] = 72; // 最後一個元素的索引是 39
和 Python 的串列(list)相比,Java 的陣列有兩個重要差別:
| Java 陣列 | Python 串列 | |
|---|---|---|
| 元素型別 | 全部相同,宣告時決定 | 可以混合 |
| 長度 | 建立時決定,之後不能改變 | 可以 append、remove |
| 負數索引 | 不行,會拋出例外 | a[-1] 是最後一個 |
需要可以增減長度的容器時,Java 用 ArrayList(第 13 課)。陣列是最基本的容器,也是 ArrayList 內部實際存放資料的方式。
建立陣列:new 與初始值清單
建立陣列有兩種寫法:
int[] counts = new int[4]; // 指定長度,元素自動填入預設值
int[] primes = {2, 3, 5, 7, 11}; // 初始值清單:直接列出元素,長度由元素個數決定
int[] 讀作「int 陣列」,是變數的型別。(int counts[] 也能編譯,這是從 C 語言沿用的寫法,一般建議把 [] 放在型別旁邊。)
用 new 建立時,每個元素都會有預設值,和物件欄位的預設值相同:
| 元素型別 | 預設值 |
|---|---|
int、long、short、byte |
0 |
double、float |
0.0 |
boolean |
false |
char |
'\u0000'(編號 0 的字元) |
String 與其他參照型別 |
null |
陣列.length是元素個數;注意它是欄位,後面沒有小括號(字串的length()才有)- 合法的索引是
0到length - 1,最後一個元素是a[a.length - 1] - 超出範圍會拋出
ArrayIndexOutOfBoundsException,例如Index 5 out of bounds for length 5 - 初始值清單只能用在宣告的那一行;之後要重新指定,必須寫
primes = new int[]{1, 2, 3};
陣列的預設值與索引
每執行一個 new,右邊就多出一個陣列,格子下方的小數字是索引。注意四個陣列剛建立時的內容:int 是 0、double 是 0.0、boolean 是 false、String 是 null。左邊的變數只存一個指向陣列的箭頭(參照)。
走訪陣列
處理陣列幾乎都會配合迴圈。用索引走訪的標準寫法:
for (int i = 0; i < scores.length; i++) { // 條件用 <,不是 <=
sum += scores[i];
}
不需要索引、只要依序讀取時,可以用第 05 課的 for-each:for (int s : scores) { sum += s; }。
但 for-each 的 s 只是元素值的複本,在 for-each 裡寫 s = 0; 不會改到陣列;要修改元素,一定要用索引 scores[i] = …。
找最大值的常見做法:先假設第一個元素最大,再逐一比較,遇到更大的就換掉。
不要把 max 的初始值設成 0:陣列如果全是負數,0 會被誤當成最大值。
計算平均時要注意整數除法:sum / scores.length 兩邊都是 int,小數會被捨去;先把其中一邊轉成 double,寫成 (double) sum / scores.length。
總和、平均與最大值
看右邊陣列的哪一格正被 i 指到。max 一開始是第一個元素 68,遇到 92 時更新;第二個 92 不會更新,因為條件是 > 而不是 >=,所以 maxIndex 記的是第一個最大值的位置。最後一個迴圈用索引把每個元素加 5,看右邊格子的值一格一格改變。
想一想:預設值、長度與索引
下面的程式會印出什麼?
int[] a = new int[3];
System.out.println(a[1] + a.length);
下面的程式會怎樣?
String[] s = new String[2];
System.out.println(s[0].length());
下面的程式會印出什麼?(注意 b 和 c 之間有兩個空格)
String[] parts = "a b c".split(" ");
System.out.println(parts.length);
陣列是參照
陣列是物件,放在堆積上;陣列變數存的是指向陣列的參照(第 08 課會對一般物件說明同一件事)。這帶來幾個結果:
1. 指派只複製參照。 int[] b = a; 不會複製陣列,a 和 b 指向同一個陣列,透過 b 改元素,用 a 也看得到。
要得到一份獨立的副本,用 Arrays.copyOf(a, a.length) 或 a.clone()。
2. 傳給方法的是參照的複本。 方法拿到的參照指向同一個陣列,所以方法裡改元素,呼叫端看得到。
但如果方法讓參數改指向一個新陣列(data = new int[3];),只改了方法自己的那份參照,呼叫端的變數不受影響。
(方法的寫法在第 07 課詳細介紹,這裡先看一個 static 方法怎麼接收陣列。)
3. == 比較的是「是不是同一個陣列」。 兩個內容相同、分別建立的陣列,== 是 false。
比較內容要用 Arrays.equals(a, b)。陣列本身的 a.equals(b) 和 == 一樣只比參照,不要用。
另外,直接 System.out.println(a) 不會印出內容,而是印出像 [I@1b6d3586 這樣的文字([I 表示 int 陣列,@ 後面的數字每次執行可能不同)。要看內容請用 Arrays.toString(a)。
兩個變數、一個陣列
第 13 行之後,a 和 b 的箭頭指向同一個陣列。呼叫 doubleAll(a) 時,新框架的 data 也指向同一個陣列,方法裡改的就是 main 的那一個。第 20 行 Arrays.copyOf 才真的建立第二個陣列。
Arrays 工具類別
java.util.Arrays 提供許多處理陣列的 static 方法,使用前在檔案開頭寫 import java.util.Arrays;:
| 方法 | 作用 |
|---|---|
Arrays.toString(a) |
把內容轉成 "[3, 7, 19, 42]" 這樣的字串 |
Arrays.sort(a) |
由小到大排序,直接改變原陣列,沒有回傳值 |
Arrays.fill(a, v) |
把每個元素都設成 v |
Arrays.copyOf(a, n) |
建立長度 n 的新陣列並複製內容;n 比較大時,多出來的格子是預設值 |
Arrays.copyOfRange(a, from, to) |
複製索引 from 到 to - 1 的部分 |
Arrays.equals(a, b) |
長度與每個元素都相等時回傳 true |
Arrays.deepToString(m) |
二維陣列用這個,toString 只會印出每列的參照 |
字串的方法也會產生陣列:
"apple,banana,cherry".split(",")依分隔字串切開,得到String[]"java".toCharArray()得到char[],可以逐字修改後用new String(letters)組回字串(字串本身不能修改)
sort、copyOf、fill、split、toCharArray
第 9 行 Arrays.sort(nums) 執行後,右邊同一個陣列的內容直接變成排序後的順序,沒有建立新陣列。第 12 行 copyOf 建立長度 6 的新陣列,多出來的兩格是 0;接著 fill 只改 nums,bigger 不受影響。
二維陣列
二維陣列是「陣列的陣列」:外層陣列的每個元素,都是一個指向一維陣列的參照。
int[][] grid = new int[2][3]; // 2 列、每列 3 行,全部是 0
int[][] seats = {
{1, 2, 3},
{4, 5, 6}
};
seats[1][2] // 第 2 列、第 3 行,值是 6
grid.length是列數,grid[r].length是第r列的行數- 走訪時用巢狀迴圈,內層的條件寫
c < grid[r].length,每一列長度不同也不會出錯 - 因為每一列是獨立的陣列,可以只指定列數:
new int[3][],再逐列建立長度不同的陣列,稱為不規則陣列(jagged array),例如三角形的座位表 - 印出內容用
Arrays.deepToString(grid)
二維陣列與不規則陣列
右邊的 grid 是一個長度 2 的陣列,裡面兩格各自用箭頭指向一列。後半段 new int[3][] 只建立外層,三格一開始都是 null;迴圈裡逐列 new,每列長度不同。
想一想:參照、比較與二維陣列
下面的程式會印出什麼?
public class Main {
static void reset(int[] a) { a = new int[]{0, 0}; }
static void setFirst(int[] a) { a[0] = 0; }
public static void main(String[] args) {
int[] n = {5, 6};
reset(n);
System.out.print(n[0] + " ");
setFirst(n);
System.out.println(n[0]);
}
}
下面的程式會印出什麼?
int[] x = {1, 2};
int[] y = {1, 2};
System.out.println((x == y) + " " + Arrays.equals(x, y) + " " + x.equals(y));
下面的程式會印出什麼?
int[] a = {1, 2, 3};
for (int v : a) {
v = v * 10;
}
System.out.println(Arrays.toString(a));
下面的程式會印出什麼?
int[][] m = {{1}, {2, 3}, {4, 5, 6}};
System.out.println(m[2].length + m[1][0]);
動手寫:一週氣溫
temps 是五天的最高氣溫。請找出最高與最低溫,並建立一個新陣列 reversed,把氣溫倒過來放(最後一天放在最前面),最後印出來。
重點回顧
- 陣列存放多個同型別的值,長度建立後不能改變;
new int[n]的元素是預設值(0、0.0、false、null),初始值清單{…}直接給內容 - 索引從 0 到
length - 1;length是欄位,不加小括號;超出範圍拋出ArrayIndexOutOfBoundsException - 用
for (int i = 0; i < a.length; i++)走訪;for-each 只能讀,不能改元素 - 陣列變數存的是參照:指派只複製參照,傳給方法後方法可以改內容;要獨立副本用
Arrays.copyOf ==和陣列的equals只比較是不是同一個陣列,比內容用Arrays.equals;印內容用Arrays.toStringArrays.sort直接改變原陣列;split產生String[]、toCharArray產生char[]- 二維陣列是陣列的陣列,每列可以長度不同;走訪時內層條件用
grid[r].length,印出用Arrays.deepToString