Java 泛型(Generics)是 Java SE 5.0 中引入的一項重要特性,它允許在定義類、接口和方法時使用類型參數。泛型的引入旨在提高代碼的類型安全性、可讀性和複用性,同時消除強制類型轉換的麻煩,並在編譯時捕獲更多錯誤。
1. 為什麼需要泛型?
在泛型出現之前,Java 使用 Object 類型來處理不同類型的數據,但這帶來了兩個主要問題:
- 類型不安全: 在編譯時無法檢查類型,運行時可能出現 ClassCastException。
- 強制類型轉換: 每次從集合中取出元素時都需要進行強制類型轉換,代碼冗餘且易錯。
考慮一個簡單的例子,一個存儲整數的列表:
code Java
downloadcontent_copy
expand_less
import java.util.ArrayList;
import java.util.List;
public class LegacyListExample {
public static void main(String[] args) {
List list = new ArrayList(); // 沒有指定類型
list.add(10);
list.add("Hello"); // 理論上這裏可以添加任何類型
Integer num = (Integer) list.get(0); // 需要強制類型轉換
// String str = (String) list.get(1); // 如果這裏是Integer,就會在運行時出錯
System.out.println("Number: " + num);
// 如果不小心取出了錯誤類型,運行時會拋出 ClassCastException
// Integer wrongNum = (Integer) list.get(1); // 運行時錯誤!
}
}
這段代碼在編譯時不會報錯,但如果嘗試將 list.get(1) 轉換為 Integer,就會在運行時拋出 ClassCastException。
2. 泛型的基本語法
泛型的核心思想是允許類型作為參數傳遞。我們用尖括號 <> 來定義類型參數。
2.1 泛型類
定義一個泛型類,可以在類名後面加上 <T>,其中 T 是類型參數的佔位符,可以代表任何類型。
code Java
downloadcontent_copy
expand_less
// 泛型類示例:一個簡單的盒子,可以存放任何類型的數據
public class Box<T> {
private T content; // content 的類型由 T 決定
public Box(T content) {
this.content = content;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
public static void main(String[] args) {
// 創建一個存放 Integer 類型的 Box
Box<Integer> integerBox = new Box<>(123);
System.out.println("Integer Box Content: " + integerBox.getContent()); // 無需強制轉換
// 創建一個存放 String 類型的 Box
Box<String> stringBox = new Box<>("Hello Generics");
System.out.println("String Box Content: " + stringBox.getContent()); // 無需強制轉換
// 編譯時類型檢查:
// integerBox.setContent("World"); // 編譯錯誤:不兼容的類型
}
}
使用泛型後,Box<Integer> 只能存放 Integer 類型或其子類,Box<String> 只能存放 String 類型或其子類。這大大提高了代碼的類型安全性。
2.2 泛型接口
泛型接口的定義與泛型類類似,在接口名後加上類型參數。
code Java
downloadcontent_copy
expand_less
// 泛型接口示例:一個數據轉換器
public interface Converter<S, T> {
T convert(S source);
}
// 實現泛型接口:將 String 轉換為 Integer
class StringToIntegerConverter implements Converter<String, Integer> {
@Override
public Integer convert(String source) {
return Integer.parseInt(source);
}
public static void main(String[] args) {
Converter<String, Integer> converter = new StringToIntegerConverter();
Integer result = converter.convert("456");
System.out.println("Converted Integer: " + result);
// Converter<Integer, String> anotherConverter = new StringToIntegerConverter(); // 編譯錯誤
}
}
2.3 泛型方法
泛型方法可以在普通類或泛型類中定義。它的類型參數是在方法聲明中定義的,獨立於類的類型參數。
code Java
downloadcontent_copy
expand_less
public class GenericMethodExample {
// 泛型方法:打印數組中的所有元素
public static <E> void printArray(E[] inputArray) {
for (E element : inputArray) {
System.out.printf("%s ", element);
}
System.out.println();
}
// 泛型方法:獲取兩個對象中較大的一個(需要實現 Comparable 接口)
public static <T extends Comparable<T>> T maximum(T x, T y) {
T max = x;
if (y.compareTo(max) > 0) {
max = y;
}
return max;
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
Double[] doubleArray = {1.1, 2.2, 3.3, 4.4};
Character[] charArray = {'H', 'E', 'L', 'L', 'O'};
System.out.print("Integer Array: ");
printArray(intArray); // 編譯器會自動推斷 E 為 Integer
System.out.print("Double Array: ");
printArray(doubleArray); // 編譯器會自動推斷 E 為 Double
System.out.print("Character Array: ");
printArray(charArray); // 編譯器會自動推斷 E 為 Character
System.out.println("Max of (3, 5): " + maximum(3, 5));
System.out.println("Max of (apple, banana): " + maximum("apple", "banana"));
}
}
在 printArray 方法中,類型參數 <E> 在 void 之前定義。在使用時,編譯器會根據傳入的參數自動推斷 E 的具體類型。
3. 類型通配符
類型通配符 (?) 提供了更大的靈活性,它代表未知類型。
3.1 上界通配符 (? extends T)
限制類型只能是 T 或 T 的子類。這表示我們可以從結構中讀取 T 類型的元素,但不能往裏寫入(除了 null)。
code Java
downloadcontent_copy
expand_less
import java.util.ArrayList;
import java.util.List;
public class UpperBoundedWildcard {
// 接收任何 List,只要其元素是 Number 或 Number 的子類
public static void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
// list.add(new Integer(10)); // 編譯錯誤!不能添加元素(除了null)
}
public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
printNumbers(integers);
List<Double> doubles = new ArrayList<>();
doubles.add(3.14);
doubles.add(2.71);
printNumbers(doubles);
// List<String> strings = new ArrayList<>();
// strings.add("hello");
// printNumbers(strings); // 編譯錯誤:String 不是 Number 的子類
}
}
上界通配符主要用於讀取數據,遵循 PECS 原則中的 Producer-Extends(生產者使用 extends)。
3.2 下界通配符 (? super T)
限制類型只能是 T 或 T 的父類。這表示我們可以往結構中寫入 T 類型的元素(或 T 的子類),但從結構中讀取時,只能保證是 Object 類型。
code Java
downloadcontent_copy
expand_less
import java.util.ArrayList;
import java.util.List;
public class LowerBoundedWildcard {
// 接收任何 List,只要其元素是 Integer 或 Integer 的父類
public static void addIntegers(List<? super Integer> list) {
list.add(10); // 可以添加 Integer 及其子類
list.add(20);
// Integer i = list.get(0); // 編譯錯誤!只能保證是 Object
Object obj = list.get(0); // 讀取時只能作為 Object
System.out.println("Added to list. First element (as Object): " + obj);
}
public static void main(String[] args) {
List<Number> numbers = new ArrayList<>();
addIntegers(numbers); // 可以將 Integer 添加到 List<Number>
System.out.println("Numbers list: " + numbers);
List<Object> objects = new ArrayList<>();
addIntegers(objects); // 可以將 Integer 添加到 List<Object>
System.out.println("Objects list: " + objects);
// List<Double> doubles = new ArrayList<>();
// addIntegers(doubles); // 編譯錯誤:Double 不是 Integer 的父類
}
}
下界通配符主要用於寫入數據,遵循 PECS 原則中的 Consumer-Super(消費者使用 super)。
3.3 無界通配符 (<?>)
<?> 表示可以匹配任何類型。它常用於不知道集合中具體類型,但又需要操作集合的情況。
code Java
downloadcontent_copy
expand_less
import java.util.ArrayList;
import java.util.List;
public class UnboundedWildcard {
// 打印任何類型的列表
public static void printList(List<?> list) {
for (Object o : list) { // 只能作為 Object 讀取
System.out.println(o);
}
// list.add("test"); // 編譯錯誤!不能添加元素(除了null)
}
public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
integers.add(10);
integers.add(20);
printList(integers);
List<String> strings = new ArrayList<>();
strings.add("Apple");
strings.add("Banana");
printList(strings);
}
}
無界通配符也遵循 PECS 原則,因為它既不能安全地添加元素,也無法安全地以具體類型讀取,因此主要用於通用操作,例如 list.size()、list.clear() 等,或者在處理不知道具體類型的集合時。
4. 泛型擦除 (Type Erasure)
Java 泛型是偽泛型,這意味着泛型信息只存在於編譯時期,在運行時會被擦除。在編譯後,所有的泛型類型都會被替換為它們的上界(如果沒有指定上界,則替換為 Object)。
code Java
downloadcontent_copy
expand_less
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class TypeErasureExample {
public static void main(String[] args) throws Exception {
List<Integer> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // 輸出 true
// 通過反射,可以繞過泛型類型檢查
list1.add(1);
Method addMethod = list1.getClass().getMethod("add", Object.class);
addMethod.invoke(list1, "Hello World"); // 成功添加 String 類型到 List<Integer>
System.out.println(list1); // 輸出: [1, Hello World]
// Integer i = list1.get(1); // 運行時會拋出 ClassCastException
}
}
從上面的例子可以看出,List<Integer> 和 List<String> 在運行時實際上是同一個類型 List。泛型擦除是 Java 為了兼容早期版本而做出的設計選擇,但也帶來了一些限制:
- 無法在運行時獲取泛型類型信息: instanceof 和 new T() 是不允許的。
- 泛型數組創建限制: new T[size] 是不允許的。
- 原始類型與泛型類型的兼容性問題。
5. 泛型中的一些最佳實踐和常見問題
- PECS 原則:
- Producer Extends(生產者使用 extends):如果你需要從泛型集合中讀取數據,使用 <? extends T>。
- Consumer Super(消費者使用 super):如果你需要向泛型集合中寫入數據,使用 <? super T>。
- 如果既要讀又要寫,那麼就不要使用通配符,直接使用確切的類型 T。
- 泛型與數組: 由於泛型擦除,不能直接創建泛型數組(如 new T[size])。如果確實需要,可以創建 Object 數組然後進行強制類型轉換,或者使用 ArrayList 等泛型集合。
- 泛型和基本數據類型: 泛型類型參數不能是基本數據類型(如 int, char, boolean),必須是它們的包裝類(Integer, Character, Boolean 等)。這是因為泛型在內部是通過 Object 進行操作的,而基本數據類型不是 Object 的子類。
- 自定義泛型類型參數命名規範:
- E - Element (在集合中使用,如 List<E>)
- K - Key (鍵)
- V - Value (值)
- N - Number (數字類型)
- T - Type (任意類型)
- S, U, V - 第二、第三、第四個類型
- 靜態方法中的泛型: 靜態方法不能直接使用類的泛型類型參數。如果靜態方法需要使用泛型,必須自己聲明為泛型方法。 code Java downloadcontent_copy expand_less
public class StaticGenericExample<T> {
// public static T getSomething(T obj) { // 編譯錯誤!靜態方法不能使用類T
// return obj;
// }
public static <U> U getSomethingStatic(U obj) { // 正確的泛型靜態方法
return obj;
}
public static void main(String[] args) {
System.out.println(getSomethingStatic("Hello"));
}
}
6. 總結
Java 泛型是現代 Java 編程中不可或缺的一部分,它通過在編譯時進行類型檢查,有效地解決了早期 Java 集合的類型安全問題,並減少了運行時錯誤。理解泛型的基本語法、通配符的使用(特別是 PECS 原則)以及泛型擦除的原理,對於編寫健壯、可維護和高效的 Java 代碼至關重要。