Java泛型之协变与逆变

Java中的泛型有两个重要特性:不可变性(invariance)和类型擦除(type erasure)。这两个概念在理解和使用泛型时非常重要。以下是对这两个特性的详细解释:

不可变性(Invariance)

在Java泛型中,不可变性意味着泛型类型是严格的,不会自动适应其子类或父类。举个例子:

java 复制代码
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
numberList = integerList; // 编译错误

上面的代码中,即使IntegerNumber的子类,但是List<Integer>并不是List<Number>的子类型。这就是不可变性。你不能将List<Integer>赋值给List<Number>,因为泛型类型是不变的。

为了解决这种问题,Java提供了通配符来处理类型的协变和逆变:

java 复制代码
List<? extends Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
numberList = integerList; // 允许

使用? extends Number表示可以赋值给任何Number的子类的列表。类似地,? super Integer表示任何Integer的父类的列表。

类型擦除(Type Erasure)

类型擦除是指在编译过程中,Java编译器会移除所有的泛型类型信息。这意味着在运行时,泛型类型的信息是不可用的。

例如:

java 复制代码
List<String> stringList = new ArrayList<>();
stringList.add("hello");
String str = stringList.get(0);

在编译时,stringList的类型是List<String>,但是在运行时,类型信息被擦除,stringList变成了List。编译器在编译时插入了类型转换代码,以确保类型安全:

java 复制代码
String str = (String) stringList.get(0);

类型擦除带来了一些限制,比如你不能在运行时获取泛型类型信息,也不能创建泛型类型的实例或数组:

java 复制代码
List<String>[] listArray = new List<String>[10]; // 编译错误

理解这两个概念对于正确使用Java泛型非常重要。不可变性确保了类型安全性,而类型擦除则是Java泛型实现的一部分,确保与Java旧版本的兼容性。

Java泛型中的逆变(contravariance)指的是允许使用某个泛型类型的超类型来替代该泛型类型。逆变通过通配符 ? super T 实现,表示接受 T 类型或 T 类型的任何超类型。这个概念主要用于写操作,确保我们可以向集合中添加元素。

逆变的示例

假设我们有以下类层次结构:

java 复制代码
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }

我们定义一个逆变的集合:

java 复制代码
List<? super Dog> animals = new ArrayList<Animal>();

这个声明表示 animals 可以是 Dog 的任何超类型的列表,例如 List<Animal>List<Object>

使用逆变的场景

逆变主要用于写入的场景,因为你可以确保向集合中添加的元素是 Dog 类型或其子类型。例如:

java 复制代码
animals.add(new Dog()); // 允许
animals.add(new Puppy()); // 允许,假设 Puppy extends Dog

但是,不能从逆变的集合中读取特定类型的元素,因为编译器无法确定从集合中读取的元素的具体类型,只能确定是某种类型的 Object

java 复制代码
Dog dog = animals.get(0); // 编译错误
Object obj = animals.get(0); // 允许

示例:逆变接口

假设我们设计一个处理动物的接口:

java 复制代码
interface AnimalHandler<T> {
    void handle(T animal);
}

我们可以实现这个接口:

java 复制代码
class DogHandler implements AnimalHandler<Dog> {
    @Override
    public void handle(Dog dog) {
        System.out.println("Handling a dog");
    }
}

利用逆变,我们可以定义一个方法,接受任何类型的 AnimalHandler

java 复制代码
public class Zoo {
    public static void processAnimal(AnimalHandler<? super Dog> handler, Dog dog) {
        handler.handle(dog);
    }

    public static void main(String[] args) {
        DogHandler dogHandler = new DogHandler();
        Dog dog = new Dog();

        processAnimal(dogHandler, dog); // Handling a dog
    }
}

在这个示例中,processAnimal 方法接受一个 AnimalHandler<? super Dog>,意味着我们可以传入处理 Dog 的任何超类型的处理器,例如 AnimalHandler<Animal>AnimalHandler<Object>

总结

逆变(? super T)使得我们可以更加灵活地处理泛型集合,特别是在写入操作中。通过使用 ? super T,我们可以向集合中添加 T 类型或其子类型的元素,同时保持类型安全。

理解逆变可以帮助我们设计更灵活和通用的接口和方法,特别是在处理继承层次结构中的类型时。

相关推荐
南京云森杉木桩6 分钟前
水利木桩源头直供,质量可靠价格更优
大数据·python
SQL-First布道者9 分钟前
持久层框架的评价标准:只有一个
java·spring boot·spring·tomcat·mybatis·spring jdbc
孔明click3313 分钟前
Sa-Token v1.46.0 发布 🚀,来看看有没有令你心动的功能!
java·sa-token·开源·springboot·权限认证
Aaron - Wistron15 分钟前
Python基础教程2/4(复合数据结构)
python
小柯南敲键盘19 分钟前
跨境电商图片翻译与视频字幕翻译工具推荐
python·音视频
ZJU_统一阿萨姆21 分钟前
【推理优化进阶】通信关键路径:NCCL、RDMA 与计算通信重叠
开发语言·人工智能·语言模型·架构·系统架构
m0_5873830036 分钟前
智慧场馆解决方案实战指南:从系统架构到落地部署全解析
java·spring boot·架构·系统架构
我不是疯子是傻子38 分钟前
Qt 程序打包部署全攻略:从 windeployqt 到 Inno Setup 的静默安装与版本迭代实战
开发语言·qt
ZC跨境爬虫41 分钟前
LeetCode 13. 罗马数字转整数(多解法详解 + Java Python 实现)
java·python·leetcode
省长1 小时前
Sa-Token v1.46.0 发布 🚀,来看看有没有令你心动的功能!
java·后端·开源