1. 概述

本文将深入探讨 Java 泛型中类型参数通配符的核心区别及正确使用方式。很多开发者容易混淆这两者,尤其在编写泛型方法时,常纠结该用哪种形式。我们将通过实际场景分析,帮你彻底搞懂这个问题。

2. 泛型类

在 Java 5 引入泛型后,我们得以创建类型参数化的类和接口。定义泛型类时必须使用类型参数,例如 java.lang.Comparable 接口:

public interface Comparable<T> {
    public int compareTo(T o);
}

这里 T 是类型参数,在整个接口范围内可用。实例化时需提供具体类型(如 String)。⚠️ 注意:通配符不能用于定义泛型类或接口

3. 泛型方法

3.1. 方法参数

编写泛型方法时,类型参数是基础。比如打印任意类型的方法:

public static <T> void print(T item) {
    System.out.println(item);
}

不能直接用通配符定义参数类型,通配符只能作为泛型类型参数的一部分使用。再看两种 swap() 方法声明:

// 方式1:无界类型参数
public static <E> void swap(List<E> list, int src, int des);

// 方式2:无界通配符
public static void swap(List<?> list, int src, int des);

优先使用通配符版本:当类型参数在方法声明中只出现一次时,通配符能让代码更简洁灵活。这个规则同样适用于有界类型参数。

3.2. 返回类型

考虑一个合并列表的方法,使用通配符作为返回类型:

public static <E> List<? extends E> mergeWildcard(
    List<? extends E> listOne, 
    List<? extends E> listTwo
) {
    return Stream.concat(listOne.stream(), listTwo.stream())
                 .collect(Collectors.toList());
}

合并两个 List<Number> 时,问题出现了:

List<Number> numbers1 = new ArrayList<>();
numbers1.add(5);
numbers1.add(10L);

List<Number> numbers2 = new ArrayList<>();
numbers2.add(15f);
numbers2.add(20.0);

// 编译失败!
List<Number> numbersMerged = CollectionUtils.mergeWildcard(numbers1, numbers2);

通配符返回类型会强制客户端处理类型问题。正确做法是使用类型参数

public static <E> List<E> mergeTypeParameter(
    List<? extends E> listOne, 
    List<? extends E> listTwo
) {
    return Stream.concat(listOne.stream(), listTwo.stream())
                 .collect(Collectors.toList());
}

这样就能正确接收 List<Number> 类型结果。

4. 类型边界

泛型边界能限制可用的类型,实现多态处理。主要有三种通配符边界:

  • 无界通配符:List<?> → 任意类型列表
  • 上界通配符:List<? extends Number>Number 或其子类型(如 Integer
  • 下界通配符:List<? super Integer>Integer 或其父类型(如 Number

类型参数边界则有两种形式:

  • 无界类型参数:List<T> → 类型 T 的列表
  • 有界类型参数:List<T extends Number & Comparable> → 实现 ComparableNumber 子类型

⚠️ 关键区别

  • 类型参数不支持下界
  • 类型参数可设置多重边界,通配符不能

4.1. 上界类型

泛型类型是不可变的。虽然 LongNumber 的子类,但 List<Long> 不是 List<Number> 的子类。看个求和方法的踩坑案例:

// 错误实现:只能接受 List<Number>
public static long sum(List<Number> numbers) {
    return numbers.stream().mapToLong(Number::longValue).sum();
}

List<Integer> integers = Arrays.asList(1, 2, 3);
// 编译失败!List<Integer> 不是 List<Number>
sum(integers);

解决方案:使用上界通配符

public static long sumWildcard(List<? extends Number> numbers) {
    return numbers.stream().mapToLong(Number::longValue).sum();
}

// 现在可以接受任何 Number 子类型列表
List<Integer> integers = Arrays.asList(1, 2, 3);
sumWildcard(integers);

等价类型参数实现:

public static <T extends Number> long sumTypeParameter(List<T> numbers) {
    return numbers.stream().mapToLong(Number::longValue).sum();
}

4.2. 下界类型

下界只能用于通配符(类型参数不支持)。使用 super 关键字指定最低层级类型。假设要向列表添加整数:

public static void addNumber(List<? super Integer> list, Integer number) {
    list.add(number);
}

这样可接受 List<Integer>List<Number>List<Object>。✅ 遵循 PECS 原则

  • 生产者(Producer)用 extends:只读取元素时
  • 消费者(Consumer)用 super:只添加元素时
  • 既读又写?用无界类型

4.3. 无界类型

修改集合时使用通配符要小心。看 swap() 方法的经典踩坑:

// 编译失败!
public static void swap(List<?> list, int srcIndex, int destIndex) {
    list.set(srcIndex, list.set(destIndex, list.get(srcIndex)));
}

原因:通配符 ? 表示未知类型,编译器无法保证 set() 操作的类型安全。解决方案是使用辅助方法捕获通配符类型

private static <E> void swapHelper(List<E> list, int src, int des) {
    list.set(src, list.set(des, list.get(src)));
}

public static void swap(List<?> list, int src, int des) {
    swapHelper(list, src, des);
}

4.4. 多重边界

当需要多重约束时,类型参数是唯一选择。定义动物类层次:

abstract class Animal {
    protected final String type;
    protected final String name;

    protected Animal(String type, String name) {
        this.type = type;
        this.name = name;
    }

    abstract String makeSound();
}

class Dog extends Animal {
    public Dog(String type, String name) {
        super(type, name);
    }

    @Override
    public String makeSound() {
        return "Wuf";
    }
}

class Cat extends Animal implements Comparable<Cat> {
    public Cat(String type, String name) {
        super(type, name);
    }

    @Override
    public String makeSound() {
        return "Meow";
    }

    @Override
    public int compareTo(Cat cat) {
        return this.name.length() - cat.name.length();
    }
}

需要排序且可比较的动物列表时:

public static <T extends Animal & Comparable<T>> void order(List<T> list) {
    list.sort(Comparable::compareTo);
}

效果List<Cat> 可通过编译,但 List<Dog> 会失败(未实现 Comparable)。

5. 总结

  • 优先通配符:编写通用库时,通配符能提供更大灵活性
  • PECS 原则:生产者用 extends,消费者用 super
  • 返回类型用类型参数:避免将类型问题抛给客户端
  • ⚠️ 注意限制:通配符不支持多重边界,类型参数不支持下界

掌握这些差异后,你就能写出既类型安全又灵活优雅的泛型代码。完整示例代码可在 GitHub 获取。


原始标题:Type Parameter vs Wildcard in Java Generics