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. 泛型在不同集合上的使用
List、Set、Map 的範例
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);。
程式碼更易讀、更可靠。 執行期的驚喜更少。
泛型的限制
不能使用基本型別。 泛型只適用於物件,而非基本型別(int、double、boolean)。請使用包裝類別:Integer、Double、Boolean。
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 API 與 Optional。
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 —— 編譯器不會放行,而這是件好事。
GO TO FULL VERSION