《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))); // √
}

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

相关推荐
哈哈老师啊1 小时前
Springboot就业管理系统bk5uv(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
java·spring boot·spring
chao1898441 小时前
基于C#实现Modbus通信及CRC校验
java·开发语言·c#
hunjinYang1 小时前
源码配置——基于Gradle搭建spring-framework-6.2.15版本阅读环境
java·后端·spring
编程饭碗1 小时前
【Spring全局异常处理 早抛晚捕】
java·数据库·spring
咸鱼2.01 小时前
【java入门到放弃】Elasticsearch概念
java·elasticsearch·jenkins
hxjhnct2 小时前
JavaScript Promise 的常用API
开发语言·前端·javascript
xiaowu0802 小时前
C# 嵌入资源加载 + 外部配置文件的兜底配置
开发语言·c#
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于JSP论坛系统设计与实现为例,包含答辩的问题和答案
java·开发语言
找不到、了2 小时前
系统常用的限流方案实践
java
FAFU_kyp2 小时前
Rust 语法速查
开发语言·后端·rust