第一部分:通配符基础

理解泛型通配符的核心概念和使用场景

学习目标

掌握三种通配符的基本语法和应用场景

Java泛型通配符类型

无界通配符

List<?>
  • 表示未知类型的集合
  • 只允许读取为Object类型
  • 不能添加除null外的任何元素
  • 最安全的读取方式

上界通配符

List<? extends Number>
  • 表示Number或其子类的集合
  • 安全读取为Number类型
  • 不能添加除null外的任何元素
  • 适用于只读操作

下界通配符

List<? super Integer>
  • 表示Integer或其父类的集合
  • 可以添加Integer及其子类对象
  • 读取只能得到Object类型
  • 适用于只写操作
1

为什么需要通配符?

Java泛型系统在实现泛型类/方法时会出现继承关系问题:

List<Number> numbers = new ArrayList<Number>(); List<Integer> integers = new ArrayList<Integer>(); // 编译错误:List<Integer>不是List<Number>的子类型 numbers = integers; // 使用通配符解决: List<? extends Number> numList = integers; // 正确

关键概念:泛型类型在Java中是不变的(Invariant),通配符提供了协变和逆变的解决方案

2

基本使用示例

演示三种通配符的使用场景:

// 无界通配符示例 public static void printList(List<?> list) { for (Object elem : list) { System.out.print(elem + " "); } System.out.println(); } // 上界通配符示例 public static double sum(List<? extends Number> list) { double total = 0.0; for (Number num : list) { total += num.doubleValue(); } return total; } // 下界通配符示例 public static void addIntegers(List<? super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); } }

第二部分:上界通配符(? extends T)

学习如何安全地读取包含子类对象的集合

学习目标

掌握上界通配符的使用场景和限制

1

上界通配符的特点

  • 读取安全:可以安全地读取元素为T类型
  • 写入限制:除null外不能添加任何元素
  • 用途:只读操作(生产者)
  • 例子List<? extends Number> 可以是List<Integer>, List<Double>等
// 正确使用示例 List<Integer> ints = Arrays.asList(1, 2, 3); List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3); System.out.println("Integer sum: " + sumList(ints)); // 6.0 System.out.println("Double sum: " + sumList(doubles)); // 6.6 public static double sumList(List<? extends Number> list) { double sum = 0.0; for (Number num : list) { sum += num.doubleValue(); } return sum; } // 错误使用:尝试添加元素 public static void addToList(List<? extends Number> list) { // 以下代码都会编译错误 list.add(10); list.add(3.14); list.add(new BigDecimal("10.5")); // 唯一允许添加的是null list.add(null); }
2

使用场景

  • 集合元素作为输入(生产者)
  • 遍历和读取集合内容
  • 集合内容不关心具体子类型
  • 计算、统计等操作
// 从多个不同但相关的集合中读取数据 public static void processNumbers(List<? extends Number> numbers) { for (Number num : numbers) { System.out.println("Number value: " + num); System.out.println("Double value: " + num.doubleValue()); System.out.println("Int value: " + num.intValue()); } }

第三部分:下界通配符(? super T)

学习如何安全地向父类型集合添加元素

学习目标

掌握下界通配符的使用场景和限制

1

下界通配符的特点

  • 写入安全:可以添加T及其子类对象
  • 读取限制:只能读取为Object类型
  • 用途:写入操作(消费者)
  • 例子List<? super Integer> 可以是List<Integer>, List<Number>等
// 正确使用示例 List<Number> numbers = new ArrayList<>(); List<Object> objects = new ArrayList<>(); addNumbers(numbers); addNumbers(objects); public static void addNumbers(List<? super Integer> list) { list.add(42); list.add(100); // 也可以添加Integer的子类 // list.add((byte)10); // 自动装箱为Byte // 读取不安全 - 只能作为Object Object obj = list.get(0); // Number num = list.get(0); // 编译错误 }
2

使用场景

  • 集合作为输出(消费者)
  • 向集合中添加元素
  • 集合类型不关心具体元素类型
  • 收集器、消费者等模式
// 复制数据到目标集合 public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (T item : src) { dest.add(item); } } // 在Collections.addAll中使用 public static <T> void addAll(Collection<? super T> c, T... elements) { for (T element : elements) { c.add(element); } }

第四部分:PECS原则

掌握使用通配符的核心设计原则

学习目标

理解和应用Producer-Extends, Consumer-Super原则

PECS原则解释

在使用泛型通配符时遵循的核心原则:

PECS - Producer-Extends, Consumer-Super
  • 如果你需要一个提供数据的生产者(Producer),使用<? extends T>
  • 如果你需要一个消费数据的消费者(Consumer),使用<? super T>
  • 如果你两者都需要,使用精确类型<T>
1

PECS原则应用

在Java API设计中广泛使用的原则:

// Java Collections.copy方法签名 public static <T> void copy( List<? super T> dest, // 消费者 - 需要写入数据 List<? extends T> src // 生产者 - 提供数据 ) { // ... } // Collections.max方法签名 public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) { // ... coll是生产者,提供元素进行比较 }
2

实践案例:合并集合

使用PECS原则设计合并集合的方法:

public class CollectionUtils { // 合并多个生产者集合到一个消费者集合 public static <T> void merge( List<? super T> destination, List<? extends T>... sources) { for (List<? extends T> source : sources) { destination.addAll(source); } } // 获取多个生产者集合中的最大值 public static <T extends Comparable<? super T>> T maxItem(List<? extends T>... lists) { T max = null; for (List<? extends T> list : lists) { for (T item : list) { if (max == null || item.compareTo(max) > 0) { max = item; } } } return max; } }

第五部分:类型擦除原理

揭秘Java泛型在编译时的转换机制

学习目标

理解Java泛型在编译时如何处理类型信息

类型擦除机制

Java泛型通过类型擦除实现,在编译阶段完成以下转换:

  1. 将泛型类型参数替换为边界类型(无边界则为Object)
  2. 在必要位置插入类型转换
  3. 生成桥接方法以保持多态性
源代码 编译后(类型擦除后) 说明
List<String> list = new ArrayList<>(); List list = new ArrayList(); 类型参数被擦除
list.add("Hello"); list.add("Hello"); 方法调用无需转换
String s = list.get(0); String s = (String) list.get(0); 插入强制类型转换
T getData() { ... } Object getData() { ... } 方法返回类型被擦除
1

边界替换

当使用边界时,编译器会使用边界类型替换类型参数:

// 源代码 public class NumericBox<T extends Number> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; } } // 类型擦除后 public class NumericBox { private Number value; // T被替换为Number public void setValue(Number value) { this.value = value; } public Number getValue() { return value; } }
2

桥接方法

当泛型方法与继承发生冲突时,编译器生成桥接方法:

// 基类 interface Comparable<T> { int compareTo(T o); } // 实现类 class String implements Comparable<String> { public int compareTo(String o) { // ... } } // 编译器生成桥接方法 class String implements Comparable { public int compareTo(String o) { // 具体实现 } // 桥接方法:为了多态兼容性 public int compareTo(Object o) { return compareTo((String) o); } }

重要:桥接方法由编译器自动生成,确保类型安全的继承机制

第六部分:类型擦除的影响

理解Java泛型在运行时的限制

学习目标

掌握类型擦除带来的限制以及解决方案

1

类型擦除的主要限制

// 1. 无法使用instanceof检查泛型类型 List<Integer> list = new ArrayList<>(); if (list instanceof List<Integer>) { // 编译错误 } // 2. 无法创建泛型数组 // 编译错误 // T[] array = new T[10]; // 3. 无法重载相同参数化类型的方法 class Example { void print(List<String> list) {} void print(List<Integer> list) {} // 编译错误 } // 4. 静态方法/字段无法使用泛型类型参数 class Box<T> { // 编译错误 static T defaultValue; static void print(T item) { // 错误 } }
2

解决方案与最佳实践

// 1. 类型检查:使用原始类型 if (list instanceof List) { // 然后通过元素检查 } // 2. 创建泛型数组:使用反射或数组列表 T[] createArray(Class<T> clazz, int size) { return (T[]) Array.newInstance(clazz, size); } // 或使用集合代替数组 List<T> list = new ArrayList<>(); // 3. 避免重载冲突:使用不同的方法名 class Example { void printStrings(List<String> list) {} void printIntegers(List<Integer> list) {} } // 4. 静态方法问题:定义为泛型方法 class Box<T> { static <U> void print(U item) { // 正确 } }

运行时类型信息技巧

通过反射获取泛型信息:

public class TypeExample<T> { private List<T> dataList; public TypeExample() { Type type = ((ParameterizedType) getClass().getGenericSuperclass()) .getActualTypeArguments()[0]; System.out.println("Type: " + type); } } // 使用示例 new TypeExample<String>(); // 输出: Type: class java.lang.String

注意:此方法只适用于编译时已知具体类型的情况

第七部分:常见错误与陷阱

避免泛型编程中的常见错误

错误1:混淆通配符类型

List<?> unknownList = new ArrayList<String>(); // 错误:不能向List<?>添加除null外的元素 unknownList.add("Hello"); // 编译错误

解决方案:使用精确类型或下界通配符进行写入操作

错误2:忽略类型擦除影响

// 尝试创建泛型数组 public <T> T[] createArray(int size) { return new T[size]; // 编译错误 } // 尝试运行时检查类型 if (list instanceof List<Integer>) { // 编译错误 // ... }

解决方案:使用类型反射或传递Class对象解决运行时类型问题

错误3:误用原始类型

// 混合使用泛型和原始类型 List<String> strings = new ArrayList<>(); List rawList = strings; // 产生未检查警告 rawList.add(100); // 运行时报错 String s = strings.get(1); // ClassCastException

解决方案:避免使用原始类型,确保所有集合都使用泛型声明

错误4:过度使用无界通配符

// 过度使用通配符降低代码安全性 public static void process(List<?> list) { for (Object o : list) { // 需要进行大量的类型判断和强制转换 if (o instanceof String) { String s = (String) o; // ... } else if (o instanceof Integer) { Integer i = (Integer) o; // ... } } }

解决方案:合理使用上界或下界通配符,或设计为泛型方法

第八部分:动手练习

通过编码练习巩固泛型知识

练习1:通用转换器

创建一个泛型类 Converter<T, R>,实现不同类型转换的功能:

public class Converter<T, R> { private final Function<T, R> conversionFunction; public Converter(Function<T, R> conversionFunction) { this.conversionFunction = conversionFunction; } // 转换单个元素 public R convert(T input) { // 实现 } // 转换集合 public List<R> convertList(List<? extends T> inputList) { // 使用上界通配符 // 实现 } } // 使用示例:将String转换为Integer Converter<String, Integer> toIntConverter = new Converter<>(Integer::parseInt); List<String> strings = List.of("1", "2", "3"); List<Integer> ints = toIntConverter.convertList(strings);

练习2:安全过滤器

创建一个泛型方法,过滤集合中满足特定条件的元素:

public static <T> List<T> filter( List<? extends T> source, Predicate<? super T> predicate) { // 实现过滤逻辑 // 注意使用Producer-Extends和Consumer-Super } // 使用示例 List<Number> numbers = Arrays.asList(1, 2.5, 3, 4.2); List<Number> integers = filter(numbers, n -> n instanceof Integer);

练习3:列表转换器

创建一个泛型方法将List<List<?>>转换为List<?>

public static List<?> flatten(List<? extends List<?>> listOfLists) { // 实现展平多维列表 // 使用通配符处理嵌套集合 } // 使用示例 List<List<?>> nested = Arrays.asList( Arrays.asList(1, 2, 3), Arrays.asList("a", "b", "c") ); List<?> flat = flatten(nested); flat.forEach(System.out::println); // 输出: 1,2,3,a,b,c

练习4:类型安全的交换

创建一个泛型方法交换数组中两个元素的位置:

public static <T> void swap(T[] array, int i, int j) { // 实现元素交换 } // 挑战:创建一个方法交换List中两个元素 public static void swapElements( List<?> list, int i, int j) { // 使用通配符处理 // 注意类型擦除的限制 }