《Effective Java》解读第31条:利用有限制通配符来提升API的灵活性

第31条:利用有限制通配符来提升API的灵活

在API中使用通配符类型(? extends 和 ? super)可以使参数类型更加灵活,同时保持类型安全。 这是实现"PECS"原则(Producer-Extends, Consumer-Super)的关键技术。

泛型不变性

如第 28 条所述,参数化类型是不变的(invariant)。换句话说,对于任何两个截然不同的类型 Type1和 Type2 而言,ist既不是 List的子类型,也不是它的超类型。虽然 List不是 List<0bject>的子类型,这与直觉相悖,但是实际上很有意义,你可以将任何对象放进一个List<0bject>中,却只能将字符串放进List中。由于 List不能像 List能做任何事情,它不是一个子类型(详见第 10 条)。

例如:

java 复制代码
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // 编译错误!泛型不变量

虽然String是Object的子类,但List不是List的子类,这是泛型的不变量性带来的限制。

两种通配符

上界通配符(Producer):? extends T

回顾第29条中我们写的自定义泛型stack代码(省略细节):

java 复制代码
// 优先设计为泛型类
public class Stack<E>{
    public Stack() {....}
    public void push(E e) {....}
    public E pop() {....}
}

在其中添加一个新方法pushAll

java 复制代码
public void pushAll(Iterable<E> src) {
    for (E e : src) {
        push(e);
    }
}

当这样使用时:

java 复制代码
Stack<Number> stack = new Stack<>(10);
Iterable<Integer> src = Collections.singletonList(1);
stack.pushAll(src);

就会报错,正如开头所说,参数化类型不可变。

Java提供了一种特殊的参数化类型,称作有限制的通配符类型(bounded wildcard type),它可以处理类似的情况。pushAll的输人参数类型不应该为"E的 Iterable 接口",而应该为"E的某个子类型的Iterable接口"通配符类型Iterable<? extends E>正是这个意思。

java 复制代码
public void pushAll(Iterable<? extends E> src) {
    for (E e : src) {
        push(e);
    }
}

下界通配符(Consumer):? super T

编写popAll也是同样的道理。super和extends在泛型中正好反过来,这里表示E的某种超类的集合。

java 复制代码
public void popAll(Collection<? super E> dst) {
   while (size != 0) {
       dst.add(pop());
   }
}

结论

结论很明显:为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型匹配,这是不用任何通配符而得到的。

核心原则:PECS(Producer-Extends, Consumer-Super)

PECS含义:

  • Producer(生产者):产生/提供数据 → 使用 ? extends T
  • Consumer(消费者):消费/接受数据 → 使用 ? super T

要注意,这里的PECS中的"生产者"和"消费者"指的是参数本身的行为,而不是方法的功能,是向方法提供数据,还是从方法接收数据。如果使用得当,通配符类型对于用户应该是无感的。

  • 如果类型参数在方法中既作为生产者又作为消费者,就不要使用通配符。通配符是为了扩大方法接受参数的范围,而不是限制。
  • 所有的comparable和comparator都是消费者

看一个复杂的例子:

java 复制代码
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
  • 返回类型T
  • 方法参数:List<? extends T> list
  • 类型参数约束:<T extends Comparable<? super T>>
    • T extends Comparable<...> T必须实现Comparable接口,确保T可以被比较
    • Comparable<? super T> T可以实现Comparable,也可以实现Comparable<T的父类>

优先使用通配符的情况

当类型参数在方法声明中只出现一次时,考虑使用通配符:

java 复制代码
// 改进前
public static <E> void swap(List<E> list, int i, int j);

// 改进后(因为E只作为参数出现,没有其他依赖)
public static void swap(List<?> list, int i, int j);
java 复制代码
// 直接实现会有问题
public static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i))); // 编译错误!
}

// 通过私有辅助方法捕获类型
public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

// 辅助方法捕获通配符类型
private static <E> void swapHelper(List<E> list, int i, int j) {
    list.set(i, list.set(j, list.get(i))); // √
}

泛型确实是一个很绕的东西。

相关推荐
Rabbit_QL15 小时前
【水印添加工具】从零设计一个工程级 Python 图片水印工具:WaterMask 架构与实现
开发语言·python
张柏慈15 小时前
Java性能优化:实战技巧与案例解析
java
天“码”行空15 小时前
简化Lambda——方法引用
java·开发语言
z203483152015 小时前
C++对象布局
开发语言·c++
Beginner x_u16 小时前
如何解释JavaScript 中 this 的值?
开发语言·前端·javascript·this 指针
带刺的坐椅16 小时前
MCP 进化:让静态 Tool 进化为具备“上下文感知”的远程 Skills
java·ai·llm·agent·solon·mcp·tool-call·skills
java1234_小锋16 小时前
Java线程之间是如何通信的?
java·开发语言
张张努力变强16 小时前
C++ Date日期类的设计与实现全解析
java·开发语言·c++·算法
feifeigo12317 小时前
基于EM算法的混合Copula MATLAB实现
开发语言·算法·matlab
LYS_061817 小时前
RM赛事C型板九轴IMU解算(4)(卡尔曼滤波)
c语言·开发语言·前端·卡尔曼滤波