Java泛型之协变与逆变

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

不可变性(Invariance)

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

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

上面的代码中,即使Integer是Number的子类,但是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 类型或其子类型的元素,同时保持类型安全。

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

相关推荐
龙亘川1 分钟前
铭记英烈守初心,数字强基促服务:亘川智城退役军人服务系统的实践与价值
java·数据库·人工智能
OxYGC17 分钟前
[AI工程] Spring AI 第廿一篇:存量 REST 接口的 MCP 化改造实录——工具从哪条路径注册、参数描述怎么写、身份怎么过去
java·人工智能·spring
Zhou14113640 分钟前
SpringBoot_02_自动配置原理
java·spring boot·后端
m0_3807438744 分钟前
PHP7.3和7.2字符串功能有什么不同
开发语言·php
FYKJ_20101 小时前
SSM校园失物招领系统41452-计算机课程设计、毕业设计
java·vue.js·spring boot·python·mysql·typescript·spark
kv1101 小时前
Android studio国内开发有关
java·数据库·android studio
卓怡学长1 小时前
w198基于springboot始于足下健康打卡平台的设计与实现
java·spring boot·spring·intellij-idea
m0_380743871 小时前
PHP 8.4的新语法怎么用才规范
开发语言·php
头发够用的程序员1 小时前
TensorRT 自定义算子插件实战(一):从零手写 customScaledTanh
c++·人工智能·pytorch·python·深度学习·边缘计算·jetson
阿狗童鞋1 小时前
Java并发编程实战指南
java·开发语言