泛型編程:讓代碼更安全的類型魔法
你是否遇到過這樣的尷尬:在使用集合時存入了字符串,取出來卻想當作整數使用,結果程序直接崩潰?或者寫了一個排序方法,卻發現只能給整數數組排序,換了字符串數組就得重寫一遍?今天我們要學的泛型編程,就是解決這些問題的"黑科技"!
為什麼需要泛型?從一個崩潰案例説起
先看一段沒有泛型的代碼:
List list = new ArrayList();
list.add("Java");
list.add(21);
String str = (String) list.get(1); // 運行時崩潰!
這段代碼在編譯時完全正常,但運行時會拋出 ClassCastException——我們把整數強行轉換成了字符串!這就是類型轉換不安全的問題。
再看另一個場景:如果要實現一個"交換數組兩個元素"的方法,沒有泛型的話,你可能需要這樣寫:
// 交換整數數組元素
public static void swapInt(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 交換字符串數組元素
public static void swapString(String[] arr, int i, int j) {
String temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// ... 還得為每個類型寫一個!
簡直是重複勞動的噩夢!而泛型能讓你用一套代碼適配所有類型,同時保證類型安全。
泛型基礎:定義與使用
泛型類:讓類成為"類型模板"
最常見的泛型用法是定義泛型類。比如我們熟悉的 ArrayList 就是一個泛型類,它的定義類似這樣:
public class ArrayList<E> {
private E[] elements; // 用泛型變量存儲元素
public boolean add(E e) { // 方法參數使用泛型
// ... 添加元素邏輯
}
public E get(int index) { // 方法返回值使用泛型
return elements[index];
}
}
這裏的 <E> 就是類型參數,你可以把它理解為一個"類型佔位符"。使用時只需指定具體類型:
// 創建存儲字符串的列表,只能添加String類型
List<String> strList = new ArrayList<String>();
strList.add("Java");
strList.add(21); // 編譯報錯!類型不匹配
// 創建存儲整數的列表
List<Integer> intList = new ArrayList<>(); // JDK 7+支持菱形語法,右側類型可省略
intList.add(21);
int num = intList.get(0); // 無需強制類型轉換
泛型接口:定義通用行為
泛型接口與泛型類類似,例如Java集合框架中的 List 接口:
public interface List<E> {
boolean add(E e);
E get(int index);
// ...
}
實現泛型接口時有兩種選擇:
- 指定具體類型:
public class StringList implements List<String> {
// 所有E都替換為String
public boolean add(String e) { ... }
public String get(int index) { ... }
}
- 繼續保留泛型:
public class MyList<E> implements List<E> {
// 保持泛型特性
public boolean add(E e) { ... }
public E get(int index) { ... }
}
泛型方法:單個方法的類型抽象
有時候不需要整個類都是泛型,只需要某個方法是泛型的。比如前面的"交換數組元素"問題,用泛型方法可以一次性解決:
public class ArrayUtils {
// 泛型方法:<T> 聲明類型參數,T作為通用類型
public static <T> void swap(T[] arr, int i, int j) {
if (i < 0 || i >= arr.length || j < 0 || j >= arr.length) {
return; // 邊界檢查
}
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 使用時自動推斷類型
String[] strArr = {"Java", "泛型"};
ArrayUtils.swap(strArr, 0, 1); // 交換字符串數組
Integer[] intArr = {1, 2};
ArrayUtils.swap(intArr, 0, 1); // 交換整數數組
類型通配符:靈活處理未知類型
假設我們要寫一個打印集合元素的方法,該怎麼定義參數類型呢?直接用 List<Object> ?不行,因為 List<String> 並不是 List<Object> 的子類!這時候就需要類型通配符 ?:
// 打印任意類型的集合
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// 可以接收各種類型的List
printList(new ArrayList<String>());
printList(new ArrayList<Integer>());
通配符的邊界限定
有時候我們需要限制未知類型的範圍,比如只允許數字類型:
// 上限限定:? extends Number 表示"Number的子類或本身"
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue();
}
return total;
}
sum(new ArrayList<Integer>()); // 整數列表可以
sum(new ArrayList<Double>()); // 小數列表可以
sum(new ArrayList<String>()); // 字符串列表不行!編譯報錯
還有下限限定 ? super Type,表示"Type的父類或本身",常用於寫入操作:
// 下限限定:只允許添加Integer及其父類(實際中主要是Object)
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(new Object()); // 編譯報錯!Object是Integer的父類但不能添加
}
類型擦除:泛型的"秘密身份"
你知道嗎?Java的泛型其實是"編譯時語法糖",在編譯後會被類型擦除。也就是説,運行時不存在泛型類型!
比如這段代碼:
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // 輸出 true!
編譯後,List<String> 和 List<Integer> 都會被擦除為 List<Object>(如果沒有限定邊界)。這就是為什麼泛型不能用於判斷類型、不能創建泛型數組的原因。
類型擦除示意圖:泛型類 Info<T> 編譯後會被擦除為 Info,T被替換為Object
如果有邊界限定,比如 <T extends Number>,則 T 會被擦除為 Number:
帶邊界的類型擦除:<T extends Number> 會被擦除為 Number
泛型限定對比表:extends vs super
表格
|
限定方式 |
語法 |
含義 |
典型用途 |
|
無限定 |
<?> |
任意類型 |
只讀操作,獲取元素 |
|
上限限定 |
<? extends T> |
只能是T及其子類 |
安全讀取,如求和、查找 |
|
下限限定 |
<? super T> |
只能是T及其父類 |
安全寫入,如添加元素 |
泛型限定示例:使用 <T extends Comparable<T>> 限定類型必須可比較
集合框架中的泛型:實戰案例
Java集合框架大量使用泛型,比如 HashSet、HashMap 等。以 HashMap 為例:
// 創建鍵為String、值為Integer的映射
Map<String, Integer> scoreMap = new HashMap<>();
scoreMap.put("Java", 95);
scoreMap.put("泛型", 90);
// 遍歷Map,鍵值類型明確
for (Map.Entry<String, Integer> entry : scoreMap.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + ": " + value);
}
沒有泛型的話,我們需要不斷進行類型轉換,既麻煩又不安全。
自定義泛型排序器
假設我們要對學生列表按成績排序,用泛型可以寫出通用的排序工具:
// 定義可比較的實體類
class Student implements Comparable<Student> {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
// 實現比較方法
@Override
public int compareTo(Student o) {
return Integer.compare(this.score, o.score);
}
@Override
public String toString() {
return name + "(" + score + ")";
}
}
// 泛型排序工具類
class Sorter<T extends Comparable<T>> {
public void sort(List<T> list) {
// 簡單冒泡排序實現
for (int i = 0; i < list.size() - 1; i++) {
for (int j = 0; j < list.size() - 1 - i; j++) {
if (list.get(j).compareTo(list.get(j + 1)) > 0) {
T temp = list.get(j);
list.set(j, list.get(j + 1));
list.set(j + 1, temp);
}
}
}
}
}
// 使用示例
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("張三", 85));
students.add(new Student("李四", 92));
students.add(new Student("王五", 78));
Sorter<Student> sorter = new Sorter<>();
sorter.sort(students);
System.out.println(students); // 輸出 [王五(78), 張三(85), 李四(92)]
}
這個排序器不僅能給學生排序,只要實現了 Comparable 接口的類型都能用,比如 Integer、String 等!
泛型使用注意事項
- 不能用基本類型作為類型參數
正確:List<Integer>,錯誤:List<int> - 泛型數組創建受限
正確:List<String>[] lists = new List[10],錯誤:List<String>[] lists = new List<String>[10] - 泛型不能用於靜態變量
錯誤:public class MyClass<T> { private static T data; } - 泛型不能用於異常類
錯誤:public class MyException<T> extends Exception { }
總結:泛型讓代碼煥發新生
泛型編程是Java中非常重要的特性,它帶來了三大好處:
- 類型安全:編譯時檢查類型,避免運行時轉換錯誤
- 代碼複用:一套代碼適配多種類型,減少重複勞動
- 可讀性:代碼中直接體現類型信息,更易理解
從今天開始,告別雜亂的類型轉換,用泛型寫出更優雅、更安全的代碼吧!下一章我們將學習集合框架高級應用,看看泛型如何與集合深度結合。
思考題:為什麼 List<?> 不能添加任何元素(除了null)?歡迎在評論區留下你的答案!