Home
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

4

Local AI

--

Uedu Code

--

Taipei AQI 57 · Taichung AQI 74 · Tainan AQI 63 · Kaohsiung AQI 64
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 14

泛型

泛型類別與泛型方法、型別參數、萬用字元
Interaction實作 20 minute(s)

Learning Objectives

  • 說明用 Object 存放任意資料時,為什麼需要強制轉型、錯誤要到執行時才會發現
  • 寫出泛型類別(Box<T>、Pair<K, V>),用鑽石運算子 <> 建立物件
  • 寫出泛型方法(static <T> …),讓同一個方法處理不同型別的資料
  • 用有界型別參數 <T extends Comparable<T>> 寫出找最大值的方法
  • 分辨 ? extends 與 ? super 的用途,並說明型別參數只能是參照型別、型別抹除的意義

為什麼需要泛型

第 13 課的 ArrayList<String>、HashMap<String, Integer> 裡,角括號 <…> 就是泛型(generics): 它告訴編譯器「這個集合裡裝的是什麼型別」。泛型在 Java 5 加入語言,在那之前,要寫一個「什麼都能裝」的類別,只能用所有類別的共同祖先 Object:

class ObjectBox {
    private Object value;          // 什麼都能放
    void set(Object value) { this.value = value; }
    Object get() { return value; }
}

ObjectBox box = new ObjectBox();
box.set("Java");
String s = (String) box.get();     // 取出來是 Object,一定要強制轉型

這樣寫有兩個問題:

  1. 每次取出都要強制轉型,程式又長又容易寫錯
  2. 放錯型別,編譯器不會發現。box.set(42) 完全合法,要等到執行 (String) box.get() 才拋出 ClassCastException;如果放和取在程式的兩個角落,錯誤就很難追

泛型讓你在使用類別時說清楚「這個盒子只裝 String」,編譯器就能替你檢查放進去的東西,取出時也不必轉型。 錯誤越早被發現越好:編譯時就被擋下,比程式跑到一半才當掉容易修正得多。

沒有泛型:用 Object 裝東西

注意 get() 回傳的值在變數旁邊顯示成字串或數字,但它的宣告型別是 Object,所以第 18、23 行都要寫 (String) 轉型。第 21 行放進 42,編譯器不會阻止;第 23 行轉型時才拋出 ClassCastException,由 catch 接住。

泛型類別與鑽石運算子

在類別名稱後面加上 <T>,T 就是型別參數(type parameter):寫類別時還不知道是什麼型別,先用 T 代表, 等到使用這個類別時才指定。

class Box<T> {
    private T value;               // T 是什麼,value 就是什麼型別
    Box(T value) { this.value = value; }
    T get() { return value; }
}

Box<String> a = new Box<String>("Amy");   // T = String
Box<Integer> b = new Box<>(20);            // 右邊的 <> 讓編譯器從左邊推斷:T = Integer
String s = a.get();                        // 不用轉型
  • 型別參數的名稱習慣用一個大寫字母:T(type)、E(element,集合的元素)、K/V(key/value)
  • 型別參數可以有好幾個,用逗號分開:class Pair<K, V>
  • new Box<>(20) 的空角括號叫鑽石運算子(diamond operator),從 Java 7 開始可以使用:編譯器能從左邊的宣告推斷型別時,右邊就不必再寫一次
  • 型別參數只能是參照型別:Box<int> 會編譯錯誤,要寫包裝類別 Box<Integer>。放進去與取出來時,int 和 Integer 之間會自動裝箱與拆箱
基本型別 包裝類別
int Integer
double Double
char Character
boolean Boolean
long Long

Box<T> 與 Pair<K, V>

同一個 Box 類別建立了兩個物件:Box<String> 和 Box<Integer>。看第 31、32 行,get() 取出來直接存進 String、int 變數,不需要轉型。框架名稱 Box(T value) 保留了型別參數 T:類別只寫一次,T 在每個物件上代表不同的型別。

想一想:泛型類別與編譯時檢查

1.

下面的程式執行結果是什麼?

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

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("a");
        list.add(5);
        System.out.println(list);
    }
}
2.

下面哪一行可以通過編譯?

3.

下面的程式執行結果是什麼?

public class Main {
    public static void main(String[] args) {
        Object o = "5";
        Integer n = (Integer) o;
        System.out.println(n + 1);
    }
}
第 1 次作答

泛型方法

不只類別,方法也可以有自己的型別參數。在回傳型別的前面寫 <T>,這個 T 只在這個方法裡有效:

static <T> T last(T[] items) {      // <T> 宣告型別參數,後面的 T 是回傳型別
    return items[items.length - 1];
}

String s = last(new String[]{"a", "b"});   // 編譯器推斷 T = String
Integer n = last(new Integer[]{3, 1, 4});   // 編譯器推斷 T = Integer

呼叫泛型方法時通常不必寫出型別,編譯器會從引數推斷 T 是什麼。 注意 T[] 要傳 Integer[],不能傳 int[]:基本型別的陣列不是「Integer 的陣列」。

一個方法處理不同型別的陣列

printAll 被呼叫兩次:第一次的 items 指向 String[],第二次指向 Integer[],同一段程式碼都能處理。第 21、22 行的 last 回傳值不需要轉型就能存進 String、int。

有界型別參數:限制 T 必須會比大小

想寫一個「找最大值」的泛型方法,會遇到一個問題:T 可能是任何型別,編譯器不知道它能不能比大小,所以 x.compareTo(best) 無法編譯。

有界型別參數(bounded type parameter)用 extends 限制 T 的範圍:

static <T extends Comparable<T>> T max(List<T> list) { … }

<T extends Comparable<T>> 的意思是「T 必須實作 Comparable<T> 介面」,也就是 T 一定有 compareTo 方法,可以和另一個 T 比大小。 Integer、Double、String、Character 都實作了 Comparable,可以傳進來;自己寫的類別要實作 Comparable 才行。

a.compareTo(b) 的回傳值:a 比 b 小回傳負數、相等回傳 0、比較大回傳正數。字串依字典順序比較,所以 "pear" 比 "apple" 大。

這裡的 extends 同時涵蓋「繼承類別」和「實作介面」;<T extends Number> 則表示 T 必須是 Number 或它的子類別(Integer、Double…),方法裡就能呼叫 doubleValue()。

找出最大值

max 呼叫兩次,一次 T = Integer、一次 T = String。觀察 best 怎麼隨著迴圈更新:數字比數值大小,字串比字典順序,比較的規則由 compareTo 決定,max 本身不需要知道。

萬用字元:? extends 與 ? super

Integer 是 Number 的子類別,但 List<Integer> 不是 List<Number> 的子型別。如果允許把 List<Integer> 當成 List<Number> 傳進方法, 方法就可以往裡面放一個 Double,原本的 List<Integer> 就被弄壞了。所以這樣寫會編譯錯誤:

static double sum(List<Number> list) { … }
List<Integer> ints = List.of(1, 2);
sum(ints);    // 編譯錯誤:List<Integer> 不能轉成 List<Number>

萬用字元(wildcard)? 讓參數接受一整群型別:

寫法 意思 可以做什麼
List<? extends Number> 元素是 Number 或它的某個子類別 讀出元素當成 Number 使用;不能放入(不知道確切是哪一種)
List<? super Integer> 元素是 Integer 或它的某個父類別 放入 Integer;讀出來只能當 Object
List<?> 任何型別 只讀不寫,讀出來是 Object

一個好記的直覺(常被稱為 PECS:Producer Extends, Consumer Super):參數是提供資料給你讀的,用 extends;是讓你放資料進去的,用 super。 第一次學只要記得這個直覺,看懂函式庫的方法宣告就夠了,例如 Collections.max(Collection<? extends T> coll)。

本站的教學直譯器只檢查泛型常見的型別錯誤。像「對 List<? extends Number> 呼叫 add」在真正的 javac 會編譯錯誤,請以 JDK 的結果為準。

讀用 extends,寫用 super

sum 的參數是 List<? extends Number>,所以 List<Integer> 和 List<Double> 都能傳進去;addOneTwo 的參數是 List<? super Integer>,List<Number> 和 List<Integer> 都能放入整數。看右邊兩個串列最後的內容。(框架名稱把萬用字元簡寫成 List<?>。)

型別抹除:泛型只活在編譯時

Java 的泛型是用型別抹除(type erasure)實作的:編譯器先用型別參數做檢查、在需要的地方自動插入轉型, 接著把型別參數拿掉,沒有限制的 T 換成 Object,有界的 T extends Comparable<T> 換成 Comparable。 所以執行時,ArrayList<String> 和 ArrayList<Integer> 其實是同一個類別,程式執行時並不知道當初寫的是哪一種。

這帶來幾個寫程式時會碰到的限制:

  • 不能寫 new T():執行時不知道 T 是什麼類別,無法建立物件
  • 不能寫 new T[10] 或 new List<String>[10]:陣列執行時要知道元素型別,泛型的型別資訊已經被抹除
  • static 欄位不能使用類別的型別參數:static T value; 編譯錯誤,因為所有 Box<String>、Box<Integer> 共用同一份 static 欄位
  • 基本型別不能當型別參數(List<int>),因為抹除後要換成 Object,而 int 不是物件

型別抹除也是為了相容:Java 5 之前寫的、沒有泛型的程式,在新版 JVM 上仍然可以執行。 沒有寫型別參數的 List list = new ArrayList(); 叫原始型別(raw type),只為了相容舊程式而存在,新程式不要這樣寫,編譯器會給出 unchecked 警告。

想一想:泛型方法與萬用字元

1.

下面的程式會印出什麼?

public class Main {
    static <T extends Comparable<T>> T bigger(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }

    public static void main(String[] args) {
        System.out.println(bigger("cat", "dog") + " " + bigger(3, 12));
    }
}
2.

下面的程式會印出什麼?

import java.util.Arrays;

public class Main {
    static <T> void swap(T[] arr, int i, int j) {
        T tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    public static void main(String[] args) {
        String[] s = {"a", "b", "c"};
        swap(s, 0, 2);
        System.out.println(Arrays.toString(s));
    }
}
3.

方法宣告為 static double sum(List<? extends Number> list),下面哪一種引數可以傳進去?

第 1 次作答

動手寫:對調 Pair 與取最小值

完成兩個泛型方法:swap 把 Pair<A, B> 的兩個值對調,回傳 Pair<B, A>;minOf 用 compareTo 回傳兩個值中比較小的那個。main 已經寫好。

你的輸出
預期輸出

重點回顧

  • 沒有泛型時用 Object 存資料:取出要強制轉型,放錯型別要到執行時才拋出 ClassCastException
  • 泛型類別在名稱後面宣告型別參數:class Box<T>、class Pair<K, V>;使用時指定型別,編譯器負責檢查,取出時不必轉型
  • 鑽石運算子 new Box<>(…)(Java 7 起)讓編譯器從左邊推斷型別參數
  • 型別參數只能是參照型別,基本型別要用包裝類別:List<Integer>,不是 List<int>
  • 泛型方法在回傳型別前面宣告:static <T> T last(T[] items),呼叫時編譯器從引數推斷 T
  • <T extends Comparable<T>> 限制 T 必須能比大小,方法裡才能呼叫 compareTo
  • ? extends X 適合只讀取的參數,? super X 適合要放入資料的參數
  • 型別抹除:泛型的型別資訊只在編譯時使用,所以不能 new T()、不能建立泛型陣列
這一課完成了!
Next lesson:檔案處理
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。