CodeSprout程式萌芽

CHAPTER 05 · JAVA 11 FOUNDATIONS

Java 集合架構

依順序、重複、排序與鍵值需求選擇容器,練習 List、Set、Map、Queue 與走訪器。

這一章學會
  • 比較 List、Set、Map 與 Queue
  • 用泛型安全地新增、查找、移除與走訪
  • 區分插入順序、自然排序與優先佇列
先預測結果,再看輸出。

每個 Java 範例都是獨立的 Main.java,可用 JDK 11 編譯執行;請一次複製一個範例。題庫使用 public class Solution 中的 public static solve(...) 方法,請保留題目指定的型別與參數,使用 return 交回答案,不必另外讀取輸入或撰寫 main。

01|先看資料需求,再挑集合

陣列長度固定,集合可依實作動態增減元素。Collection 是 List、Set、Queue 等介面的共同上層;Map 保存 key-value 配對,屬於集合架構,但沒有繼承 Collection。Collection 是介面;Collections 則是提供排序等靜態工具方法的類別。

  • List:元素有索引與順序,允許重複;常用 ArrayList。
  • Set:元素不重複;HashSet 無指定走訪順序,LinkedHashSet 保留插入順序,TreeSet 依比較規則排序。
  • Map:key 不可重複,每個 key 對應一個 value;不同 key 可以保存相同 value。
  • Queue:依規則取出待處理元素。LinkedList 可當 FIFO 佇列;PriorityQueue 依優先順序取出。

教材對照:PDF 第 2–5、10–11、26 頁

02|泛型與 Collection 常用操作

泛型把元素型別寫在角括號,例如 Collection<String>,讓編譯器檢查放入與取出的資料。集合保存物件,整數使用 Integer;自動裝箱讓 add(11) 可轉成 Integer。元素不會因放入集合就失去本身的執行時類別。

  • add/addAll/remove/removeAll/retainAll 的 boolean 回傳值表示集合是否改變;不是通用的成功/失敗代碼。
  • containsAll 檢查是否包含指定元素;retainAll 只留下交集元素,removeAll 移除指定集合中的元素。
  • 某些集合不支援修改,可能拋出 UnsupportedOperationException,例如 List.of 建立的不可修改清單。
  • List 的相等比較考慮元素與順序;Set 的相等比較考慮元素內容,不是只要同屬 Set 就相等。
JAVA 11 · Main.java程式碼
import java.util.Collection;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        Collection<String> topics = new ArrayList<>();
        topics.add("Java");
        topics.add("Python");
        System.out.println(topics.contains("Java"));
        System.out.println(topics.size());
        System.out.println(topics.remove("Python"));
        System.out.println(topics);
        topics.clear();
        System.out.println(topics.isEmpty());
    }
}
預期輸出
true
2
true
[Java]
true

教材對照:PDF 第 2、4–5、8 頁

03|List 與 ArrayList:索引操作

List 最像可增減的陣列。get 依索引取值,set 替換原位置,add(index, value) 插入。indexOf 找不到時回傳 -1。ArrayList 適合經常依索引讀取;中間插入或移除通常需要移動後面的元素。

JAVA 11 · Main.java程式碼
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        List<Integer> scores = new ArrayList<>(Arrays.asList(70, 80, 70));
        scores.add(1, 90);
        scores.set(0, 75);
        System.out.println(scores);
        System.out.println(scores.remove(1));
        System.out.println(scores.remove(Integer.valueOf(70)));
        System.out.println(scores);
        System.out.println(scores.indexOf(99));
    }
}
預期輸出
[75, 90, 80, 70]
90
true
[75, 80]
-1

教材對照:PDF 第 17–19 頁

04|Iterator 安全走訪與移除

Iterator 用 hasNext 檢查是否還有下一筆,next 取下一筆。需要邊走訪邊移除時,使用這個走訪器的 remove;每次 next 後最多移除一次。一般增強 for 中直接呼叫原清單 remove,可能引發 ConcurrentModificationException。

JAVA 11 · Main.java程式碼
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        List<Integer> values = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
        Iterator<Integer> iterator = values.iterator();
        while (iterator.hasNext()) {
            int value = iterator.next();
            if (value % 2 == 0) {
                iterator.remove();
            }
        }
        System.out.println(values);
    }
}
預期輸出
[1, 3]

教材對照:PDF 第 7–9 頁

05|ListIterator 與 Enumeration

ListIterator 支援 List 的雙向走訪,也可在允許修改的清單中 add、set、remove。nextIndex/previousIndex 只回報位置,不會移動游標。較早的 Enumeration 常見於 Vector、Hashtable,使用 hasMoreElements/nextElement,沒有移除方法;它也不是物件序列化。

JAVA 11 · Main.java程式碼
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ListIterator;
import java.util.Vector;
import java.util.Enumeration;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(Arrays.asList("A", "B"));
        ListIterator<String> iterator = names.listIterator(names.size());
        while (iterator.hasPrevious()) {
            System.out.println(iterator.previous());
        }
        Vector<String> oldList = new Vector<>(names);
        Enumeration<String> elements = oldList.elements();
        while (elements.hasMoreElements()) {
            System.out.println(elements.nextElement());
        }
    }
}
預期輸出
B
A
A
B

教材對照:PDF 第 6–9、19 頁

06|Set 的唯一性與插入順序

HashSet 依 hashCode 與 equals 協同處理唯一性,不保證走訪順序。LinkedHashSet 從一開始就保留插入順序,同一元素再次加入不會往後移動。若先放入 HashSet 再轉 LinkedHashSet,已經失去的原始插入順序不會自動恢復。

JAVA 11 · Main.java程式碼
import java.util.Set;
import java.util.LinkedHashSet;

public class Main {
    public static void main(String[] args) {
        Set<String> topics = new LinkedHashSet<>();
        System.out.println(topics.add("Java"));
        topics.add("Python");
        System.out.println(topics.add("Java"));
        System.out.println(topics);
        System.out.println(topics.size());
    }
}
預期輸出
true
false
[Java, Python]
2

教材對照:PDF 第 10–14 頁

07|TreeSet 與排序範圍

TreeSet 依元素的自然順序或建構時提供的 Comparator 排序;自然順序由 Comparable.compareTo 定義,不是 equals。比較結果為 0 的元素在 TreeSet 中視為同一元素;應讓比較規則與 equals 一致。數字按數值、字串按字串比較規則,不能一概視為字母排序。

JAVA 11 · Main.java程式碼
import java.util.TreeSet;
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        TreeSet<Integer> values = new TreeSet<>(Arrays.asList(8, 3, 5, 3));
        System.out.println(values);
        System.out.println(values.first());
        System.out.println(values.last());
        System.out.println(values.headSet(5));
        System.out.println(values.subSet(3, 8));
        System.out.println(values.tailSet(5));
    }
}
預期輸出
[3, 5, 8]
3
8
[3]
[3, 5]
[5, 8]

教材對照:PDF 第 14–16 頁

08|LinkedList、Queue、Stack 與 Deque

LinkedList 可作 List 或雙端佇列。它在已知節點位置的插入、移除有優勢,但依索引尋找位置仍要走訪,不能認為所有增刪都必定比 ArrayList 快。FIFO 先進先出;LIFO 後進先出。教材中的 Stack 繼承 Vector;Java 11 的新程式也可用 Deque 表達堆疊。

  • Queue 的 peek 只看頭部、poll 取出並移除;空佇列都回傳 null。element/remove 在空佇列會拋出例外。
  • offer 新增元素,以特殊回傳值表達容量限制;add 在無法新增時可能拋出例外,並非每次呼叫都會失敗。
  • Stack 的 push 新增,pop 取出並移除,peek 只看頂端;空 Stack 的 pop/peek 會拋出 EmptyStackException。
  • Vector、Hashtable 等舊集合有同步方法,但『先檢查再操作』等多步行為不因此自動成為不可分割的操作。
JAVA 11 · Main.java程式碼
import java.util.Queue;
import java.util.LinkedList;
import java.util.Deque;
import java.util.ArrayDeque;

public class Main {
    public static void main(String[] args) {
        Queue<String> queue = new LinkedList<>();
        queue.offer("A");
        queue.offer("B");
        System.out.println(queue.peek());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
        Deque<String> stack = new ArrayDeque<>();
        stack.push("A");
        stack.push("B");
        System.out.println(stack.peek());
        System.out.println(stack.pop());
    }
}
預期輸出
A
A
B
null
B
B

教材對照:PDF 第 19–24 頁

09|PriorityQueue 與 Comparator

PriorityQueue 的頭部是依比較規則最優先的元素,因此它不是一般 FIFO。要取得完整排序結果,應反覆 poll;直接印整個 PriorityQueue 或使用 iterator,不保證所有元素依排序列出。Comparator.compare 回傳負數、0、正數表示前後關係。

JAVA 11 · Main.java程式碼
import java.util.PriorityQueue;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>(Comparator.reverseOrder());
        queue.offer(3);
        queue.offer(8);
        queue.offer(5);
        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
    }
}
預期輸出
8
5
3

教材對照:PDF 第 24–25 頁

10|Map:用 key 存取 value

Map 的 put(key, value) 新增配對;key 已存在時會取代 value,並回傳舊值。get 取得值,containsKey 檢查 key 是否存在。HashMap 不保證走訪順序;LinkedHashMap 預設保留插入順序,也可透過特定建構子改成存取順序。

  • keySet 回傳 key 的 Set 檢視;values 回傳 value 的 Collection 檢視;entrySet 回傳配對的 Set 檢視。
  • Map 本身沒有 iterator 方法,但可透過 entrySet 等檢視走訪。
  • HashMap/LinkedHashMap 可有 null key 與 null value;若 get 回傳 null,可再用 containsKey 區分沒有 key 或值本來就是 null。
  • Hashtable 不接受 null key 或 value。避免把容器的 null 規則套用到所有 Map。
JAVA 11 · Main.java程式碼
import java.util.Map;
import java.util.LinkedHashMap;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> scores = new LinkedHashMap<>();
        scores.put("Amy", 80);
        scores.put("Ben", 70);
        System.out.println(scores.put("Amy", 95));
        System.out.println(scores.get("Amy"));
        System.out.println(scores.get("Cindy"));
        System.out.println(scores.containsKey("Cindy"));
        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }
    }
}
預期輸出
80
95
null
false
Amy=95
Ben=70

教材對照:PDF 第 26–29 頁

11|TreeMap 依 key 排序與頻率統計

TreeMap 依 key 的自然順序或 Comparator 排序,value 不參與排序且可以重複。firstKey/lastKey 取得兩端,headMap/tailMap/subMap 依 key 範圍建立檢視,邊界概念與 TreeSet 相同。以下以字串為 key 記錄出現次數,再按 key 順序輸出。

JAVA 11 · Main.java程式碼
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        TreeMap<String, Integer> counts = new TreeMap<>();
        String[] words = {"java", "python", "java"};
        for (String word : words) {
            counts.put(word, counts.getOrDefault(word, 0) + 1);
        }
        System.out.println(counts);
        System.out.println(counts.firstKey());
        System.out.println(counts.headMap("python"));
        counts.put("empty", null);
        System.out.println(counts.containsKey("empty"));
    }
}
預期輸出
{java=2, python=1}
java
{java=2}
true

教材對照:PDF 第 29–31 頁

WHEN SOMETHING GOES WRONG

遇到錯誤,先檢查這裡

假設 HashSet/HashMap 每次依加入順序輸出

這兩種實作不保證走訪順序;保留插入順序用 LinkedHashSet/LinkedHashMap,依值或 key 排序用 TreeSet/TreeMap。

把泛型寫成 List<int>

泛型參數是參考型別;整數集合使用 List<Integer>,注意 null 拆箱會產生 NullPointerException。

用 PriorityQueue 的 toString 當排序答案

堆積只保證頭部符合優先規則;應持續 poll 取出,才得到依比較規則排列的序列。

PAUSE AND TRY

先不要急著看別人的寫法

  1. 分別用 ArrayList、LinkedHashSet、TreeSet 放入 3、1、3、2,預測內容。
  2. 將佇列中的 peek 改為 poll,下一次取出結果會有什麼變化?
  3. 統計字詞次數時,為什麼需要讀取舊次數再加 1?

先用紙筆預測,再改動範例中的數字或文字。結果與預期不同時,找出最早開始不同的那一步。

把這幾件事帶走

  • 先決定順序、重複、排序與 key 的需求。
  • 集合用泛型清楚表示元素型別,操作前確認是否支援修改。
  • 依正確走訪規則取得資料,別依賴未保證的容器順序。

FROM READING TO PRACTICE

用三道免費題,練習本章觀念

先做簡單題確認語法,再把幾個步驟組合起來。三題都來自本章主題的 Java 免費題庫,難度表示需要組合的基本概念。

教材來源與 Java 11 對照

本章依據《第二部分 05 Java集合架構.pdf》PDF 第 2–31 頁整理,將原有觀念重新編寫為可閱讀與執行的網站教學。頁碼為 PDF 檔案頁次;舊版語法說明與已知誤植依 Java 11 修正,補充內容另有標示。

需要查閱更完整的規則時,可參考以下官方文件。