National Yang Ming Chiao Tung University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

陽明交大 AQI 69 24°C PM2.5 26
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 13

集合框架

ArrayList、HashMap、HashSet 與走訪
Interaction實作 30 minute(s)

Learning Objectives

  • 說明為什麼需要集合,以及 List、Set、Map 三種介面的差別
  • 用 ArrayList 新增、讀取、修改、刪除元素,分辨 remove(int) 與 remove(Object)
  • 用 HashSet、TreeSet 去除重複,用 HashMap 計數、查詢與走訪
  • 依需求選用 HashMap、TreeMap、LinkedHashMap,並說明 HashMap 不保證順序
  • 寫出 List<Integer> 這類泛型型別,並在走訪時用 Iterator.remove 或 removeIf 安全地移除元素

為什麼需要集合

第 06 課的陣列有一個限制:長度在建立時就固定了。如果你要記錄購物車裡的商品,事先不知道使用者會買幾樣; 陣列開太小會放不下,開太大又浪費,刪掉中間的元素還得自己把後面的往前搬。

Java 的集合框架(Collections Framework)在 java.util 套件裡提供了一整套現成的資料結構,會自動變大變小,常用操作都寫好了。 它們依用途分成三大類,每一類是一個介面(第 11 課),底下有幾種實作:

介面 特性 常用實作 生活例子
List 有順序、可以重複、用索引存取 ArrayList、LinkedList 待辦清單、排隊名單
Set 不放重複的元素 HashSet、TreeSet、LinkedHashSet 已選修的課程代碼、標籤
Map 鍵(key)對應到值(value),鍵不重複 HashMap、TreeMap、LinkedHashMap 學號對應姓名、商品對應價格

List 和 Set 都繼承自 Collection 介面(一群元素);Map 存的是「一對一對」的資料,自成一個介面,不屬於 Collection。

使用前要 import,例如 import java.util.ArrayList;、import java.util.List;(或用 import java.util.*; 一次匯入整個套件)。

角括號:告訴集合要裝什麼

建立集合時,要用角括號寫出它裝的元素型別,這叫做泛型(generics,第 14 課詳細介紹):

List<String> names = new ArrayList<>();         // 裝 String 的串列
Map<String, Integer> price = new HashMap<>();    // 鍵是 String、值是 Integer
  • 右邊的 <> 可以留空,編譯器會依左邊推斷(俗稱「鑽石運算子」)
  • 變數的型別通常寫介面 List,右邊才寫實作 ArrayList。之後想換成 LinkedList,只要改右邊,其他程式都不用動(第 10 課的多型)
  • 寫了型別之後,放錯型別的元素會編譯錯誤,取出來也不必轉型

角括號裡只能放類別,不能放基本型別。 List<int> 無法編譯,要寫包裝類別(wrapper class)List<Integer>:

基本型別 int double char boolean long
包裝類別 Integer Double Character Boolean Long

不用擔心轉換:nums.add(5) 會自動把 int 包成 Integer(自動裝箱,autoboxing),int x = nums.get(0); 會自動拆回 int(自動拆箱)。

ArrayList 的基本操作

看右邊 ArrayList 的格子:每次 add 多一格;第 11 行 add(1, "回信") 插在中間,原本索引 1 以後的元素都往後移一格。字串的值直接顯示在格子裡。

remove 的陷阱、排序與不能修改的串列

ArrayList 常用方法:

方法 作用
add(e)/add(i, e) 加到最後面/插在索引 i
get(i)/set(i, e) 讀取/替換索引 i 的元素
remove(i)/remove(obj) 移除索引 i 的元素/移除第一個等於 obj 的元素
size()、isEmpty()、clear() 個數、是否為空、清空
contains(obj)、indexOf(obj) 是否包含、第一次出現的索引(找不到為 -1)

remove 有兩個多載:remove(int index) 依索引移除,remove(Object o) 依值移除。List<String> 不會搞混, 但 List<Integer> 呼叫 nums.remove(1) 時,1 是 int,編譯器選的是 remove(int),移除的是索引 1,不是值 1(第 10 課:多載看引數的型別)。 要依值移除,寫 nums.remove(Integer.valueOf(1))。

排序:Collections.sort(list) 由小到大排序(元素要能比較大小,第 11 課的 Comparable)。Collections 類別還有 reverse、max、min、frequency 等工具方法。

不能修改的串列:List.of(1, 2, 3)(Java 9 起)可以快速建立串列,但它不能修改,呼叫 add、remove、set 會拋出 UnsupportedOperationException。 需要修改時,把它複製成新的 ArrayList:new ArrayList<>(List.of(1, 2, 3))。

ArrayList 與 LinkedList:兩者都實作 List,用法幾乎一樣,差在內部結構。ArrayList 內部是陣列,get(i) 直接算出位置,很快; 但在前面插入或刪除時,後面的元素都要搬動。LinkedList 是一個接一個串起來的節點,在頭尾插入刪除很快,但 get(i) 要從頭一個一個數過去。 大多數情況用 ArrayList 就對了。

remove(int) 與 remove(Object)

串列一開始是 [30, 10, 20, 10]。第 9 行 remove(1) 移除的是索引 1(第一個 10);第 11 行 remove(Integer.valueOf(10)) 移除的才是值為 10 的元素。

想一想:List

1.

下面的程式會印出什麼?

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
        list.remove(1);
        list.remove(Integer.valueOf(1));
        System.out.println(list);
    }
}
2.

下面的程式能不能編譯?

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<int> nums = new ArrayList<>();
        nums.add(3);
        System.out.println(nums);
    }
}
3.

下面的程式會怎樣?

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> week = List.of("一", "二", "三");
        week.set(0, "日");
        System.out.println(week);
    }
}
第 1 次作答

Set:不重複的元素

Set 不放重複的元素:add 一個已經存在的元素,集合不會改變,add 回傳 false。它沒有索引,不能 get(i),常用的是 add、remove、contains、size 與 for-each 走訪。

實作 走訪的順序 說明
HashSet 不保證 用雜湊(hash)安排位置,contains 非常快
TreeSet 由小到大排序 元素要能比較大小
LinkedHashSet 加入的順序 去除重複,又保留原本的順序

HashSet 的「不保證順序」是指:元素的排列由雜湊值決定,跟加入的順序、大小都沒有關係,換一個 Java 版本也可能改變。 程式不應該依賴 HashSet、HashMap 走訪出來的順序;需要固定的順序時,改用 TreeSet 或 LinkedHashSet。

去除重複的標籤

同一組標籤分別加進三種 Set。重複的 "java"、"ai" 第二次加入時 add 回傳 false,三個集合都不會變大。最後比較 TreeSet(排序)與 LinkedHashSet(加入的順序)印出來的樣子。

Map:鍵與值

Map 存的是「鍵 → 值」的對應,像字典用單字查解釋。鍵不能重複,值可以重複。

方法 作用
put(k, v) 放入一組;鍵已經存在時,用新的值覆蓋舊的值
get(k) 取得鍵對應的值;鍵不存在時回傳 null
getOrDefault(k, d) 鍵不存在時回傳預設值 d
containsKey(k)、remove(k)、size() 是否有這個鍵、移除、組數
merge(k, 1, Integer::sum) 鍵不存在就放 1,存在就把舊值加 1:計數的好幫手
keySet()、values()、entrySet() 所有鍵、所有值、所有「鍵值對」

Integer::sum 是方法參照,意思是「用 Integer.sum(舊值, 1) 算出新值」,第 17 課會介紹。不用 merge 的寫法是 count.put(v, count.getOrDefault(v, 0) + 1);。

走訪 Map 最常用 entrySet(),每一個 Map.Entry 有 getKey() 與 getValue():

for (Map.Entry<String, Integer> e : count.entrySet()) {
    System.out.println(e.getKey() + "=" + e.getValue());
}

三種實作的差別和 Set 一樣:HashMap 不保證順序、TreeMap 依鍵排序、LinkedHashMap 保留放入的順序。 (小心:int x = map.get("沒有的鍵"); 會因為 null 無法拆箱成 int 而拋出 NullPointerException,不確定鍵在不在時用 getOrDefault。)

投票計數

右邊的 HashMap 以「鍵 = 值」表格顯示。迴圈每一圈用 merge 替一個飲料加 1 票,看表格裡的數字變化。第 13 行印出的順序是 HashMap 依雜湊安排的,不是投票順序,也不是排序結果。

想一想:Set 與 Map

1.

下面的程式會印出什麼?

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Set<String> t = new TreeSet<>(List.of("b", "a", "c", "a"));
        System.out.println(t.size() + " " + t);
    }
}
2.

下面的程式會印出什麼?

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> m = new LinkedHashMap<>();
        m.put("b", 1);
        m.put("a", 2);
        m.put("b", 3);
        System.out.println(m);
    }
}
3.

下面的程式會印出什麼?

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> stock = new TreeMap<>();
        stock.put("pen", 5);
        stock.merge("pen", 3, Integer::sum);
        stock.merge("cup", 2, Integer::sum);
        System.out.println(stock.getOrDefault("box", 0) + " " + stock);
    }
}
4.

要記錄「每位學生(學號)選了哪些課」,同一門課不能重複,而且印出時課程代碼要排好順序。哪個型別最合適?

第 1 次作答

走訪時移除元素

用 for-each 走訪串列時,不能直接呼叫串列的 remove 或 add。for-each 其實是靠一個 Iterator(走訪器)一格一格往前走, 串列在走訪途中被改變,Iterator 下一次取元素時會發現「串列被別人動過了」,拋出 ConcurrentModificationException。 (有時候剛好沒有拋出,例如移除的是倒數第二個元素,但那只是碰巧,結果也可能少檢查到元素。)

正確的做法有兩種:

Iterator<Integer> it = scores.iterator();
while (it.hasNext()) {          // 還有下一個嗎?
    int s = it.next();          // 取出下一個
    if (s < 60) it.remove();    // 用 Iterator 自己的 remove,移除剛剛取出的那一個
}

scores.removeIf(s -> s < 60);   // 更簡潔:移除所有符合條件的元素

s -> s < 60 是 Lambda 表示式,意思是「給我一個 s,回傳 s 是否小於 60」,第 17 課會詳細介紹。

安全地移除不及格的分數

第一段在 for-each 裡移除 45,下一圈取元素時拋出 ConcurrentModificationException,30 也沒有被檢查到。第二段改用 Iterator:next() 取出元素,it.remove() 移除它,走訪可以正常完成。

動手寫:統計單字出現次數

把一句英文拆成單字,用 TreeMap 統計每個單字出現幾次,依字母順序印出「單字=次數」;最後再印出有幾個不同的單字。

你的輸出
預期輸出

重點回顧

  • 集合會自動調整大小;List 有順序可重複、Set 不重複、Map 存鍵值對且鍵不重複
  • 角括號寫元素型別,只能放類別:List<Integer> 不能寫成 List<int>;基本型別與包裝類別之間會自動裝箱、拆箱
  • ArrayList:add、get、set、remove、size、contains、indexOf;List<Integer> 的 remove(1) 是移除索引 1,依值移除要寫 remove(Integer.valueOf(1))
  • Collections.sort 排序;List.of 建立的串列不能修改,要修改就複製成 new ArrayList<>(…)
  • HashSet/HashMap 不保證順序,TreeSet/TreeMap 自動排序,LinkedHashSet/LinkedHashMap 保留加入的順序
  • Map 用 put(同鍵覆蓋)、get、getOrDefault、containsKey、merge 計數,用 entrySet() 走訪
  • 走訪時不要直接修改集合,否則會拋出 ConcurrentModificationException;改用 Iterator.remove() 或 removeIf
這一課完成了!
Next lesson:泛型
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。