泛型协变疑问:ArrayList与List对嵌套类的类型兼容性差异
这个问题的核心在于泛型的可变性规则,以及 Kotlin/Java 对不同容器类型的泛型设计差异,咱们一步步拆解:
先搞懂核心概念:泛型的不变性
咱们都知道如果 PhotoItem 继承自 AbstractItem,那 PhotoItem 的实例可以直接赋值给 AbstractItem 类型的变量。但对于泛型容器来说,默认情况可不是这样的——ArrayList<PhotoItem> 不是 ArrayList<AbstractItem> 的子类型,这就是泛型的不变性。
为什么要这么设计?为了类型安全。如果允许把 ArrayList<PhotoItem> 当成 ArrayList<AbstractItem> 使用,那你就可以往这个列表里添加另一个 AbstractItem 的子类(比如 VideoItem),但原列表实际只能存 PhotoItem,后续取出元素时就会抛出类型转换异常。
Kotlin 中的差异原因
你在 Kotlin 里遇到的情况,本质是只读 List 和可变 MutableList/ArrayList 的泛型特性不同:
- MutableList/ArrayList 是不变的:
ArrayList<T>实现了MutableList<T>,而MutableList<T>是不变的(没有out修饰符)。所以ArrayList<PhotoItem>不能赋值给ArrayList<AbstractItem>,编译器会直接报错,避免刚才说的类型安全问题。 - 只读 List 是协变的:Kotlin 的只读
List<T>被定义为List<out T>,out关键字表示这个接口是协变的。也就是说,只要PhotoItem是AbstractItem的子类,List<PhotoItem>就可以安全地赋值给List<AbstractItem>——因为只读列表不允许添加元素,不会出现往列表里塞错类型的情况。这就是为什么把addItems的参数改成List<AbstractItem>就能编译通过。
Java 中的情况补充
在 Java 里,不管是 List 接口还是 ArrayList 类,泛型默认都是不变的。所以你写的 Java 代码里,不管 addItems 用 ArrayList<AbstractItem> 还是 List<AbstractItem>,传入 ArrayList<PhotoItem> 都会编译失败。要让它编译,得用通配符实现协变:
// 修改参数为带通配符的 List public void addItems(List<? extends AbstractItem> list) { int position = items.size(); items.addAll(list); notifyItemRangeInserted(position, list.size()); }
? extends AbstractItem 表示“任何继承自 AbstractItem 的类型”,这样就允许传入 ArrayList<PhotoItem> 这类列表了。
总结:不是接口 vs 具体类的差异
你感觉到的“接口和具体类的兼容性差异”,其实是 Kotlin 对只读/可变容器的泛型设计区别。如果在 Kotlin 里把参数改成 MutableList<AbstractItem>,同样会编译失败,和用 ArrayList<AbstractItem> 是完全一样的——本质都是因为可变容器的泛型是不变的,而只读容器是协变的。
内容的提问来源于stack exchange,提问作者CoolMind

