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 通配符的等价物)
kotlinfun 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 声明处变型(接口/类定义时)
kotlininterface 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) 易错点清单
- 想往 ? extends 里写 → 不行;那是生产者。要写就用 ? super / in。
- 把 List 当 List 传 → 不行;要用 ? extends Animal。
- Kotlin 把 MutableList 当可写 → 投影后几乎只读。
- 数组"看似方便" → 小心 ArrayStoreException;集合 + 通配更安全。
- 原始类型/平台类型(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 写清楚,既类型安全又好用;遇到"写不进去/读不出来",回到上面的读/写能力表对照一下,十有八九就对上了。
-