Java基礎:泛型機制
1. 簡介
泛型(Generics)是 Java 程式語言中的一個重要特性,允許在定義類別、介面和方法時使用類型參數。也就是說,泛型就是參數化類型,使得程式碼可以適用於多種資料類型,而不需要對每種類型都寫一次。
泛型的重要性體現在以下幾個方面:
-
類型安全:泛型在編譯時提供類型檢查,減少運行時錯誤的可能性。
-
程式碼重用:通過使用泛型,可以編寫出更通用、更靈活的程式碼,適用於多種資料類型。
-
性能提升:泛型消除許多顯式類型轉換的需要,提高程式的執行效率。
-
可讀性增強:泛型使得程式碼意圖更加明確,提高程式碼的可讀性和可維護性。
-
API 設計:泛型為庫設計者提供更強大的工具,使得 API 更加靈活和易於使用。
2. 泛型的基本概念
泛型的核心思想是將類型參數化。在 Java 中,泛型主要應用於類別、介面和方法。讓我們來解泛型的基本概念:
泛型類別
泛型類別是在類別名稱後使用尖括號 <>
來定義一個或多個類型參數的類別。例如:
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
在這個例子中,T
是類型參數,可以在創建 Box
物件時指定具體的類型。
泛型方法
泛型方法是在返回類型前使用 <>
聲明類型參數的方法。例如:
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.print(element + " ");
}
System.out.println();
}
這個方法可以印出任何類型的陣列。
類型參數命名慣例
雖然可以使用任何有效的標識符作為類型參數名,但通常遵循以下慣例:
- E - Element(常用於集合)
- T - Type
- K - Key
- V - Value
- N - Number
- S, U, V 等 - 第 2、3、4 個類型參數
這些命名慣例有助於提高程式碼的可讀性,特別是在處理多個類型參數時。
3. 泛型的優點
泛型為 Java 程式設計帶來許多顯著的優點,使得程式碼更加安全、高效和可重用。以下是泛型的主要優點:
類型安全
泛型提供編譯時的類型檢查,這意味著許多錯誤可以在編譯階段就被發現,而不是在運行時才出現,換言之,減少運行時錯誤,提高程式的穩定性。例如:
消除類型轉換
在使用泛型之前,從集合中取出元素時常常需要進行顯式的類型轉換。泛型消除這種需要:
// 不使用泛型
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 需要類型轉換
// 使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 不需要類型轉換
可使程式碼更加簡潔,也消除因類型轉換錯誤而可能產生的 ClassCastException。
程式碼重用
泛型允許我們編寫更通用的程式碼,可以適用於多種類型,提高程式碼的重用性:
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// getters and setters
}
Pair
類別可以用於任何類型的鍵值對,無需為每種類型組合都創建一個新的類別。
4. 泛型的使用
泛型在 Java 中有廣泛的應用,特別是在集合框架中。讓我們來看看泛型的幾種常見使用方式:
在集合中使用泛型
Java 集合框架大量使用泛型,這使得集合的使用更加安全和方便:
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
// names.add(123); // 編譯錯誤
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
自定義泛型類別
我們可以創建自己的泛型類別,以增加程式碼的靈活性:
public class Pair<T, U> {
private T first;
private U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() { return first; }
public U getSecond() { return second; }
}
// 使用
Pair<String, Integer> pair = new Pair<>("Hello", 42);
String first = pair.getFirst(); // "Hello"
int second = pair.getSecond(); // 42
泛型方法的實現
泛型方法可以獨立於類別而存在:
public class Utilities {
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static <T extends Comparable<T>> T findMax(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
// 使用
Integer[] numbers = {1, 2, 3, 4, 5};
Utilities.swap(numbers, 0, 4); // 交換第一個和最後一個元素
String max = Utilities.findMax("apple", "banana"); // 返回 "banana"
例子中,swap
方法可以交換任何類型陣列的元素,而 findMax
方法可以比較任何實現 Comparable
介面的類型。
5. 泛型的限制
雖然泛型為 Java 程式設計帶來許多好處,但也有一些限制,以下是泛型的主要限制:
類型擦除
Java 的泛型是通過類型擦除(Type Erasure)實現的。這意味著泛型資訊只在編譯時存在,運行時會被擦除。例如:
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // 輸出:true
這兩個 List 在運行時實際上是相同的類型。這種機制保證向後兼容性,但也帶來一些限制。
不能創建泛型陣列
由於類型擦除,不能直接創建泛型類型的陣列。例如:
// 這是不合法的
T[] array = new T[10]; // 編譯錯誤
// 但可以這樣做
T[] array = (T[]) new Object[10]; // 需要類型轉換,可能產生 ClassCastException
這個限制是因為陣列需要在運行時知道確切的元素類型,而泛型資訊在運行時已經被擦除。
不能使用基本類型作為類型參數
泛型不能使用基本數據類型(如 int, double, char 等)作為類型參數。必須使用對應的包裝類(如 Integer, Double, Character 等)。
// 不合法
List<int> numbers = new ArrayList<>(); // 編譯錯誤
// 正確的做法
List<Integer> numbers = new ArrayList<>();
泛型類型的靜態成員限制
泛型類型的靜態成員不能使用類的類型參數。例如:
public class GenericClass<T> {
private static T staticMember; // 編譯錯誤
public static T getStaticMember() { // 編譯錯誤
return null;
}
}
這是因為靜態成員屬於類本身,而不是類的實例,在類加載時就已經初始化,此時類型參數還未確定。
6. 泛型萬用字元
泛型萬用字元是 Java 泛型中的一個重要概念,提供更大的靈活性,特別是在處理不同但相關的泛型類型時。Java 中有三種主要的萬用字元:無界萬用字元、上界萬用字元和下界萬用字元。
無界萬用字元 (?)
無界萬用字元用問號 ?
表示,代表任何類型。當你只關心操作而不關心具體類型時,可以使用無界萬用字元。
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
}
// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = Arrays.asList("Hello", "World");
printList(intList); // 輸出:1 2 3
printList(strList); // 輸出:Hello World
上界萬用字元 (? extends T)
上界萬用字元限制未知類型必須是指定類型 T 或其子類型。這在讀取具體元素時很有用。
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sumOfList(intList)); // 輸出:6.0
System.out.println(sumOfList(doubleList)); // 輸出:6.6
下界萬用字元 (? super T)
下界萬用字元的限制是,未知類型必須是指定類型 T 或其父類型 ->這在寫入元素時很有用。
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
// 使用
List<Number> numberList = new ArrayList<>();
addNumbers(numberList);
System.out.println(numberList); // 輸出:[1, 2, 3, 4, 5]
萬用字元的使用可以增加程式碼的靈活性,但也需要注意:
- 使用上界萬用字元 (? extends T
) 的集合是只讀的,你不能添加元素到這樣的集合中。
- 使用下界萬用字元 (? super T
) 的集合允許添加元素,但從中讀取元素時只能當作 Object 類型。
7. 泛型和繼承
泛型類別的繼承
當涉及到泛型類別的繼承時,需要注意以下幾點:
- 泛型類別可以擴展其他泛型類別或非泛型類別。
- 子類別可以添加自己的類型參數。
例如:
class GenericParent<T> {
T value;
// ...
}
class GenericChild<T, U> extends GenericParent<T> {
U anotherValue;
// ...
}
簡單來說,雖然 Integer
是 Number
的子類,但 GenericParent<Integer>
並不是 GenericParent<Number>
的子類。這種情況稱為泛型不變性(invariance),也就是說,泛型類別的類型參數不會自動轉換。
泛型方法的覆寫
當覆寫泛型類別中的方法時,方法簽名必須完全匹配。例如:
class Animal {
public <T> void feed(T food) {
// ...
}
}
class Dog extends Animal {
@Override
public <T> void feed(T food) {
// 正確的覆寫
}
}
如果嘗試改變類型參數,編譯器會報錯:
使用萬用字元處理繼承關係
為處理泛型和繼承之間的關係,我們經常需要使用萬用字元:
List<? extends Number> numbers = new ArrayList<Integer>();
// 可以讀取,但不能添加元素(除 null)
Number n = numbers.get(0); // 可以
// numbers.add(Integer.valueOf(1)); // 編譯錯誤
List<? super Integer> integers = new ArrayList<Number>();
// 可以添加 Integer 或其子類型,但讀取時只能當作 Object
integers.add(Integer.valueOf(1)); // 可以
// Integer i = integers.get(0); // 編譯錯誤
Object obj = integers.get(0); // 可以
8. 泛型的實踐
以下是一些重要的泛型使用建議:
何時使用泛型
- 集合類:幾乎總是應該使用泛型來參數化集合類。
- 通用算法:當你的方法可以操作多種類型時,考慮使用泛型。
- 類型安全:當你需要在編譯時捕獲類型錯誤時。
泛型命名規範
遵循標準的泛型命名慣例可以提高程式碼的可讀性:
- E - Element(常用於集合)
- T - Type
- K - Key
- V - Value
- N - Number
- S, U, V 等 - 第 2、3、4 個類型參數
避免過度使用泛型
雖然泛型很強大,但過度使用可能會使程式碼變得複雜難懂。只在真正需要的地方使用泛型。
優先使用 List 而不是 T[]
由於泛型陣列創建的限制,通常建議使用 List<T>
而不是 T[]
。
// 避免這樣做
T[] array = (T[]) new Object[10]; // 可能導致 ClassCastException
// 推薦這樣做
List<T> list = new ArrayList<>(10);
使用菱形運算符
在 Java 7 及以後版本中,使用菱形運算符 <>
可以簡化泛型程式碼:
明智地使用萬用字元
- 使用
? extends T
當你只需要從結構中讀取。 - 使用
? super T
當你只需要寫入結構。 - 使用無界萬用字元
?
當你既不需要讀也不需要寫具體類型。
考慮泛型方法
如果只有方法需要類型參數,使用泛型方法而不是使整個類泛型化。