Java 泛型语法系统讲解
从基础到高级,结合大量代码示例
牢记 PECS 原则与类型擦除机制1. 什么是泛型?
为了更通用:使用 Object
class Box {
private Object item;
public void set(Object item) { this.item = item; }
public Object get() { return item; }
}
Box box = new Box();
box.set("Hello");
// String text = (String) box.get(); // 需要强制转换,且可能 ClassCastException
更好的通用:使用类型参数 T
class Box<T> {
private T item;
public void set(T item) { this.item = item; }
public T get() { return item; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
// stringBox.set(123); // 编译错误:类型不匹配
String text = stringBox.get(); // 无需强制转换
📝 编程练习
设计一个泛型类 Container<T>,它可以存储任意类型的单个对象。
要求:
- 提供构造方法来初始化容器
- 提供
get()和set()方法 - 提供
isEmpty()方法判断是否为空
public class Container<T> {
private T item;
// 构造方法
public Container() {
this.item = null;
}
public Container(T item) {
this.item = item;
}
// 获取存储的对象
public T get() {
return item;
}
// 设置存储的对象
public void set(T item) {
this.item = item;
}
// 判断是否为空
public boolean isEmpty() {
return item == null;
}
// 使用示例
public static void main(String[] args) {
// 存储字符串
Container<String> stringContainer = new Container<>("Hello");
System.out.println(stringContainer.get()); // 输出: Hello
// 存储整数
Container<Integer> intContainer = new Container<>();
intContainer.set(42);
System.out.println(intContainer.get()); // 输出: 42
}
}
2.1 泛型类
当我们描述 Stack 数据结构时,并不限定其是 Integer 还是 Double。更重要的是元素在 Stack 中的活动规律。
难道需要为 int 创建一个 IntStack 类,为 double 创建一个 DoubleStack 类吗?显然答案是否定的。
所以,最好定义一套针对任意类型都适用的“模板”,这个任意的类型我们暂时用 T 来指代。
在类名后用 <T> 声明我们要定义的类对任意类型T都适用,即是定义了泛型类。
- 命名约定:T(Type)、E(Element)、K/V(Key/Value)
- 设计意图:在类的声明处绑定能力与约束,避免运行期错误
- 实践建议:优先只读暴露,减少可变性带来的复杂度
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) { this.key = key; this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
// 右侧原来应该是Pair<Integer, String>, 可以省略为Pair<>
Pair<Integer, String> pair = new Pair<>(1, "One");
Integer id = pair.getKey();
String name = pair.getValue();
📝 编程练习
设计一个泛型类 Triple<T, U, V>,可以存储三个不同类型的值。
要求:
- 提供构造方法初始化三个值
- 提供
getFirst()、getSecond()、getThird()方法 - 提供
setFirst()、setSecond()、setThird()方法
public class Triple<T, U, V> {
private T first;
private U second;
private V third;
// 构造方法
public Triple(T first, U second, V third) {
this.first = first;
this.second = second;
this.third = third;
}
// Getter方法
public T getFirst() { return first; }
public U getSecond() { return second; }
public V getThird() { return third; }
// Setter方法
public void setFirst(T first) { this.first = first; }
public void setSecond(U second) { this.second = second; }
public void setThird(V third) { this.third = third; }
// 使用示例
public static void main(String[] args) {
// 存储ID、姓名、分数
Triple<Integer, String, Double> student =
new Triple<>(1001, "张三", 95.5);
System.out.println("学号: " + student.getFirst()); // 1001
System.out.println("姓名: " + student.getSecond()); // 张三
System.out.println("分数: " + student.getThird()); // 95.5
// 修改分数
student.setThird(98.0);
System.out.println("新分数: " + student.getThird()); // 98.0
}
}
2.2 泛型接口
可以定义泛型类,那么也可以定义泛型接口。
普通的接口中定义的是一套行为,当这个行为对很多类型都具备时,就可以在后面添加<T>把它定义为泛型接口。
泛型接口相当于制定了一套通用的“行为规范”。接口里处理的数据类型先用占位符代替,具体是什么类型,可以留到实现的时候再决定。
- 更通用的规范:同一个接口,既可以规范字符串的操作,也可以规范整数的操作,不需要重复定义。
- 更灵活的实现:实现这个接口时,你可以选择写一个通用的实现类,也可以专门为某种类型写一个定制的实现类。
// 泛型接口:定义比较协议,T 表示可比较的任意类型
public interface Comparator<T> {
int compare(T o1, T o2); // 比较两个 T 类型对象
}
// 具体实现:为 String 类型提供比较器
class StringComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length(); // 按字符串长度比较
}
}
📝 编程练习
定义一个泛型接口 Processor<T>,它定义了数据处理的标准。
要求:
- 接口包含
process(T input)方法 - 创建一个实现类
UpperCaseProcessor,将字符串转为大写 - 创建一个实现类
NumberProcessor,返回数字的平方
// 泛型接口定义
public interface Processor<T> {
T process(T input);
}
// 字符串处理器
class UpperCaseProcessor implements Processor<String> {
@Override
public String process(String input) {
return input.toUpperCase();
}
}
// 数字处理器
class NumberProcessor implements Processor<Integer> {
@Override
public Integer process(Integer input) {
return input * input; // 返回平方
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
// 处理字符串
Processor<String> stringProcessor = new UpperCaseProcessor();
String result1 = stringProcessor.process("hello");
System.out.println(result1); // 输出: HELLO
// 处理数字
Processor<Integer> numberProcessor = new NumberProcessor();
Integer result2 = numberProcessor.process(5);
System.out.println(result2); // 输出: 25
}
}
2.3 泛型方法
当方法的行为对许多类型都适用,且也不依赖类型的特性时,就可以定义泛型方法。
定义方式:将类型参数声明在方法返回类型之前,仅作用于当前方法。
最常见的是实例泛型方法(例如处理任意类型的数据):
public class DataProcessor {
// 实例泛型方法:方法自己定义了 <T>,与类无关
public <T> T process(T data) {
System.out.println("处理数据: " + data);
return data;
}
}
// 调用:
DataProcessor processor = new DataProcessor();
String result = processor.process("Hello"); // 自动推断 T 为 String
Integer num = processor.process(123); // 自动推断 T 为 Integer
除了普通实例方法,还有三个值得注意的点:
-
1. 静态方法需独立声明:
静态方法不依赖对象,无法使用类的泛型,必须在static后独立声明<T>。public class ArrayUtils { // 静态泛型方法:<T> 必须声明在 static 之后 public static <T> T getFirst(T[] arr) { return (arr != null && arr.length > 0) ? arr[0] : null; } } -
2. 自动识别(类型推断):
调用时通常无需指定类型,编译器会根据传入的参数自动推导出 T 的类型。// 编译器自动推断 T 为 String String first = ArrayUtils.getFirst(new String[]{"A", "B"}); -
3. 手动指定(显式调用):
当编译器无法推断(如参数类型不一致)时,可以通过类名.<类型>方法名显式指定。// 定义一个工具类,包含 pick 方法 public class Util { // 返回两个参数中的任意一个(这里简单返回第一个) public static <T> T pick(T a, T b) { return a; } } // 参数一个是 String,一个是 Integer // 显式指定 T 为它们的共同父接口 Serializable Serializable s = Util.<Serializable>pick("Key", 1);
📝 编程练习
编写一个泛型工具类 ArrayUtils,包含实用的数组操作方法。
要求:
- 静态泛型方法
swap(T[] array, int i, int j)交换数组中两个元素 - 静态泛型方法
printArray(T[] array)打印数组内容 - 静态泛型方法
<T extends Comparable<T>> T max(T[] array)找出最大值
public class ArrayUtils {
// 交换数组中两个元素的位置
public static <T> void swap(T[] array, int i, int j) {
if (i < 0 || i >= array.length || j < 0 || j >= array.length) {
throw new IllegalArgumentException("索引越界");
}
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// 打印数组内容
public static <T> void printArray(T[] array) {
System.out.print("[");
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]);
if (i < array.length - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
// 找出数组中的最大值(需要实现Comparable接口)
public static <T extends Comparable<T>> T max(T[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("数组不能为空");
}
T max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i].compareTo(max) > 0) {
max = array[i];
}
}
return max;
}
// 使用示例
public static void main(String[] args) {
// 字符串数组
String[] names = {"Alice", "Bob", "Charlie"};
System.out.print("原数组: ");
printArray(names);
swap(names, 0, 2);
System.out.print("交换后: ");
printArray(names);
System.out.println("最大值: " + max(names));
// 整数数组
Integer[] numbers = {3, 1, 4, 1, 5};
System.out.print("数字数组: ");
printArray(numbers);
System.out.println("最大值: " + max(numbers));
}
}
3. 类型参数的限定
之前我们主要强调类型的"任意性",所编写的代码也都主要强调"什么类型都可以"
但这也意味着无法对这种"任意"的类型有什么要求,比如使用它的某种能力
因此,若希望它具备某种"能力"时,就应该对它进行限定。
此时可使用 extends 关键字声明类型T能力前置到编译期。
- 上界限定:
<T extends Number>保证可用 Number 的方法 - 多重限定:类在前、接口以
&连接,如<T extends Number & Comparable<T>> - 意图清晰:把“可数值”“可比较”等能力写进签名,便于维护
// 单重限定:T 必须是 Number 或其子类
public static <T extends Number> double calculateSum(T num1, T num2) {
return num1.doubleValue() + num2.doubleValue(); // 可调用 Number 的方法
}
// 多重限定:T 既是数值类型,又可比较大小
public static <T extends Number & Comparable<T>> T findMaxAndCalculateAverage(T[] array) {
if (array == null || array.length == 0) { return null; }
T max = array[0];
double sum = 0.0;
// 同时利用 Number 和 Comparable 的能力
for (T t : array) {
if (t.compareTo(max) > 0) { max = t; } // 使用 Comparable 接口比较
sum += t.doubleValue(); // 使用 Number 类的数值方法
}
System.out.println("平均值: " + (sum / array.length)); // 数值计算
return max; // 返回最大值
}
📝 编程练习
编写一个泛型方法 findMinMax(),用于找出实现了 Comparable 接口的数组中的最小值和最大值。
要求:
- 方法签名:
<T extends Comparable<T>> String findMinMax(T[] array) - 返回格式为:"最小值: xxx, 最大值: yyy"
- 处理数组为空的情况,返回"数组为空"
public class MinMaxFinder {
// 找出数组中的最小值和最大值
public static <T extends Comparable<T>> String findMinMax(T[] array) {
// 处理空数组
if (array == null || array.length == 0) {
return "数组为空";
}
// 处理单元素数组
if (array.length == 1) {
return "最小值: " + array[0] + ", 最大值: " + array[0];
}
T min = array[0];
T max = array[0];
// 遍历数组找出最小值和最大值
for (int i = 1; i < array.length; i++) {
if (array[i].compareTo(min) < 0) {
min = array[i];
}
if (array[i].compareTo(max) > 0) {
max = array[i];
}
}
return "最小值: " + min + ", 最大值: " + max;
}
// 使用示例
public static void main(String[] args) {
// 字符串数组
String[] names = {"Alice", "Bob", "Charlie", "David"};
System.out.println(findMinMax(names));
// 输出: 最小值: Alice, 最大值: David
// 整数数组
Integer[] numbers = {42, 17, 89, 23, 56};
System.out.println(findMinMax(numbers));
// 输出: 最小值: 17, 最大值: 89
// Double数组
Double[] doubles = {3.14, 2.71, 1.41, 1.73};
System.out.println(findMinMax(doubles));
// 输出: 最小值: 1.41, 最大值: 3.14
// 空数组
String[] empty = {};
System.out.println(findMinMax(empty));
// 输出: 数组为空
}
}
4.1 无界通配符
🎯 情景设定
新需求:写一个通用工具方法,能够打印任意类型的 List 集合
🚨 第一阶段:直觉的陷阱
尝试1:利用多态(直觉做法)
// 既然大家都继承自 Object,那我就接收 List<Object>
public void printList(List<Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// 测试调用
List<String> nameList = Arrays.asList("Java", "Python");
processor.printList(nameList); // ❌ 编译报错!
List<String> 不是 List<Object> 的子类!(泛型具有不可变性 Invariance)
😅 第二阶段:使用 T 的尴尬
尝试2:使用泛型方法 T(可行但啰嗦)
// 使用泛型方法 <T>
public <T> void printList(List<T> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
List<Integer> intList = Arrays.asList(1, 2, 3);
processor.printList(intList);
// 这样确实能工作,但问题来了...
List<String> strList = Arrays.asList("A", "B", "C");
// 需求:我想要一个"万能引用" myList
// 它可以指向 intList,也可以指向 strList
// 尝试:用 T?
// List<T> myList = intList; // ❌ 报错:T 是谁?找不到符号!
T 必须依托于类或方法存在!在普通代码里,我只是想声明一个变量,
T 无能为力了。
🌟 第三阶段:引入通配符(救世主降临)
核心需求:表达"这是一个 List,但元素类型我不确定"
Java 为这种"不确定的、未知的类型"专门提供了符号——问号 ?
尝试3:使用通配符(完美解决)
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = Arrays.asList("A", "B", "C");
// 成功!
// 读作:myList 是一个 List,它装载某种未知的类型
List<?> myList;
myList = intList; // ✅ 编译通过
myList = strList; // ✅ 编译通过
// 打印方法也变得更清爽
printList(myList);
}
// 方法定义:不需要声明 <T>
public void printList(List<?> list) {
for (Object elem : list) {
System.out.print(elem + " ");
}
System.out.println();
// 写入限制:只能添加 null
list.add(null); // ✅ 允许
// list.add("hello"); // ❌ 编译错误:类型不匹配
}
📊 总结对比
| 场景 | List<Object> |
List<T> |
List<?> |
|---|---|---|---|
| 语义 | 装且仅装 Object | 装特定类型 T | 装任意类型 |
| 多态性 | ❌ 差 | ✅ 好 | ✅ 好 |
| 局部变量声明 | ✅ (但没法赋值) | ❌ 不能 | ✅ 完美 |
| 代码简洁度 | 简洁 | 啰嗦 | 最简洁 |
🎯 核心结论
T 是一个确定的变量(运行时知道具体类型)
? 是一个模糊的范围(表示"某种未知类型")
当我们不关心具体类型,或需要声明通用引用时,通配符 ? 就是唯一选择!
📝 编程练习
编写一个通用的集合工具类 CollectionUtils,包含处理任意类型集合的方法。
要求:
- 静态方法
printCollection(Collection<?> coll)打印集合内容 - 静态方法
int getSize(Collection<?> coll)返回集合大小 - 静态方法
boolean isEmpty(Collection<?> coll)判断集合是否为空
import java.util.*;
public class CollectionUtils {
// 打印任意类型的集合
public static void printCollection(Collection<?> coll) {
if (coll == null) {
System.out.println("集合为null");
return;
}
System.out.print("集合内容: [");
Iterator<?> iterator = coll.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next());
if (iterator.hasNext()) {
System.out.print(", ");
}
}
System.out.println("]");
}
// 获取集合大小
public static int getSize(Collection<?> coll) {
return coll == null ? 0 : coll.size();
}
// 判断集合是否为空
public static boolean isEmpty(Collection<?> coll) {
return coll == null || coll.isEmpty();
}
// 使用示例
public static void main(String[] args) {
// 创建不同类型的集合
List<String> stringList = Arrays.asList("Apple", "Banana", "Cherry");
Set<Integer> intSet = new HashSet<>(Arrays.asList(1, 2, 3, 4, 5));
Queue<Double> doubleQueue = new LinkedList<>(Arrays.asList(1.1, 2.2, 3.3));
// 打印各种集合
printCollection(stringList); // 集合内容: [Apple, Banana, Cherry]
printCollection(intSet); // 集合内容: [1, 2, 3, 4, 5]
printCollection(doubleQueue); // 集合内容: [1.1, 2.2, 3.3]
// 获取大小
System.out.println("字符串列表大小: " + getSize(stringList)); // 3
System.out.println("整数集合大小: " + getSize(intSet)); // 5
// 判断是否为空
System.out.println("队列是否为空: " + isEmpty(doubleQueue)); // false
// 测试空集合
List<String> emptyList = new ArrayList<>();
System.out.println("空列表大小: " + getSize(emptyList)); // 0
System.out.println("空列表是否为空: " + isEmpty(emptyList)); // true
// 测试null
System.out.println("null集合大小: " + getSize(null)); // 0
System.out.println("null集合是否为空: " + isEmpty(null)); // true
}
}
4.2 上界通配符
🚫 痛点:多态的失效
我们需要统计 List<Number> 的总和。直觉写法:
public double sum(List<Number> list) { ... }
List<Integer> ints = Arrays.asList(1, 2);
sum(ints); // ❌ 编译报错!List<Integer> 不是 List<Number> 的子类
解决方案:使用 List<? extends Number> 表示“Number 或其子类的列表”。
- 读取安全:可以安全地读取为
Number。 - 写入受限:除
null外不可写入(因为不知道具体是哪个子类)。 - 典型场景:生产者(Producer),用于数据读取。
public static double sumOfList(java.util.List<? extends Number> numbers) {
double sum = 0.0;
for (Number n : numbers) { sum += n.doubleValue(); }
return sum;
}
System.out.println(sumOfList(java.util.Arrays.asList(1,2,3)));
System.out.println(sumOfList(java.util.Arrays.asList(1.1,2.2)));
🤔 思考:能不能用 T 代替 ?
可以!public <T extends Number> double sum(List<T> list) 也能工作。
但通配符 ? 更简洁,它明确表达了"我不关心具体类型,只关心它是 Number"的语义。
📝 编程练习
编写一个数值统计工具类 NumberStats,使用上界通配符处理各种数值类型的列表。
要求:
- 方法
double calculateSum(List<? extends Number> numbers)计算总和 - 方法
double calculateAverage(List<? extends Number> numbers)计算平均值 - 方法
Number findMax(List<? extends Number> numbers)找出最大值
import java.util.List;
import java.util.Arrays;
import java.util.Collections;
public class NumberStats {
// 计算数值列表的总和
public static double calculateSum(List<? extends Number> numbers) {
if (numbers == null || numbers.isEmpty()) {
return 0.0;
}
double sum = 0.0;
for (Number num : numbers) {
sum += num.doubleValue();
}
return sum;
}
// 计算数值列表的平均值
public static double calculateAverage(List<? extends Number> numbers) {
if (numbers == null || numbers.isEmpty()) {
return 0.0;
}
return calculateSum(numbers) / numbers.size();
}
// 找出数值列表中的最大值
public static Number findMax(List<? extends Number> numbers) {
if (numbers == null || numbers.isEmpty()) {
return null;
}
Number max = numbers.get(0);
for (int i = 1; i < numbers.size(); i++) {
if (numbers.get(i).doubleValue() > max.doubleValue()) {
max = numbers.get(i);
}
}
return max;
}
// 使用示例
public static void main(String[] args) {
// 整数列表
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
System.out.println("整数列表: " + integers);
System.out.println("总和: " + calculateSum(integers)); // 15.0
System.out.println("平均值: " + calculateAverage(integers)); // 3.0
System.out.println("最大值: " + findMax(integers)); // 5
System.out.println("---");
// Double列表
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5, 4.5);
System.out.println("Double列表: " + doubles);
System.out.println("总和: " + calculateSum(doubles)); // 12.0
System.out.println("平均值: " + calculateAverage(doubles)); // 3.0
System.out.println("最大值: " + findMax(doubles)); // 4.5
System.out.println("---");
// 混合类型的Number列表
List<Number> mixed = Arrays.asList(10, 20.5, 30, 40.5);
System.out.println("混合列表: " + mixed);
System.out.println("总和: " + calculateSum(mixed)); // 101.0
System.out.println("平均值: " + calculateAverage(mixed)); // 25.25
System.out.println("最大值: " + findMax(mixed)); // 40.5
System.out.println("---");
// 空列表测试
List<Integer> empty = Arrays.asList();
System.out.println("空列表总和: " + calculateSum(empty)); // 0.0
System.out.println("空列表最大值: " + findMax(empty)); // null
}
}
4.3 下界通配符
🚫 痛点:复用性受限
我们需要将整数添加到列表中。直觉写法:
public void addIntegers(List<Integer> list) { ... }
List<Number> nums = new ArrayList<>();
addIntegers(nums); // ❌ 编译报错!List<Number> 无法传给 List<Integer>
解决方案:使用 List<? super Integer> 表示“Integer 或其父类的列表”。
- 写入安全:可以安全地写入
Integer(因为它是任何父类的子类型)。 - 读取退化:只能读取为
Object(因为不知里面的是 Integer还是Number,干脆当Object)。 - 典型场景:消费者(Consumer),用于数据写入。
public static void addNumbers(java.util.List<? super Integer> dest) {
for (int i = 1; i <= 5; i++) { dest.add(i); }
}
java.util.List<Number> numList = new java.util.ArrayList<>();
java.util.List<Object> objList = new java.util.ArrayList<>();
addNumbers(numList);
addNumbers(objList);
🤔 思考:能不能用 T 代替 ?
不能! Java 不支持 <T super Integer> 这样的语法。
这是通配符的不可替代性场景——当需要表示"某个类型的父类"时,必须使用 ? super。
📝 编程练习
编写一个数据收集器 DataCollector,使用下界通配符实现可以向多种类型的集合中添加整数的功能。
要求:
- 方法
void addIntegers(List<? super Integer> list)添加1到5的整数 - 方法
void addStrings(List<? super String> list)添加字符串元素 - 演示该方法可以接受
List<Integer>、List<Number>、List<Object>
import java.util.*;
public class DataCollector {
// 向可以容纳Integer或其父类型的列表中添加整数
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
}
// 向可以容纳String或其父类型的列表中添加字符串
public static void addStrings(List<? super String> list) {
list.add("Hello");
list.add("World");
list.add("Java");
list.add("Generic");
}
// 打印列表内容(注意:只能读取为Object)
public static void printList(List<?> list, String listName) {
System.out.print(listName + ": [");
for (Object obj : list) {
System.out.print(obj);
if (list.indexOf(obj) < list.size() - 1) {
System.out.print(", ");
}
}
System.out.println("]");
}
// 使用示例
public static void main(String[] args) {
System.out.println("=== 添加整数示例 ===");
// 创建不同类型的列表
List<Integer> integerList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
// 使用下界通配符方法添加整数
addIntegers(integerList); // 可以:List<Integer> 可以接受Integer
addIntegers(numberList); // 可以:List<Number> 可以接受Integer的子类
addIntegers(objectList); // 可以:List<Object> 可以接受任何对象
// 打印结果
printList(integerList, "整数列表");
printList(numberList, "数字列表");
printList(objectList, "对象列表");
System.out.println("\n=== 添加字符串示例 ===");
// 清空列表用于字符串测试
integerList.clear();
numberList.clear();
objectList.clear();
// 注意:不能调用 addStrings(integerList)
// 因为 List<Integer> 不能容纳 String
// addStrings(integerList); // 编译错误!
// List<Number> 也不能容纳 String
// addStrings(numberList); // 编译错误!
// 只有 List<Object> 可以容纳 String
addStrings(objectList); // 可以:List<Object> 可以容纳任何对象
printList(objectList, "字符串列表");
// 创建字符串专用的列表
List<String> stringList = new ArrayList<>();
addStrings(stringList); // 可以:List<String> 可以接受String
printList(stringList, "字符串专用列表");
System.out.println("\n=== 类型安全说明 ===");
System.out.println("下界通配符确保了写入的安全性:");
System.out.println("- List<? super Integer> 保证可以安全地写入Integer");
System.out.println("- List<? super String> 保证可以安全地写入String");
System.out.println("- 但读取时只能读取为Object类型");
}
}
4.4 PECS原则
🎯 Producer Extends, Consumer Super
啤(P)儿(E)茶(C)爽(S):一种神奇味道的饮料
如果你只需要读取数据,用 ? extends T;如果你只需要写入数据,用 ? super T。
📖 PECS原则详解
🏭 Producer (生产者)
用途:只从集合中读取数据,不写入
语法:? extends T
含义:可以是 T 或 T 的任意子类
// 只能读取,不能写入
List<? extends Number> numbers = Arrays.asList(1, 2, 3);
Number n = numbers.get(0); // ✅ 可以读取
// numbers.add(1); // ❌ 不能写入
🛒 Consumer (消费者)
用途:只向集合中写入数据,不读取
语法:? super T
含义:可以是 T 或 T 的任意父类
// 只能写入,读取受限
List<? super Integer> numbers = new ArrayList<Number>();
numbers.add(1); // ✅ 可以写入
// Integer n = numbers.get(0); // ❌ 读取类型不确定
🔍 实际应用示例
示例1:复制集合(PECS的经典应用)
// 局限性写法 - 虽然可以编译,但在使用时非常受限
public static <T> void copyNotGood(List<T> src, List<T> dest) {
for (T item : src) {
dest.add(item);
}
}
// 正确写法 - 使用PECS原则
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) { // src是生产者,只读取
dest.add(item); // dest是消费者,只写入
}
}
// 使用示例
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Number> numList = new ArrayList<>();
copy(intList, numList); // ✅ 完美工作
为什么说"类型过于严格"?
在 copyNotGood 方法中,src 和 dest 被强制要求是完全相同的类型 List<T>。
假设我们要把 List<Integer>(整数列表)复制到 List<Number>(数字列表)中:
- 逻辑上是完全可行的(整数也是数字)。
- 但
copyNotGood会报错,因为它要求两个列表的泛型类型必须完全一致。 - 如果 T 是 Integer,
dest必须是List<Integer>(不能是 List<Number>)。 - 如果 T 是 Number,
src必须是List<Number>(不能是 List<Integer>,因为泛型是不变的)。
这就是为什么我们需要 PECS 原则来放宽类型限制。
示例2:查找最大值
// 查找集合中的最大值
// <T extends Comparable<? super T>>: T 必须是可比较的,且可以与 T 的父类比较
// List<? extends T>: 可以传入 T 或 T 的子类列表(生产者)
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException("空列表");
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
// 使用示例
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5);
Integer maxNum = max(numbers); // ✅ 返回 5
💡 记忆技巧
- Producer Extends:生产者(提供数据)用 extends,像工厂一样"产出"东西
- Consumer Super:消费者(接收数据)用 super,像垃圾桶一样"接收"东西
- 读写原则:extends 只能读,super 只能写
- PECS = Get-Put Principle:Get 用 extends,Put 用 super
⚠️ 常见错误
// 错误1:混淆生产者和消费者
List<? extends Number> list = new ArrayList<Integer>();
list.add(10); // ❌ 编译错误:不能写入生产者
// 错误2:读取消费者时类型不安全
List<? super Integer> list = new ArrayList<Number>();
Integer num = list.get(0); // ❌ 编译错误:读取类型不确定
Object obj = list.get(0); // ✅ 只能读取为Object
4.5 自限定类型:让 T 能和 T 比较
很多算法(排序、求最大值)都需要“元素可比较”。泛型通过上界把这种约束写进类型系统。
import java.util.List;
public class SelfBoundedDemo {
// 自限定:T 必须实现 Comparable<T>,保证 compareTo 的参数就是同类型
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException("空列表");
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
}
💡 为什么叫“自限定”?
类型参数 T 的上界里又出现了 T 本身:T extends Comparable<T>。这能避免“拿苹果去和橘子比”的类型错误。
4.6 动态类型安全:Collections.checkedXxx()
泛型的类型检查主要发生在编译期。但在一些“泛型边界场景”中,你仍然可能在运行时把错误类型塞进集合里:
- 对接旧代码使用了原始类型(raw type),编译器只能给 warning
- 反射/框架/跨模块 API 把类型信息绕开了编译器
- 你拿到的是“别人创建的集合”,无法保证它的元素全都正确
Collections.checkedList/checkedSet/checkedMap 会返回一个带运行时类型检查的视图:当你通过这个视图进行 add/set/addAll 等写操作时,会做类型校验,从而更早暴露问题。
不使用 checkedList:错误在“很远的地方”爆炸
import java.util.ArrayList;
import java.util.List;
public class WithoutCheckedList {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
// 模拟旧代码/反射/原始类型绕过编译器
@SuppressWarnings({"rawtypes", "unchecked"})
List raw = list;
raw.add("not int"); // 这里不会立刻报错(只是悄悄污染了集合)
// 更“远”的地方才爆炸:读取并当作 Integer 使用
int x = list.get(0); // 这里才抛 ClassCastException,定位更痛苦
System.out.println(x);
}
}
使用 checkedList:在 add 当场爆炸
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class WithCheckedList {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
List<Integer> safeView = Collections.checkedList(list, Integer.class);
@SuppressWarnings({"rawtypes", "unchecked"})
List raw = safeView;
raw.add("not int"); // ✅ 当场抛 ClassCastException,更好定位
}
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CheckedListDemo {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
// 创建“带运行时类型检查”的视图(safeView 不是新集合,而是包装视图)
List<Integer> safeView = Collections.checkedList(list, Integer.class);
// 下面这段模拟“旧代码/反射/原始类型”把错误类型塞进来
@SuppressWarnings({"rawtypes", "unchecked"})
List raw = safeView;
raw.add("not int"); // 这里会在运行时抛出 ClassCastException(更早暴露问题)
}
}
它能做什么?它不能做什么?
- 能做:拦截“写入集合”的错误类型,让错误发生在 add/set 那一刻
- 不能做:把 Java 泛型变成“运行时可见”的真泛型;它只是一个防线,不是魔法
- 注意:如果底层集合在包装前就已经混入了错误类型,读取时仍可能出问题
建议:在“泛型边界”处(对接旧代码、反射、跨模块API)使用 checked 视图,把错误尽早变成可定位的异常。原理会在 5.1 类型擦除 里完整解释。
4.7 混型(Mixin)思想:用组合“拼装能力”
Java 没有多继承,但可以用接口 + 组合 + 委托实现“把多个能力混入一个类”。泛型常用在“能力的返回类型/约束”上。
interface Named {
String name();
}
interface Identified<ID> {
ID id();
}
final class NameImpl implements Named {
private final String name;
NameImpl(String name) { this.name = name; }
@Override public String name() { return name; }
}
final class IntIdImpl implements Identified<Integer> {
private final int id;
IntIdImpl(int id) { this.id = id; }
@Override public Integer id() { return id; }
}
// “混入”多个能力:对外实现接口;对内用字段组合实现并委托
final class User implements Named, Identified<Integer> {
private final Named named;
private final Identified<Integer> identified;
User(String name, int id) {
this.named = new NameImpl(name);
this.identified = new IntIdImpl(id);
}
@Override public String name() { return named.name(); }
@Override public Integer id() { return identified.id(); }
}
核心取舍:Mixin 让“能力复用”更灵活,但会带来更多委托代码;工程里常用 IDE 自动生成委托方法。
5.1 类型擦除
🎭 一句话理解类型擦除
在编译时看得见,在运行时却消失。
🤔 什么是类型擦除?
类型擦除是Java泛型的核心机制:编译器在生成字节码时,会偷偷擦掉所有泛型信息,把它们变成普通的原始类型。
📝 你写的代码(编译前)
List<String> names = new ArrayList<>();
names.add("张三");
String name = names.get(0); // 很自然,不需要转换
🔍 JVM看到的代码(编译后)
List names = new ArrayList(); // 泛型信息被擦除!
names.add("张三");
String name = (String) names.get(0); // 编译器加了强制转换!
💡 关键洞察
✅ 类型检查严格
✅ 编码更安全
✅ IDE智能提示
❌ 泛型信息消失
❌ List<String> == List<Integer>
❌ 无法知道具体类型
5.2 擦除过程详解
示例: 泛型类的"变身"
让我们通过一个完整的例子,看看编译器是如何施展它的"魔术"的:
📝 第一步:你写的代码
public class Box<T> {
private T treasure; // 神秘的宝藏,类型是T
public void put(T item) {
this.treasure = item;
System.out.println("放入了:" + item);
}
public T take() {
System.out.println("取出了:" + treasure);
return treasure;
}
}
// 使用泛型
Box<String> stringBox = new Box<>();
stringBox.put("钻石");
String gem = stringBox.take(); // 不需要转换,很优雅
🔍 第二步:编译器眼中的代码(擦除后)
public class Box {
private Object treasure; // T变成了Object!
public void put(Object item) { // T变成了Object
this.treasure = item;
System.out.println("放入了:" + item);
}
public Object take() { // T变成了Object
System.out.println("取出了:" + treasure);
return treasure;
}
}
// 编译器自动添加类型转换
Box stringBox = new Box();
stringBox.put("钻石");
String gem = (String) stringBox.take(); // 编译器偷偷加了强制转换!
🔧 编译器的"秘密操作"
1️⃣ 类型替换
所有的<T>都被替换为边界类型(通常是Object)
2️⃣ 自动加转换
在需要的地方自动插入强制类型转换
3️⃣ 生成桥接方法
为了保持多态性,有时会生成额外的方法
🤯 现实检验:运行时的真相
真相1: List<String> 和 List<Integer> 在运行时就是同一个类!
为什么运行时是同一个类,但编译时却无法互相赋值?
因为类型擦除:编译器在编译时检查类型安全,但运行时泛型信息被擦除,只剩下原始类型 ArrayList。
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // true!
真相2: 你无法在运行时知道一个泛型对象的具体类型参数
if (strList instanceof List<String>) { // 编译错误!
// 这是不允许的,因为运行时没有泛型信息
}
5.3 为什么类型擦除?
🕰️ 回到2004年:Java面临的艰难选择
2004年,Java准备引入泛型时,站在了一个历史的十字路口。设计者们面临两个选择:
| 维度 | 🅰️ 方案A:彻底革命(像C++模板那样) | 🅱️ 方案B:妥协方案(类型擦除) |
|---|---|---|
| ✅ 优点 |
|
|
| ❌ 缺点 |
|
|
🏆 最终选择:类型擦除
🛡️ 向后兼容性
Java最大的优势就是其庞大的生态系统。类型擦除确保了:
- JDK 1.4的代码可以在JDK 5.0上运行
- 新的泛型代码可以与旧的非泛型代码互操作
- 企业可以逐步迁移,而不是重写所有代码
⚡ 平滑迁移
开发者可以:
- 先学习泛型,但保持现有代码不变
- 逐步将关键部分改为泛型
- 新旧代码可以完美协作
🎯 工程现实
这是一个务实的工程决策:
- 牺牲了理论上的完美性
- 换来了现实中的可行性
- 让Java生态得以平稳进化
💭 设计者的智慧
"完美的设计往往是不现实的。真正的工程艺术在于在约束条件下做出最好的选择。" —— Java泛型设计师
5.4 类型擦除的限制
⚠️ 类型擦除带来的"怪癖"
理解了类型擦除,你就会明白为什么Java泛型总有一些"奇怪"的限制。这些不是设计缺陷,而是类型擦除的必然结果。
🚫 限制1:基本类型不能作为类型参数
❌ 这样写会报错
List<int> numbers = new ArrayList<>(); // 编译错误!
✅ 正确的写法
List<Integer> numbers = new ArrayList<>(); // 使用包装类
🤔 为什么会这样?
因为擦除后,List<T> 变成了 List,里面的元素类型是 Object。
但是 int 不是对象,不能存放在 Object 中,而 Integer 可以。
🚫 限制2:不能创建泛型实例
❌ 这样写会报错
因为类型擦除,编译器在编译时擦成了 Object, 不知道 T 的具体类型,无法生成 new T() 的字节码指令
public class Factory<T> {
public T create() {
return new T(); // 编译错误!
}
// 如果让 new T() 等价于调用 new Object(), 那么下面的代码就会出错:
// Factory factory = new Factory<>();
// String s = factory.create(); <--- create()出的Object无法强转成String
}
✅ 解决方案:使用反射
public class Factory<T> {
private Class<T> clazz;
public Factory(Class<T> clazz) {
this.clazz = clazz;
}
public T create() throws Exception {
return clazz.newInstance(); // 通过反射创建
}
}
🤔 为什么会这样?
擦除后,T 变成了 Object,所以 new T() 实际上会变成 new Object(),这显然不是你想要的。编译器无法知道 T 具体是什么类型。
🚫 限制3:不能创建泛型数组
❌ 这样写会报错
List<String>[] array = new List<String>[10]; // 编译错误!
⚠️ 如果不禁止会发生什么?
// 假设这是合法的
List<String>[] stringLists = new List<String>[1];
Object[] objects = stringLists; // 数组协变,这是允许的
objects[0] = new ArrayList<Integer>(); // 运行时检查通过!
// 灾难!
String s = stringLists[0].get(0); // ClassCastException!
🤔 为什么会这样?
数组在Java中是"协变"的,并且会在运行时做类型检查。如果允许泛型数组,就会绕过泛型的类型安全检查,导致运行时异常。
🚫 限制4:不能在静态上下文中使用类型参数
❌ 这样写会报错
public class Container<T> {
private static T staticItem; // 编译错误!
public static T getStatic() { // 编译错误!
return null;
}
}
🤔 为什么会这样?
静态方法属于类本身,不依赖任何实例。而类的泛型参数(如 Container<T> 中的 T)只有在创建具体实例时才确定。比如 Container<String> 和 Container<Integer> 是同一个类,只是类型参数不同。静态方法无法知道当前应该用哪个类型参数,所以不能使用类的泛型参数。
5.5 桥接方法
🎯 什么是桥接方法?
桥接方法是编译器为了保持泛型继承中的多态性而自动生成的方法。它们是类型擦除的"副作用",但对程序的正确运行至关重要。
🎭 问题场景:继承中的类型冲突
父类(泛型)
class Node<T> {
public void setData(T data) {
System.out.println("Node.setData: " + data);
}
}
子类(指定具体类型)
class IntNode extends Node<Integer> {
@Override
public void setData(Integer data) {
System.out.println("IntNode.setData: " + data);
super.setData(data);
}
}
🚨 问题出现:擦除后的签名不匹配
擦除后的真实情况
父类变成:
class Node {
public void setData(Object data) {
System.out.println("Node.setData: " + data);
}
}
子类变成:
class IntNode extends Node {
public void setData(Integer data) {
System.out.println("IntNode.setData: " + data);
super.setData(data);
}
}
💥 冲突分析
- 父类方法:
setData(Object) - 子类方法:
setData(Integer) - 问题: 这两个方法签名不同,
setData(Integer)并没有重写setData(Object)! - 后果: 多态性被破坏,运行时调用会出现问题
🔧 解决方案:编译器自动生成桥接方法
为了解决这个问题,编译器会在 IntNode 类中偷偷生成一个桥接方法:
class IntNode extends Node {
// 这是程序员写的方法
public void setData(Integer data) {
System.out.println("IntNode.setData: " + data);
super.setData(data);
}
// 🔧 这是编译器自动生成的桥接方法!
// 它真正重写了父类的 setData(Object) 方法
public void setData(Object data) {
// 先检查类型,然后调用真正的方法
this.setData((Integer) data);
}
}
✨ 桥接方法的作用
🎯 保持多态性
确保通过父类引用调用时,能正确找到子类的方法
🔒 类型安全
在桥接方法中进行类型检查和转换,保证运行安全
🤝 无缝协作
让泛型代码和非泛型代码能够和谐共存
👀 亲眼见证:如何看到桥接方法
你可以通过反射来观察到这些"隐藏"的桥接方法:
Class<?> clazz = IntNode.class;
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
System.out.println("方法:" + method.getName() +
" 参数:" + Arrays.toString(method.getParameterTypes()) +
" 是否是桥接方法:" + method.isBridge());
}
// 输出中会看到两个setData方法,其中一个isBridge()为true
5.6 类型擦除测试
🎯 互动挑战:你真的理解类型擦除了吗?
让我们通过几个有趣的挑战来检验你对类型擦除的理解程度!
🧩 挑战1:猜猜看,哪个会编译失败?
选项A
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1; // 这行会?
选项B
List<String> list1 = new ArrayList<>();
List<?> list2 = list1; // 这行会?
🎯 答案揭晓
选项A会编译失败! 因为 List<String> 不是 List<Object> 的子类型,即使String是Object的子类型。这是泛型的不变性。
选项B可以编译! 无界通配符 List<?> 可以引用任何类型的List。
🧩 挑战2:运行时类型判断
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // 输出什么?
System.out.println(strList instanceof List); // 输出什么?
System.out.println(strList instanceof List<String>); // 会怎样?
🎯 答案揭晓
strList.getClass() == intList.getClass()返回 true!运行时它们都是同一个ArrayList类strList instanceof List返回 truestrList instanceof List<String>编译错误! 运行时无法知道泛型参数
🧩 挑战3:反射获取泛型信息
public class StringList extends ArrayList<String> {}
StringList myList = new StringList();
Type genericSuperclass = myList.getClass().getGenericSuperclass();
System.out.println(genericSuperclass); // 会输出什么?
🎯 答案揭晓
输出:java.util.ArrayList<java.lang.String>
神奇吧! 虽然运行时泛型被擦除,但通过继承关系,我们还是可以获取到泛型信息!这就是很多框架(如Spring、Jackson)能够实现类型安全序列化的秘密。
🎓 核心要点回顾
🎭 类型擦除的本质
编译时的"隐形斗篷",运行时的"类型消失术"
🛡️ 历史的选择
为了向后兼容性做出的务实妥协
⚠️ 限制的来源
所有"奇怪"的限制都源于运行时类型信息的缺失
🔧 编译器的智慧
桥接方法等机制保证了多态性的正常工作