CodeGym /課程 /JAVA 25 SELF /泛型(Generics):為什麼需要、基礎語法

泛型(Generics):為什麼需要、基礎語法

JAVA 25 SELF
等級 26 , 課堂 4
開放

1. 「原始」集合(raw types)的問題

稍微回顧一下歷史。在 Java 5 之前,所有集合都是「什麼都收」。它們儲存的是 Object,編譯器不會檢查你實際放了什麼。想放字串?可以。數字?當然可以。貓咪?也行。

// 「原始」集合(raw types)的範例,Java 5 之前
List list = new ArrayList();
list.add("你好");
list.add(42);
list.add(new Object());

問題在於從集合「取出」並使用值時才會暴露:

String s = (String) list.get(0); // OK,這是字串
String s2 = (String) list.get(1); // 砰!ClassCastException

編譯器不發一語,但在執行期你會得到 ClassCastException。這就像一個標著「蘋果」的箱子,裡面卻放著杯子、香蕉和刺蝟。

為什麼這很糟?

  • 錯誤只在執行期才會出現。
  • 型別容易混淆:你得手動把物件轉成需要的型別(cast)。
  • 程式碼較難讀且較危險。

解法 — 泛型(Generics)

Generics(泛型)是一種允許在類別、介面與方法上使用型別參數的機制。也就是你對集合說:「只存放字串」,而編譯器會嚴格遵守這個約束。

List<String> words = new ArrayList<>();
words.add("你好");
words.add("世界");
// words.add(42); // 編譯錯誤!不能把 int 加到 List<String> 中

現在編譯器不會讓你往清單裡放入字串以外的任何東西。錯誤會在程式啟動前就被抓到。

泛型的核心理念:
提供集合(以及更多)在型別上的安全性,讓錯誤在編譯期就能被捕捉,而不是等到執行期。

2. 泛型語法:在程式碼中長什麼樣

在尖括號中指定型別

建立集合時,請在 <> 中指定元素型別:

List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123); // 錯誤:不能把數字加入字串清單

String first = names.get(0); // 不需要轉型!

經典:

  • List<String> — 字串的清單
  • List<Integer> — 整數的清單
  • Set<Double> — 浮點數的集合
  • Map<String, Integer> — 鍵是 String,值是 Integer

為什麼不能只寫 List

可以,但你會失去泛型的所有優點,而且編譯器會給出警告:

List list = new ArrayList(); // raw type — 不建議!
list.add("Hello");
list.add(7.5);
String s = (String) list.get(1); // 你好,ClassCastException!

現代的 Java 程式碼一律使用泛型。

菱形運算子 <>

自 Java 7 起,如果右側的型別可由上下文推斷,就不必再寫:

List<String> list = new ArrayList<>(); // 編譯器會自動推斷這裡是 <String>

3. 泛型在不同集合上的使用

ListSetMap 的範例

List<Integer> numbers = new ArrayList<>();
numbers.add(10);
numbers.add(20);

Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("Alice");
uniqueNames.add("Bob");

Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 23);
ages.put("Bob", 31);

自訂類別的範例

class Student {
    String name;
    int age;
    // ...
}

List<Student> students = new ArrayList<>();
students.add(new Student());

4. 實用細節

泛型的優點

型別安全。 編譯器會檢查集合裡只能放入正確型別的元素。

不再需要轉型。 以前:String s = (String) list.get(0);。現在:String s = list.get(0);

程式碼更易讀、更可靠。 執行期的驚喜更少。

泛型的限制

不能使用基本型別。 泛型只適用於物件,而非基本型別(intdoubleboolean)。請使用包裝類別:IntegerDoubleBoolean

List<Integer> numbers = new ArrayList<>();
numbers.add(10); // int 會自動裝箱為 Integer(autoboxing)

關於型別擦除(type erasure)的簡述

在 Java 中,泛型是透過型別擦除實作的:編譯之後,型別參數的資訊會被移除,JVM 在執行期不知道這原本是 List<String>,而不只是 List。這是為了向後相容。

結果:無法使用帶有具體型別參數的 instanceof 來做檢查。

List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { ... } // 編譯錯誤!

嘗試加入不同型別的元素 — 會導致編譯錯誤

List<String> words = new ArrayList<>();
words.add("Hello");
// words.add(123); // 編譯錯誤:incompatible types: int cannot be converted to String

Map<String, Integer> map = new HashMap<>();
map.put("貓", 5);
// map.put(3, "大象"); // 錯誤:鍵必須是 String,值必須是 Integer

這很棒:錯誤會在編譯階段就被抓到。

不只用在集合

泛型也可以用在你自己的類別與方法中。舉例來說,一個通用的「盒子」:

class Box<T> {
    private T value;

    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> stringBox = new Box<>();
stringBox.set("你好");
System.out.println(stringBox.get());

Box<Integer> intBox = new Box<>();
intBox.set(42);
System.out.println(intBox.get());

在集合中使用泛型是標準配置,但你也會在其他地方遇到,例如 Stream APIOptional

5. 使用泛型時的常見錯誤

錯誤 №1:使用「原始」集合。List list = new ArrayList(); 這樣的寫法會失去型別安全。請一律標註型別參數,例如 List<String>

錯誤 №2:嘗試使用基本型別。 不能寫 List<int>,請使用 List<Integer>

錯誤 №3:從集合讀取時手動轉型。 若你使用了泛型,就不需要像 (String) list.get(i) 這樣的轉型。若非得轉,代表某處破壞了型別一致性。

錯誤 №4:以為型別參數在執行期可用。 由於型別擦除,不能用 instanceof 來檢查像 List<String> 這樣具體的型別參數。

錯誤 №5:在同一個集合裡混用不同型別。 如果宣告為 List<String>,就不要嘗試加入 Integer —— 編譯器不會放行,而這是件好事。

1
問卷/小測驗
集合與 generics,等級 26,課堂 4
未開放
集合與 generics
集合與 generics
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION