Java 陣列使用攻略:原理、語法、實務與雷點全整理【Thinking in Java筆記(4-5)】

Java 陣列使用攻略:原理、語法、實務與雷點全整理【Thinking in Java筆記(4-5)】
Photo by orbtal media / Unsplash

如果你是 Java 新手,那「陣列」這個詞可能就像冰箱裡的透明盒子一樣:看得到、摸不到,更不知道裡面要放什麼。簡單來說,陣列是一種用來「一次裝很多東西」的容器。你可以想成是成績表、購物清單,或是一排櫃子,每格都可以放一筆資料

陣列是寫程式的必備工具,但初學者常常搞混怎麼宣告、怎麼初始化,甚至會搞出一些神秘的錯誤像是 NullPointerException

這篇文章就要幫你從零開始搞懂陣列這種東西,確保你下次看到它的時候不會想要格式化電腦

所以這篇文章就是來幫你:

  1. 解釋 Java 陣列怎麼宣告、初始化、動態建立
  2. 用具體的生活比喻解釋每個概念
  3. 示範怎麼在方法之間丟來丟去不爆炸

陣列的宣告方式

你想像一下:你要幫全班同學做一份成績表,那你會需要一張表格對吧?陣列就是程式裡的表格。你要先說明你要放「什麼類型的資料」,再來才是放在哪裡

int[] scores;

這是說:「我要一張放整數的表格。」方括號放在型別後或變數後面都行:

int scores[];

但注意:這裡你只是告訴 Java:「我要一份能裝整數的表格」,但你還沒跟它說這表格到底要多大。換句話說,這只是建立一個『參考』,就像你在地圖上圈出要蓋房子的地點,還沒真正動工

int scores[5]; // ❌ 不合法

Java 編譯器會報錯。因為 Java 要求你在建立實體時,用 new 關鍵字來分配記憶體空間。否則你只是說我要這種東西,卻沒真正提供它可以住的地方

陣列初始化方式

你可以想像初始化陣列,就像設計一張成績表。當你聲明陣列(像 int[] scores;),就像你在白紙上畫了一個表格的大致輪廓,決定這張表要記錄的是分數(整數),但你還沒決定這張表有幾格、也沒真的印出來

初始化,就是你真的去印出一張有固定格數的表格,準備拿來填資料。如果你沒做這步,程式執行時就會發現:「你說要用表格,但現在一張都沒有!」這時你還不能對這張表格做任何操作,因為它根本就還不存在

所以記住:宣告只是說「我要這個東西」,初始化才是「真的給我這個東西,而且我準備好了要用了」

有兩種常見方式可以初始化陣列:

  1. 直接指定內容(匿名陣列)
int[] scores = {90, 80, 70};

這個寫法表示你不只說「我要這樣的表格」,你還直接把三筆資料塞進去了。Java 會根據你填的東西自動幫你決定陣列的大小,這就像你印了一份已經填好三格內容的表格

  1. 使用 new 分配記憶體空間
int[] scores = new int[3];

這代表你說:「我要三格的空白表格。」此時內容會自動初始化為 0,就像你印了一份三格但還沒填資料的表格。你可以之後用迴圈一格格填進去

不初始化的話,就只是許下一個承諾而已,等你真的要用資料時,很可能會遇到錯誤(像是 null 或 0 沒搞清楚哪一個),所以一開始就把事情做清楚,對你有好處

陣列的參考與修改(⚠️ 陷阱多)

我們在初始化方式中提過,當你用 new 關鍵字建立陣列,其實是產生了一個在記憶體中的「表格」,並回傳一個參考給變數

但要特別注意:Java 中的陣列是參考型別(reference type),這意味著當你將一個陣列變數指派給另一個陣列變數時,兩者其實指向的是同一塊記憶體位置,也就是同一份陣列資料

這就像是你原本有一份共用的 Google Sheet,然後把連結分享給別人。你們看到的是同一份表格,只是從不同帳號登入。你改了,他也會看到改過的資料

如果你不清楚這一點,就會像下面這段程式碼一樣,踩到坑:

public class ArraysOfPrimitives {
  public static void main(String[] args) {
    int[] a1 = { 1, 2, 3, 4, 5 };
    int[] a2;
    a2 = a1;
    for(int i = 0; i < a2.length; i++)
      a2[i] = a2[i] + 1;
    for(int i = 0; i < a1.length; i++)
      System.out.println("a1[" + i + "] = " + a1[i]);
  }
} /* Output:
a1[0] = 2
a1[1] = 3
a1[2] = 4
a1[3] = 5
a1[4] = 6
*/

你以為你改的是 a2,其實 a1 也變了。因為他們指向的是同一張表格,不是複製品

提示:所有陣列都有一個內建的屬性 length,可以用來獲取陣列的長度,但不能修改它。陣列的索引從 0 開始,最大索引為 length - 1。如果超出這個範圍,就會發生執行時錯誤,通常會看到像 ArrayIndexOutOfBoundsException 這種訊息。這是 Java 在保護你,不讓你存取不屬於陣列的空間

動態建立陣列(你不知道會有幾格)

有時候你寫程式時並不知道一開始需要多大的陣列,這種情況就會用到「動態建立陣列」。這就像你要辦活動,開放現場報名,但你事先不知道到底會來幾個人。你得等人真的出現後,根據人數決定要印幾張報到表

在 Java 裡,你可以使用 new 搭配變數來動態決定陣列的大小: new 來動態建立:

public class ArrayNew {
  public static void main(String[] args) {
    int[] a;
    Random rand = new Random();
    a = new int[rand.nextInt(20)];
    System.out.println("length of a = " + a.length);
    System.out.println(Arrays.toString(a));
  }
} /* Output:
length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
*/

這裡你就像是在說:「我不知道會來幾個人,但幫我預留一排椅子,數量等我骰個骰子決定。」這就是 rand.nextInt(20) 的用途——它會給你一個 0 到 19 的隨機整數,Java 就會根據這個數字幫你開出對應大小的陣列

但這時你還沒填入任何真正的資料。因為這是基本型態(int)陣列,Java 就會貼心地幫你把每個格子都預設成 0。這有點像是你先把椅子排好了,每張椅子上先放一張紙條寫著「空位中」

物件陣列的陷阱(Integer vs int)

現在你換成了 Integer:

Integer[] objs = new Integer[rand.nextInt(10)];

你可能期待它像 int[] 陣列一樣,預設值通通是 0。但實際上,你印出來會看到:

[null, null, null...]

這不是 bug,而是 Java 的設計。因為 Integer 是一種物件型態,不是基本型態,所以在建立這類陣列時,每一格的預設值是 null,也就是「什麼都還沒放進去」的意思

想像你有一張空白表格,每一格代表一位學生的報名資料。基本型態(像 int)的表格會自動填上「0」,像是預設說「還沒填分數」,但物件型態的表格就像每一格上面只貼了一張紙條寫「這裡應該放一個人,但目前沒人來」

如果你直接使用這些 null,例如對它們呼叫方法,就會出現 NullPointerException,所以你必須先一格一格地「把人填進去」

public class ArrayClassObj {
  public static void main(String[] args) {
    Random rand = new Random(47);
    Integer[] a = new Integer[rand.nextInt(20)];
    System.out.println("length of a = " + a.length);
    for(int i = 0; i < a.length; i++)
      a[i] = rand.nextInt(500); // Autoboxing
    System.out.println(Arrays.toString(a));
  }
} /* Output: (Sample)
length of a = 18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
*/

這裡我們使用了 Java 的 autoboxing 功能,它會自動把 int 轉成 Integer,幫你省掉 new Integer(...) 這種老派寫法。總之,記得:物件陣列預設是 null,不自己動手填好是不會動的

陣列初始化的另一種方式(多一點語法糖)

除了前面介紹的使用 new 關鍵字來明確分配陣列大小外,Java 也提供了一些語法更簡潔、方便閱讀的初始化方式,特別適合在你一開始就知道要放哪些資料時使用。這種語法通常被稱為語法糖(syntax sugar),因為它讓寫起來更輕鬆、讀起來更順眼

你可以直接用大括號 {} 搭配一串值,Java 會自動判斷大小並幫你產生一個對應大小的陣列物件。例如:

public class ArrayInit {
  public static void main(String[] args) {
    Integer[] a = {
      new Integer(1),
      new Integer(2),
      3, // Autoboxing
    };
    Integer[] b = new Integer[]{
      new Integer(1),
      new Integer(2),
      3, // Autoboxing
    };
    System.out.println(Arrays.toString(a));
    System.out.println(Arrays.toString(b));
  }
} /* Output:
[1, 2, 3]
[1, 2, 3]
*/

兩者結果都一樣。這裡 3 是一個 int,但因為你要放進 Integer[] 這個物件型態的陣列,Java 會自動幫你把 3 包裝成 Integer.valueOf(3),這個過程稱為自動裝箱(autoboxing)

你可以把這個過程想像成:你本來是直接把硬幣(int)丟進抽屜(陣列),但現在規定每個硬幣都要裝進一個透明小盒子(Integer 物件)才能放進去。Java 幫你完成這個包裝,讓你不需要手動寫 new Integer(3),而是直接寫 3,省時省心。

這讓程式碼更簡潔,也更容易閱讀,特別是在初始化時快速塞值的時候

列表最後的逗號是可以有的,不用強迫症地刪掉它

使用陣列傳遞參數給方法

想像你要請別人幫你處理一整排的報名表,那你就要把這一排資料打包成陣列傳給他。陣列在這裡扮演的角色就像是一個資料打包箱,可以讓你一次把很多資料(不管是整數、字串、還是物件)統一遞交給某個方法進行處理

這種做法讓方法更靈活,因為你不用每次都寫死只能接受幾個參數,也不用每次都寫一樣的邏輯來處理單個資料。用陣列,你可以傳 3 筆也可以傳 300 筆,方法都能用一樣的方式來處理

最常見的例子就是 main(String[] args),這就是 Java 的入口方法,它接收的是一個字串陣列,可以讓使用者從 command line 輸入任意多個參數

陣列可以作為方法的參數,允許在方法之間傳遞多個資料。這在需要動態創建並初始化陣列時特別有用,例如將字串陣列作為參數傳遞給其他方法

例如你寫了一個程式叫做 Greet.java,在命令列輸入:

java Greet Alice Bob Charlie

那麼 args 這個字串陣列就會自動收到:

{"Alice", "Bob", "Charlie"}

你可以在程式裡用 args[0] 拿到 "Alice",用 args.length 知道總共有幾個名字。

這種設計讓使用者能靈活地傳入不同數量的資料,不用每次寫死,只要透過陣列接收就能處理彈性參數數量

以下是一個直接對另一個方法傳遞整個陣列的範例:

public class DynamicArray {
  public static void main(String[] args) {
    Other.main(new String[]{ "fiddle", "de", "dum" });
  }
}

class Other {
  public static void main(String[] args) {
    for(String s : args)
      System.out.print(s + " ");
  }
} /* Output:
fiddle de dum
*/

這個範例中,我們在 DynamicArray 類別的 main 方法中,透過 new String[]{"fiddle", "de", "dum"} 建立了一個字串陣列,並把它當作參數傳給 Other.main()

Other 類別的 main 方法中,透過增強型 for 迴圈 for (String s : args) 將接收到的字串陣列一一印出。這清楚示範了陣列如何作為方法參數被傳遞與使用

你可以想像這個流程就像把一疊報名資料遞給另一個人(方法),他拿到後照順序讀出來

總結:這些你真的該記住的事

到這裡為止,你應該已經對 Java 陣列有個比較完整的概念。總結一下,我們一路從宣告講到初始化,從基本型態講到物件型態,還帶你看了動態大小、方法參數傳遞、autoboxing 的小技巧。這邊快速幫你整理幾個你真的應該要記住的觀念:

  • 陣列大小是固定的:你一開始說幾格,就只能用幾格。想改大小?很抱歉,請新開一個,自己搬家
  • 宣告不等於初始化:你說「我要一份整數表格」只是宣告。你還得用 new 才會真的生出那份表格
  • 基本型態陣列預設值是 0,物件型態預設是 null:這差很多。物件的 null 如果你不初始化還拿來用,程式會爆炸給你看
  • 陣列是參考型別:兩個變數指到同一個陣列的話,改一個,另一個也會變。就像你們在改同一份 Google Sheet,誰動都會同步更新
  • length 是你的好朋友:用它控制迴圈,不用擔心少跑或超出界。但記住它是唯讀,你不能改它
  • 陣列可以當作方法參數傳來傳去:很適合處理多筆資料,也讓方法設計更彈性,不用寫死接受幾個變數

理解這些基本概念,你就能用陣列處理大多數入門需求了。這是邁向 Java 中階的地基,蓋房子要打好地基你知道吧?