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 类型或其子类型的元素,同时保持类型安全。

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

相关推荐
海尔源码5 分钟前
支持多语言的开源 Web 应用
java
pitepa11 分钟前
安装 PyCharm
ide·python·pycharm
喜欢新新子13 分钟前
pycharm 中文字体报错
ide·python·pycharm
RunsenLIu19 分钟前
基于Flask前后端分离智慧安防小区系统
后端·python·flask
dragon090730 分钟前
Python打卡day49!!!
开发语言·python
摩天崖FuJunWANG32 分钟前
c语言中的hashmap
java·c语言·哈希算法
LUCIAZZZ36 分钟前
Java设计模式基础问答
java·开发语言·jvm·spring boot·spring·设计模式
IsPrisoner39 分钟前
Go 语言实现高性能 EventBus 事件总线系统(含网络通信、微服务、并发异步实战)
开发语言·微服务·golang
一个天蝎座 白勺 程序猿1 小时前
Python爬虫(53)Python爬虫数据清洗与分析实战:Pandas+Great Expectations构建可信数据管道
爬虫·python·pandas