在Java后端開發中,你一定在寫集合類或工具類時,見過 T、E、K、V、? 這樣的泛型通配符。但你是否有過以下疑惑:
- T、E、K、V 到底有什么區別?為什么大家都用這些字母?
- List<?> 和 List 有什么不同?什么時候該用通配符,什么時候該用類型參數?
- 如果不用泛型,代碼也能跑,為什么一定要用泛型?
1. 為什么要用泛型
類型不安全與強制轉換
假設我們要寫一個簡單的盒子類,用來存放物品:
// 沒有泛型的盒子類
public class Box {private Object item; // 只能用Object存儲任何類型public void setItem(Object item) {this.item = item;}public Object getItem() {return item;}
}
使用方式:?
public static void main(String[] args) {Box box = new Box();box.setItem("Hello"); // 存入StringString s = (String) box.getItem(); // 必須強制轉換回Stringbox.setItem(123); // 也可以存入IntegerString i = (String) box.getItem(); // 但這里會拋出ClassCastException!
}
問題:??
- ??類型不安全??:可以存入任何類型(String、Integer等),但取出時容易忘記轉換或轉換錯誤
- ??繁瑣的強制轉換??:每次取出都要手動cast
- ??運行時錯誤??:如果類型轉換錯了,只能在運行時才發現(拋出ClassCastException)
使用泛型后**
// 泛型盒子類
public class Box<T> {private T item; // T是類型參數public void setItem(T item) {this.item = item;}public T getItem() {return item; // 不需要強制轉換}
}public static void main(String[] args) {Box<String> stringBox = new Box<>();stringBox.setItem("Hello");String s = stringBox.getItem(); // 自動就是String類型,無需轉換Box<Integer> intBox = new Box<>();intBox.setItem(123);Integer i = intBox.getItem(); // 自動就是Integer類型stringBox.setItem(123); // 編譯錯誤!不能放入Integer}
2. T、E、K、V、? 的含義
首先,我們要明確一個概念,T
,E
,K
,V
是類型參數(Type Parameter),而?
是通配符(Wildcard)。他們雖然都用在泛型中,但扮演的角色完全不同。Java 官方并沒有強制規定這些字母的含義,只是社區形成了約定俗成的寫法。常見規則如下:
符號 | 常見含義 | 使用場景 |
---|---|---|
T | Type(類型) | 通用類型,最常見 |
E | Element(元素) | 集合中的元素 |
K | Key(鍵) | 映射的鍵(Map) |
V | Value(值) | 映射的值(Map) |
? | 通配符 | 表示未知類型,常用于 API 的參數或返回值 |
2.1 使用 T (Type,任意類型)
示例:API響應包裝器
// 使用 T 定義一個通用的API響應類
public class ApiResponse<T> {private int code;private String message;private T data; // T 代表響應的業務數據類型// 構造方法public ApiResponse(int code, String message, T data) {this.code = code;this.message = message;this.data = data;}// 成功響應的靜態工廠方法public static <T> ApiResponse<T> success(T data) {return new ApiResponse<>(200, "成功", data);}public static ApiResponse<?> error(int code, String message) {return new ApiResponse<>(code, message, null);}// Getter 和 Setterpublic T getData() {return data;}public void setData(T data) {this.data = data;}// ... 其他getter/setter
}// 業務實體
public class User {private Long id;private String name;private String email;// ... 構造方法、getter、setter
}public class Product {private Long id;private String name;private BigDecimal price;// ... 構造方法、getter、setter
}// 在Service層使用
public class UserService {public ApiResponse<User> getUserById(Long id) {User user = userRepository.findById(id);if (user != null) {return ApiResponse.success(user); // T 被推斷為 User} else {return ApiResponse.error(404, "用戶不存在");}}
}public class ProductService {public ApiResponse<List<Product>> getFeaturedProducts() {List<Product> products = productRepository.findFeatured();return ApiResponse.success(products); // T 被推斷為 List<Product>}
}// Controller層調用
@GetMapping("/users/{id}")
public ApiResponse<User> getUser(@PathVariable Long id) {return userService.getUserById(id);// 返回: {"code":200,"message":"成功","data":{"id":1,"name":"張三","email":"zhang@example.com"}}
}@GetMapping("/products/featured")
public ApiResponse<List<Product>> getFeaturedProducts() {return productService.getFeaturedProducts();// 返回: {"code":200,"message":"成功","data":[{"id":101,"name":"手機","price":2999.00}]}
}
2.2 E(Element,集合中的元素)
示例:樹形結構節點
// 通用樹節點(可用于組織架構、分類目錄等)
public class TreeNode<E> {private E data;private List<TreeNode<E>> children;public void addChild(TreeNode<E> child) {if (children == null) children = new ArrayList<>();children.add(child);}
}// 使用示例
TreeNode<String> root = new TreeNode<>();
root.setData("總公司");TreeNode<String> branch1 = new TreeNode<>();
branch1.setData("北京分公司");
root.addChild(branch1);TreeNode<String> branch2 = new TreeNode<>();
branch2.setData("上海分公司");
root.addChild(branch2);
2.3 類型參數 K(Key)和 V(Value)——鍵值對
示例:本地緩存類
// 本地緩存實現
public class LocalCache<K, V> {private Map<K, V> cache = new ConcurrentHashMap<>();private long expireTime;public void put(K key, V value) {cache.put(key, value);}public V get(K key) {return cache.get(key);}
}// 使用示例
LocalCache<Long, User> userCache = new LocalCache<>();
userCache.put(1001L, new User(1001L, "Alice"));LocalCache<String, List<Product>> categoryCache = new LocalCache<>();
categoryCache.put("electronics", Arrays.asList(new Product(...), ...));
2.4 通配符 ? ——處理未知類型
Java 泛型通配符主要有三種形態
1)無界通配符 ?
無界通配符表示可以匹配任何類型,適用于不確定或無關具體類型的情況。
示例:打印任意集合元素
import java.util.*;public class Demo1 {public static void printList(List<?> list) {for (Object element : list) {System.out.println(element);}}public static void main(String[] args) {List<String> names = Arrays.asList("Tom", "Jerry");List<Integer> scores = Arrays.asList(88, 99);printList(names); // 輸出 Tom, JerryprintList(scores); // 輸出 88, 9}
}
特點:
- 可以接收任何類型的 List。
- 只能讀取元素,不能隨意 add。
2)上界通配符 ? extends T
表示“某種類型是 T 或 T 的子類”,適合生產者 / 只讀場景(PECS 原則中的 Producer)。
示例:打印數字列表
import java.util.*;public class Demo1 {public static void printNumbers(List<? extends Number> list) {for (Number n : list) {System.out.println(n);}}public static void main(String[] args) {List<Integer> ints = Arrays.asList(1, 2, 3);List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);printNumbers(ints); // Integer extends NumberprintNumbers(doubles); // Double extends Number}
}
特點:
- 可以讀取元素為 Number 類型。
- 不能寫入 list.add(…),因為不知道具體是 Integer 還是 Double。
3)下界通配符 ? super T
表示“某種類型是 T 或 T 的父類”,適合消費者 / 寫入場景(PECS 原則中的 Consumer)。
示例:向集合中添加數字
import java.util.*;public class Demo3 {public static void addNumbers(List<? super Integer> list) {list.add(10);list.add(20);}public static void main(String[] args) {List<Number> numbers = new ArrayList<>();List<Object> objects = new ArrayList<>();addNumbers(numbers); // Number 是 Integer 的父類addNumbers(objects); // Object 是 Integer 的父類System.out.println(numbers); // 輸出 [10, 20]System.out.println(objects); // 輸出 [10, 20]}
}
特點:
- 可以安全向集合寫入 Integer 類型。
- 讀取出來的元素只能當作 Object,因為類型不確定
總結
通配符 | 含義 | 適用場景 | 示例特點 |
---|---|---|---|
? | 無界 | 泛用工具、只讀 API | 可以讀取,不能寫入,適合日志/導出/打印等 |
? extends T | 上界 | 生產者 / 只讀 | 可以讀取 T 或其子類,不能寫入 |
? super T | 下界 | 消費者 / 寫入 | 可以寫入 T 類型,讀取只能當 Object |
3. 通配符中的PECS原則
PECS 是 Java大師Joshua Bloch 在《Effective Java》里提出的一個泛型使用經驗法則,用來指導我們在選擇通配符時,應該用 extends 還是 super。
- Producer Extends:如果參數是生產者(提供數據給你),就用 ? extends T。
- Consumer Super:如果參數是消費者(你要把數據放進去),就用 ? super T。
簡單一句話:
- 讀(生產者)用 extends,寫(消費者)用 super。
示例 1:Producer(讀數據)
假設我們有個方法,需要從集合里讀取元素:
public static void printNumbers(List<? extends Number> list) {for (Number n : list) {System.out.println(n);}
}
- list 是一個 生產者(提供數字給我們打印),所以用 ? extends Number,允許 List、List 傳進來。
示例 2:Consumer(寫數據)
假設我們有個方法,需要往集合里寫入數據:
public static void addIntegers(List<? super Integer> list) {list.add(1);list.add(2);
}
list 是一個 消費者(我們往里面放 Integer)所以用 ? super Integer,允許 List、List、List 傳進來。
4. 注意事項
- 能用泛型參數就別用 Object,除非你明確就是要“任意類型”,否則優先用泛型。
- 合理選擇通配符
?
, 只讀數據 →? extends T
,只寫數據 →? super T
- 不要濫用泛型,有些場景寫成泛型反而增加理解成本,比如方法內部只操作 String,就直接用 String