學完本章你將能夠:

  • 宣告、建立與初始化陣列,並用索引存取元素。

  • 解釋索引從 0 開始的規則,避開越界例外。

  • 依場景選用傳統 for 與增強式 for 走訪陣列。

  • 說明陣列是參考型別的意義,並正確複製陣列。

  • 實作找最大值、加總、反轉、搜尋等基礎演算法。

  • 使用二維陣列處理表格資料,完成成績統計程式。

想像你是老師,要記錄全班 100 位學生的成績。用上一章的知識,你得宣告 |score1|、|score2| 一路寫到 |score100|——光宣告就一百 行,想算平均還得把一百個變數名字全部加起來。這顯然不對勁。程式世界的 解法是「陣列」(array):一排連續的格子,共用一個名字,用編號區分彼此。 一個變數,就能管理一百筆、一萬筆資料。

陣列也是我們遇到的第一個「參考型別」實例。上一章埋下的伏筆——盒子裡 放的是遙控器,不是電視——會在本章第一次真正發揮威力(也埋下第一批地雷)。 請特別留意「複製陣列」那一節。

宣告與建立陣列

建立陣列有兩種方式:知道長度但還沒有資料時,用 |new 型別[長度]|; 資料已經確定時,直接用大括號字面值。無論哪種方式,陣列的長度 一旦決定就不能改變。

/** * 陣列的兩種建立方式與索引存取。 */ public class ArrayBasics

public static void main(String[] args)

// 方式一:指定長度建立,每格自動填入預設值 0 int[] scores = new int[5]; scores[0] = 90; // 索引從 0 開始! scores[1] = 85; scores[4] = 70; // 最後一格的索引是「長度減 1」

System.out.println("第一格:" + scores[0]); System.out.println("最後一格:" + scores[4]); System.out.println("沒指定的格子:" + scores[2]);

// 方式二:用字面值直接給資料,長度自動算好 int[] primes = 2, 3, 5, 7, 11; System.out.println("陣列長度:" + primes.length); System.out.println("第三個質數:" + primes[2]);

第一格:90 最後一格:70 沒指定的格子:0 陣列長度:5 第三個質數:5

三個立刻要內化的重點:

  • 索引從 0 開始:第一個元素是 |scores[0]|, 長度為 5 的陣列,合法索引是 0 到 4。

  • length 是屬性不是方法:陣列用 |scores.length| (沒有括號);之後學的字串卻是 |s.length()| 方法, 這個不一致是 Java 的歷史包袱,只能記住。

  • 元素有預設值:數值型別是 0、|boolean| 是 false、參考型別是 |null|——所以上例沒指定的 |scores[2]| 印出 0。

越界:最常見的執行期錯誤

拿不存在的索引存取陣列,編譯器不會發現(它不追蹤執行期的值),但 執行時 JVM 會立刻拋出例外:

public class IndexError

public static void main(String[] args)

int[] data = 10, 20, 30;

System.out.println("長度:" + data.length); System.out.println("最後一個元素:" + data[2]);

// 長度是 3,合法索引只有 0、1、2——data[3] 不存在! System.out.println(data[3]);

長度:3 最後一個元素:30 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3 at IndexError.main(IndexError.java:9)

ArrayIndexOutOfBoundsException(陣列索引越界)是初學者最頻繁遇到的 執行期例外,九成的成因是「差一錯誤」:忘記索引從 0 開始,或迴圈 條件寫成 |i <= data.length|(多跑一圈)。黃金守則:最後一個 元素永遠是 |data[data.length - 1]|;迴圈條件永遠是 |i < data.length|(小於,不是小於等於)。

走訪陣列:兩種 for 的選擇

逐一處理陣列中的每個元素稱為「走訪」(traverse)。傳統 for 迴圈透過 索引存取;Java 5 之後的「增強式 for」(for-each)語法更簡潔,直接把 每個元素依序交到你手上:

/** * 傳統 for 與增強式 for 的對照。 */ public class TraverseDemo

public static void main(String[] args)

int[] scores = 88, 92, 79, 65, 70;

// 傳統 for:有索引 i 可用,適合需要「位置」的場合 for (int i = 0; i < scores.length; i++) System.out.println("第 " + (i + 1) + " 位:" + scores[i]);

// 增強式 for:讀作「對 scores 裡的每個 score」 // 不需要索引時,這種寫法更簡潔、更不會越界 int sum = 0; for (int score : scores) sum += score; System.out.println("總分:" + sum);

第 1 位:88 第 2 位:92 第 3 位:79 第 4 位:65 第 5 位:70 總分:394

兩種寫法的取捨:

面向 傳統 for 增強式 for
取得索引位置 可以 不行
修改元素內容 可以(|scores[i] = ...|) 不行(拿到的是複本)
倒著走、跳著走 可以 不行(固定從頭到尾)
語法簡潔安全 普通 佳(不可能越界)

原則:單純讀取全部元素用增強式 for;需要索引或要改值時用 傳統 for。注意增強式 for 中對迴圈變數賦值(如 |score = 0|) 改的只是複本,原陣列毫髮無傷——這是初學者常見的誤解。

陣列是參考型別

第 2 章說過:參考型別的變數裡放的是「遙控器」。現在來看這件事的實際 後果。把陣列變數指定給另一個變數,複製的只是遙控器——兩個變數控制的 是同一排格子:

import java.util.Arrays;

/** * 陣列是參考型別:等號只複製「遙控器」,不複製陣列本身。 */ public class ArrayReference

public static void main(String[] args)

int[] original = 1, 2, 3;

// 這行「沒有」複製陣列!兩個變數指向同一個陣列 int[] alias = original; alias[0] = 99; // 透過 alias 改…… System.out.println("original[0] = " + original[0]);

// 正確複製方式一:Arrays.copyOf(最常用) int[] copy = Arrays.copyOf(original, original.length); copy[0] = 1; // 改複製品,本尊不受影響 System.out.println("original[0] = " + original[0]); System.out.println("copy[0] = " + copy[0]);

// 正確複製方式二:System.arraycopy(較底層) // 參數:來源, 來源起點, 目的地, 目的起點, 複製幾個 int[] copy2 = new int[3]; System.arraycopy(original, 0, copy2, 0, 3); System.out.println("copy2 = " + Arrays.toString(copy2));

original[0] = 99 original[0] = 99 copy[0] = 1 copy2 = [99, 2, 3]

第一行輸出是本節的靈魂:明明改的是 |alias[0]|, |original[0]| 卻跟著變成 99——因為它們是同一個陣列的兩個名字。 想要真正的獨立複本,必須用 |Arrays.copyOf| 或 |System.arraycopy| 把每個格子的值搬過去。

比較陣列也有同樣的陷阱:|a == b| 比的是「兩支遙控器是否控制 同一台電視」,不是內容!比較兩個陣列的內容是否相同,要用 |Arrays.equals(a, b)|。同理,直接 |System.out.println(a)| 印出的是型別加上雜湊碼(類似 |[I@7ad041f3| 的神祕文字), 想看內容請用 |Arrays.toString(a)|。

陣列基礎演算法

接下來是陣列的實戰價值:把「走訪」與「條件判斷」組合成常用演算法。 這些模式未來會反覆出現,值得練到閉著眼睛都寫得出來。

找最大值、最小值與平均

/** * 一趟走訪同時找出最大值、最小值與總和。 */ public class MaxMinAverage

public static void main(String[] args)

int[] scores = 72, 88, 95, 60, 79;

// 起手式:先假設第一個元素就是最大值兼最小值 int max = scores[0]; int min = scores[0]; int sum = 0;

for (int score : scores) if (score > max) max = score; // 看到更大的,更新擂台主 if (score < min) min = score; sum += score;

// 平均要轉型成 double,否則整數除法會截斷小數 double average = (double) sum / scores.length; System.out.println("最高分:" + max); System.out.println("最低分:" + min); System.out.println("總分:" + sum); System.out.println("平均:" + average);

最高分:95 最低分:60 總分:394 平均:78.8

反轉與線性搜尋

import java.util.Arrays;

/** * 就地反轉陣列,以及最樸素的搜尋法——線性搜尋。 */ public class ReverseAndSearch

public static void main(String[] args)

int[] data = 10, 20, 30, 40, 50;

// 反轉:頭尾指標互換內容,一路向中間逼近 int left = 0; int right = data.length - 1; while (left < right) int temp = data[left]; // 暫存,否則值會被蓋掉 data[left] = data[right]; data[right] = temp; left++; right–; System.out.println("反轉後:" + Arrays.toString(data));

// 線性搜尋:從頭到尾逐一比對,找到就停 int target = 30; int foundIndex = -1; // -1 是「沒找到」的慣用暗號 for (int i = 0; i < data.length; i++) if (data[i] == target) foundIndex = i; break; // 已找到,不必再看後面 System.out.println(target + " 位於索引:" + foundIndex);

反轉後:[50, 40, 30, 20, 10] 30 位於索引:2

交換兩個值必須借助第三個變數 |temp|——直接互相指定會讓其中 一個值先被蓋掉。這個「三杯水交換」手法是基本功,第 6 章討論方法 傳參時它還會再登場。

Arrays 工具類別

自己寫演算法是為了理解原理;日常開發則直接用 Java 內建的 |java.util.Arrays| 工具類別,它把常用操作全包了:

import java.util.Arrays;

/** * Arrays 工具類別常用方法巡禮。 */ public class ArraysUtil

public static void main(String[] args)

int[] data = 5, 2, 8, 1, 9;

// toString:轉成好讀的文字 System.out.println("排序前:" + Arrays.toString(data));

// sort:由小到大排序(直接修改原陣列) Arrays.sort(data); System.out.println("排序後:" + Arrays.toString(data));

// binarySearch:二分搜尋。前提:陣列必須已排序! int index = Arrays.binarySearch(data, 8); System.out.println("8 的索引:" + index);

// fill:整個陣列填滿同一個值 int[] filled = new int[5]; Arrays.fill(filled, 7); System.out.println("填滿 7:" + Arrays.toString(filled));

// equals:逐格比較內容(回顧:== 比的是遙控器) int[] a = 1, 2, 3; int[] b = 1, 2, 3; System.out.println("a == b:" + (a == b)); System.out.println("內容相同:" + Arrays.equals(a, b));

排序前:[5, 2, 8, 1, 9] 排序後:[1, 2, 5, 8, 9] 8 的索引:3 填滿 7:[7, 7, 7, 7, 7] a == b:false 內容相同:true

|binarySearch| 之所以要求「先排序」,是因為二分搜尋的原理是 不斷把搜尋範圍砍半:跟中間值比大小,就能丟掉一半的資料。對已排序 的百萬筆資料,線性搜尋最多要看一百萬次,二分搜尋只要約 20 次 (\(O(n)\) 對 \(O(\log n)\) 的差距)。對未排序的陣列呼叫它, 會得到毫無意義的結果。

二維陣列

陣列的每個格子也可以再放一個陣列,形成「二維陣列」——本質是「陣列的 陣列」,用來表示表格:第一個索引選「列」,第二個索引選「行」。 典型應用:多位學生、多個科目的成績表。

/** * 二維陣列:3 位學生乘 3 個科目的成績表。 * scores[i][j] 代表第 i 位學生第 j 科的分數。 */ public class TwoDimensional

public static void main(String[] args)

int[][] scores = 85, 92, 78, // 學生 0 的國文、英文、數學 60, 71, 66, // 學生 1 90, 88, 95 // 學生 2 ;

// 外層走「每一位學生」,內層走「每一個科目」 for (int i = 0; i < scores.length; i++) int total = 0; for (int j = 0; j < scores[i].length; j++) total += scores[i][j]; // 平均四捨五入到小數點後一位 double avg = Math.round(total / 3.0 * 10) / 10.0; System.out.println("學生 " + (i + 1) + ":總分 " + total + ",平均 " + avg);

學生 1:總分 255,平均 85.0 學生 2:總分 197,平均 65.7 學生 3:總分 273,平均 91.0

讀法解密:|scores.length| 是列數(學生數 3), |scores[i].length| 是第 i 列的長度(科目數 3), |scores[1][2]| 是學生 1 的第 2 科成績(66)。

不規則陣列

既然二維陣列是「陣列的陣列」,每一列的長度自然可以不同——只要先固定 列數,再逐列建立不同長度的內層陣列,這稱為不規則陣列(jagged array):

/** * 不規則陣列:每一列的長度可以不同。 */ public class JaggedArray

public static void main(String[] args)

// 先只決定「有 3 列」,每列的長度稍後各自決定 int[][] triangle = new int[3][]; triangle[0] = new int[]1; triangle[1] = new int[]1, 2; triangle[2] = new int[]1, 2, 3;

// 增強式 for 也能走訪二維陣列:外層拿到的是「一列」 for (int[] row : triangle) for (int value : row) System.out.print(value + " "); System.out.println();

1 1 2 1 2 3

綜合範例:成績統計程式

期末總驗收:給定全班成績,計算總分、平均、最高最低分、高於平均的 人數,以及各分數區間的人數分布。這支程式綜合了本章所有技巧——多趟 走訪、複製排序、計數器陣列:

import java.util.Arrays;

/** * 成績統計:總分、平均、極值、高於平均人數與區間分布。 */ public class GradeStats

public static void main(String[] args)

int[] scores = 72, 88, 95, 60, 79, 83, 51, 90;

// 第一步:總分與平均 int sum = 0; for (int s : scores) sum += s; double average = (double) sum / scores.length;

// 第二步:最高最低分——先複製再排序,保護原始資料 int[] sorted = Arrays.copyOf(scores, scores.length); Arrays.sort(sorted); int min = sorted[0]; int max = sorted[sorted.length - 1];

// 第三步:高於平均的人數 int aboveCount = 0; for (int s : scores) if (s > average) aboveCount++;

// 第四步:區間分布——用小陣列當「計數器」 // buckets[0]:60 以下,buckets[1]:60 到 79,buckets[2]:80 以上 int[] buckets = new int[3]; for (int s : scores) if (s < 60) buckets[0]++; else if (s < 80) buckets[1]++; else buckets[2]++;

System.out.println("人數:" + scores.length); System.out.println("總分:" + sum); System.out.println("平均:" + average); System.out.println("最高分:" + max + ",最低分:" + min); System.out.println("高於平均:" + aboveCount + " 人"); System.out.println("不及格 " + buckets[0] + " 人,及格 " + buckets[1] + " 人,優良 " + buckets[2] + " 人");

人數:8 總分:618 平均:77.25 最高分:95,最低分:51 高於平均:5 人 不及格 1 人,及格 3 人,優良 4 人

值得學走的兩個設計決策:排序前先 |copyOf| 複製,避免破壞原始 順序(呼應本章的參考型別觀念);區間統計用「計數器陣列」而不是三個 獨立變數——如果將來要分十個區間,陣列版本幾乎不用改。

本章小結

  • 陣列用一個名字管理一整排同型別資料;長度固定,用 |length| 屬性取得。

  • 索引從 0 開始,最後一格是 |length - 1|;越界會拋出 ArrayIndexOutOfBoundsException。

  • 元素有預設值:數值 0、boolean 為 false、參考型別為 null。

  • 單純讀取用增強式 for;需要索引或修改元素用傳統 for。

  • 陣列是參考型別:|=| 只複製參考,兩個變數會指向同一個 陣列;真複製用 |Arrays.copyOf|。

  • 比內容用 |Arrays.equals|、印內容用 |Arrays.toString|,不要用 |==| 與直接列印。

  • 基礎演算法四件套:找極值(擂台法)、加總平均(記得轉型)、 反轉(頭尾指標)、線性搜尋(找到就 break)。

  • |Arrays.sort| 就地排序;|binarySearch| 只對 已排序陣列有效。

  • 二維陣列是「陣列的陣列」,外層列內層行;每列長度可以不同 (不規則陣列)。

本章練習

  1. 觀念題:長度為 10 的陣列,合法索引範圍是多少? |for (int i = 0; i <= 10; i++)| 走訪它會發生什麼事? (提示:差一錯誤與越界例外。)

  2. 觀念題:|int[] b = a;| 之後修改 |b[0]|, |a[0]| 會不會變?如果希望不會,該怎麼寫? (提示:參考複製與 Arrays.copyOf 的差別。)

  3. 實作題:建立一個 int 陣列存放 1 到 20,用一個迴圈把偶數 元素全部改成它的兩倍,最後用 |Arrays.toString| 印出。(提示:需要修改元素,選哪種 for?)

  4. 實作題:寫程式找出陣列 |4, 9, 2, 9, 1| 中最大值 「第一次出現」的索引。(提示:擂台法,同時記錄值與位置。)

  5. 實作題:宣告二維陣列存放 4 位學生的 2 科成績,印出每位 學生的總分,以及「每一科」的全班平均。(提示:科目平均 要固定行、走訪列——內外層迴圈對調。)

  6. 挑戰題:不使用 Arrays.sort,自己實作選擇排序:每一輪從 未排序區找最小值,換到未排序區的最前面。(提示:雙層 迴圈加上「三杯水」交換。)