实用指南:10分钟理解泛型的通配符(extends, super, ?)

泛型通配符是什么?

在学习Java泛型的过程中,我们经常会遇到三个特殊的符号:?extendssuper。它们组合起来形成的通配符(wildcard)是泛型中最让人头疼的部分,但也是最强大的特性之一。

简单来说,通配符就是用来表示"某种不确定的类型"。就像我们在填表时遇到的"其他"选项一样,当具体类型不重要或者无法确定时,就可以用通配符来代替。

三种基本通配符

1. 无边界通配符:<?>

这是最简单的通配符形式。<?> 表示"某个未知的类型"。

List<?> list = new ArrayList<String>();  // 可以指向任何类型的List
  list = new ArrayList<Integer>();         // 也可以重新赋值为其他类型

使用 <?> 的限制是:你不能向其中添加除了 null 以外的任何对象,因为你不知道实际的类型是什么。

List<?> list = new ArrayList<String>();
  list.add(null);     // 这个可以
  // list.add("hello");  // 编译错误!
2. 上界通配符:<? extends T>

这种写法表示"某个继承自 T 的类型",或者说"某种 T 类型或其子类型"。

List<? extends Number> numbers = new ArrayList<Integer>();  // Integer是Number的子类
  numbers = new ArrayList<Double>();   // Double也是Number的子类

为什么要叫"上界"?因为 [Number]是上限,实际类型必须是 [Number]或其子类。

使用上界通配符时的限制:

  • 可以安全地从中读取数据(因为读到的肯定能当成 [Number]处理)
  • 不能向其中写入数据(因为不知道具体是哪种子类型)
List<? extends Number> numbers = new ArrayList<Integer>();
  Number num = numbers.get(0);  // 可以读取,肯定是Number类型
  // numbers.add(new Integer(1));  // 编译错误!
3. 下界通配符:<? super T>

这种写法表示"某个 T 的父类型",也就是"某种 T 类型或其父类型"。

List<? super Integer> integers = new ArrayList<Number>();   // Number是Integer的父类
  integers = new ArrayList<Object>();  // Object是所有类的父类

这里 Integer 是下限,实际类型必须是 Integer 或其父类。

使用下界通配符时的特点:

  • 可以安全地向其中写入T 类型的数据
  • 从中读取数据时只能当作 Object 处理(因为不知道具体是哪个父类)
List<? super Integer> integers = new ArrayList<Number>();
  integers.add(new Integer(1));    // 可以写入Integer
  integers.add(42);                // 自动装箱也行
  // Integer num = integers.get(0);   // 错误!只能当Object用
  Object obj = integers.get(0);    // 正确!

PECS原则:Producer Extends, Consumer Super

这是使用泛型通配符的重要口诀:

  • Producer Extends:如果你只是从容器中获取数据(生产者),使用 <? extends T>
  • Consumer Super:如果你只是向容器中放入数据(消费者),使用 <? super T>

让我们通过一个具体的例子来理解:

public static void copy(List<? extends Number> source,
  List<? super Number> destination) {
    for (Number num : source) {        // source是生产者,用extends
    destination.add(num);          // destination是消费者,用super
    }
    }

在这个例子中:

  • [source]列表我们只从中读取数据,所以用 <? extends Number>
  • [destination]列表我们只向其中写入数据,所以用 <? super Number>

实际应用举例

1. 方法参数设计
// 错误的设计:太严格了
public static void printList(List<Object> list) {
  for (Object item : list) {
  System.out.println(item);
  }
  }
  // 正确的设计:灵活得多
  public static void printList(List<?> list) {
    for (Object item : list) {
    System.out.println(item);
    }
    }
    // 使用
    List<String> strings = Arrays.asList("a", "b", "c");
      List<Integer> integers = Arrays.asList(1, 2, 3);
        printList(strings);    // 现在可以用了
        printList(integers);   // 这个也可以
2. 集合操作工具方法
// 向一个列表中添加另一个列表的所有元素
public static <T> void addAll(List<? super T> dest, List<? extends T> src) {
  for (T item : src) {
  dest.add(item);
  }
  }
  // 使用
  List<Object> objects = new ArrayList<>();
    List<String> strings = Arrays.asList("hello", "world");
      addAll(objects, strings);  // 可以把String列表加到Object列表中

常见误区和注意事项

1. 通配符不能用于创建对象
// 错误!
// List<?> list = new ArrayList<?>();
// 正确的做法
List<?> list = new ArrayList<String>();
2. 通配符不能作为泛型方法的实际类型参数
public static <T> void method(List<T> list) {
  // ...
  }
  // 错误!
  // method(new ArrayList<?>());
  // 正确的做法
  List<String> stringList = new ArrayList<>();
    method(stringList);
3. 理解边界的方向

很多人容易搞混 extendssuper 的方向:

// extends: 设置上界(上限)
List<? extends Number>  // 实际类型是Number或其子类
  // super: 设置下界(下限)  
  List<? super Number>    // 实际类型是Number或其父类

总结

泛型通配符的核心思想是为了增加代码的灵活性:

  1. <?>:完全不知道类型,只能读取为 Object,基本不能写入
  2. <? extends T>:知道是 T 或其子类,可以安全读取为 T,不能写入
  3. <? super T>:知道是 T 或其父类,可以安全写入 T,读取只能当 Object

记住PECS原则,在设计API时合理使用通配符,可以让代码既安全又灵活。刚开始接触时会觉得复杂,但多练习几次就很容易掌握了。

posted on 2025-11-24 10:06  ljbguanli  阅读(0)  评论(0)    收藏  举报