泛型变体与通配符(PECS)+ 集合

0) 一句话记忆:PECS

PECS = Producer Extends, Consumer Super

  • 生产者(只"产出"给你读)→ ? extends T / out T
  • 消费者(只"消费"你写进去)→ ? super T / in T
  • 又读又写 → 不变(不用通配/投影)

1) 三个概念:协变/逆变/不变

  • 协变 (covariant):"方向同向"。若 Cat <: Animal,则 Box <: Box。安全条件:只读
  • 逆变 (contravariant):"方向相反"。Cat <: Animal,则 Comparator <: Comparator。安全条件:只写
  • 不变 (invariant):Box 与 Box没有子类型关系。同时读写时保持不变才安全。

2) Java:通配符与集合

2.1 读/写能力表(把它贴墙上)

形态 能读成 能写入
List T T
List<? extends T> T(及其父类引用接收) 不能写(除 null)
List<? super T> 只能读成 Object(或 T 的上界中最宽) T 或其子类
List<?> Object 不能写(除 null)

为什么 ? extends 不能写?****

实参可能是 List 或 List,你不能往里放 Long------编译器无法保证安全。

2.2 典型 API 签名(都在用 PECS)

java 复制代码
// 从 src 读(生产者 extends),往 dst 写(消费者 super)
public static <T> void copy(List<? super T> dst, List<? extends T> src)

// 排序:比较器是"消费者"(它消费 T 作为参数)→ 逆变
public static <T> void sort(List<T> list, Comparator<? super T> c)

2.3 可抄代码

javascript 复制代码
// 复制
static <T> void copy(List<? super T> dst, List<? extends T> src) {
  for (T e : src) dst.add(e);
}

// 累加到父类型容器
static void addInts(List<? super Number> out, List<? extends Integer> in) {
  for (Integer x : in) out.add(x);
}

2.4 通配符捕获(Wildcard Capture)小技巧

csharp 复制代码
// 交换 List<?> 会写不进去,改用捕获辅助方法
static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); }
private static <T> void swapHelper(List<T> list, int i, int j) {
  T tmp = list.get(i); list.set(i, list.get(j)); list.set(j, tmp);
}

2.5 数组对比(面试常考)

  • Java 数组协变 :String[] 是 Object[],但可能 ArrayStoreException

  • 泛型集合不协变 :List 不是 List,靠通配符表达"读/写意图"。


    3) Kotlin:声明处变型 + 使用处投影 + 集合差异

    3.1 集合的变型设计

    • List:协变只读接口(天然"生产者")。

    • MutableList:不变(要读要写都安全)。

    • Kotlin 的数组 Array 不协变(需投影)。

    3.2 使用处投影(Java 通配符的等价物)

    kotlin 复制代码
    fun readAll(xs: MutableList<out Number>) { val n: Number = xs[0] /* 不能 add */ }
    fun writeAll(xs: MutableList<in Number>) { xs.add(1); val a: Any? = xs[0] }
    • MutableList ≈ List<? extends Number>

    • MutableList<in Number> ≈ List<? super Number>

    3.3 声明处变型(接口/类定义时)

    kotlin 复制代码
    interface Source<out T> { fun get(): T }   // 协变,只能产出
    interface Sink<in T>   { fun put(x: T) }   // 逆变,只能消费

    3.4 星投影

    • List<*>:未知元素类型的只读视图,可读 Any?,不可写(除 null)。对标 Java 的 List<?>。

    3.5 可抄代码

    kotlin 复制代码
    // Kotlin 版拷贝:src 只读(out),dst 只写(in)
    fun <T> copy(src: List<out T>, dst: MutableList<in T>) {
      for (e in src) dst.add(e)
    }
    
    // 函数类型:参数逆变,返回协变
    val f: (Number) -> Any = { it }  // OK:P 用 in,R 用 out

    4) 设计与实践:怎么给自己的 API 定类型

    • 只读参数 → ? extends T / out T

      例:sum(List<? extends Number>)、fun printAll(List)

    • 只写参数 → ? super T / in T

      例:copy(dst: MutableList, src: List)

    • 既读又写 → 不要通配/投影:List 或 MutableList

    • 返回值 → 一般不用通配(让调用者拿到具体 T 更友好)

    • Comparator / Predicate / Consumer 这类函数对象:逆变参数(<? super T>、in T)

    • Stream/Flow 管道:倾向只读输入(extends/out)+ 具体化输出


    5) 易错点清单

    1. 想往 ? extends 里写 → 不行;那是生产者。要写就用 ? super / in。
    2. 把 List 当 List 传 → 不行;要用 ? extends Animal。
    3. Kotlin 把 MutableList 当可写 → 投影后几乎只读。
    4. 数组"看似方便" → 小心 ArrayStoreException;集合 + 通配更安全。
    5. 原始类型/平台类型(Java 互操作) → 谨慎,容易把类型错误推迟到运行时。

    6) 迷你速查卡(R/W 能力一目了然)

    Java

    • List<? extends T>:读 T,不写

    • List<? super T>:读 Object ,写 T

    • List<?>:读 Object,不写

    Kotlin

    • List:读 T(只读接口)
    • MutableList:读 T,不写
    • MutableList:写 T ,读 Any?
    • List<*>:读 Any? ,不写

    7) 一段"综合题"示例(从签名看出变体意图)

    javascript 复制代码
    // 合并若干子类列表到父类列表
    static <T> void mergeInto(List<? super T> dst, List<? extends T>... parts) {
      for (List<? extends T> p : parts) dst.addAll(p);
    }
    kotlin 复制代码
    // 过滤后拷贝:读取 src(out),写入 dst(in)
    fun <T> filterCopy(
      src: List<out T>,
      dst: MutableList<in T>,
      keep: (T) -> Boolean
    ) {
      for (e in src) if (keep(e)) dst.add(e)
    }

    一句话收束

    先判断你的参数是"只读还是只写" :只读 → extends/out,只写 → super/in,读写皆要 → 不变。用这套把集合 API 写清楚,既类型安全又好用;遇到"写不进去/读不出来",回到上面的读/写能力表对照一下,十有八九就对上了。

相关推荐
shepherd1117 小时前
JDK 8钉子户进阶指南:十年坚守,终迎Java 21升级盛宴!
java·后端·面试
南北是北北7 小时前
界类型参数、递归边界与交叉类型
面试
南北是北北7 小时前
java&kotlin泛型语法详解
面试
前端缘梦7 小时前
Webpack 5 核心升级指南:从配置优化到性能提升的完整实践
前端·面试·webpack
LL_break8 小时前
线程1——javaEE 附面题
java·开发语言·面试·java-ee
王中阳Go8 小时前
面试官:“聊聊最复杂的项目?”90%的人开口就凉!我面过最牛的回答,就三句话
java·后端·面试
聪明的笨猪猪9 小时前
Java Spring “Bean” 面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
绝无仅有10 小时前
面试真实经历某节跳动大厂Java和算法问答以及答案总结(一)
后端·面试·github
绝无仅有10 小时前
某大厂跳动面试:计算机网络相关问题解析与总结
后端·面试·github