JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

逆变与协变:Java 泛型的“双面间谍”,带你轻松拿捏!

wys521 2024-12-07 22:36:27 精选教程 25 ℃ 0 评论

前言

“泛型已经够烧脑了,啥还来个逆变和协变?别啊,我放弃!”

别慌,今天我们用最通俗易懂的方式,告诉你 逆变协变 是什么、怎么用,以及它们出没的常见场景。看完保证你信心满满,直呼 “啊,原来这么简单!”


一、啥是逆变和协变?(搞清概念,打好地基)

要理解逆变和协变,先看个问题:

你有一群动物,DogCat 都是 Animal 的子类。如果你有一个 List<Animal>,你觉得能装 DogCatList 吗?

很抱歉,Java 的泛型系统会告诉你:不行!

List<Dog> dogList = new ArrayList<>();
List<Animal> animalList = dogList; // 报错!不兼容。

这是因为,泛型不支持子类型替换父类型,也就是所谓的 类型不变性

那么问题来了,我就是想灵活一点,怎么办? 这时,Java 泛型提供了两种“变通方式”:

  1. 协变(? extends T:向下兼容——"能看不能改" 容器里的类型可以是 TT 的子类,但你不能往里加东西(除了 null)。
  2. 逆变(? super T:向上兼容——"能改不能看" 容器里的类型可以是 TT 的父类,但你只能保证添加操作安全。

二、协变和逆变的代码演示

1. 协变(? extends T):容器中的元素,只能“读”不能“写”

例子:定义一个方法,打印任何类型的列表内容:

public static void printList(List<? extends Animal> list) {
    for (Animal animal : list) {
        System.out.println(animal);
    }
}

调用:

List<Dog> dogs = List.of(new Dog("旺财"), new Dog("小黑"));
List<Cat> cats = List.of(new Cat("咪咪"), new Cat("花花"));

printList(dogs); // 输出:旺财,小黑
printList(cats); // 输出:咪咪,花花

要点:

  • ? extends T 表示列表中的元素是 TT 的子类。
  • 只能读取列表中的内容,不能往里添加(除了 null)。
list.add(new Dog()); // 报错:禁止添加!
list.add(null);      // 允许,但慎用。

场景适用

  • 只读操作:当你只需要“遍历”和“读取”列表中的元素时,用协变。

2. 逆变(? super T):容器中的元素,只能“写”不能“读”

例子:定义一个方法,往列表中添加某种类型的动物:

public static void addAnimals(List<? super Dog> list) {
    list.add(new Dog("小黄"));
    list.add(new Dog("大黄"));
}

调用:

List<Animal> animals = new ArrayList<>();
addAnimals(animals); // 成功添加两只狗

List<Object> objects = new ArrayList<>();
addAnimals(objects); // 成功,因为 Object 是 Animal 的父类

要点:

  • ? super T 表示列表中的元素是 TT 的父类。
  • 可以安全地往列表中添加 T 或其子类,但不能保证读取出来的类型。
Object obj = list.get(0); // 只能当作 Object 处理
Dog dog = list.get(0);    // 报错:无法确认是 Dog!

场景适用

  • 只写操作:当你只需要“添加”特定类型的元素时,用逆变。

三、协变和逆变的终极场景(大显身手)

1. 协变场景:动物园的饲养员(只看不动手)

饲养员想知道有哪些动物,但不负责加新动物:

public static void showAnimals(List<? extends Animal> animals) {
    for (Animal animal : animals) {
        System.out.println("发现了:" + animal);
    }
}

调用:

List<Dog> dogs = List.of(new Dog("大白"), new Dog("二哈"));
showAnimals(dogs);

2. 逆变场景:动物园的管理员(只加不查看)

管理员要把动物关进笼子,但不关心笼子里原来有什么:

public static void cageAnimals(List<? super Dog> cages) {
    cages.add(new Dog("哈士奇"));
    cages.add(new Dog("柯基"));
}

调用:

List<Animal> cages = new ArrayList<>();
cageAnimals(cages);

3. 协变 + 逆变场景:大棚收割机(双管齐下!)

既要读取,又要添加,怎么办?分开两步来解决!

// 读取
List<? extends Fruit> fruits = new ArrayList<>();
Fruit fruit = fruits.get(0); // 可以读
// 添加
List<? super Apple> apples = new ArrayList<>();
apples.add(new Apple()); // 可以写

四、终极口诀(方便记忆!)

协变 extends:只能“读”,不能“写”;上限是 TT 的子类。 逆变 super:只能“写”,不能“读”;下限是 TT 的父类。

记住一句话: “能读就别写,能写就别读,想全能,那是不可能!”


五、总结

协变和逆变,是 Java 泛型中非常强大的工具,尤其适合应对复杂的类型安全问题。

  • 协变:适用于只读场景,比如迭代器、打印列表。
  • 逆变:适用于只写场景,比如批量添加数据。

掌握了它们,你就能在写代码时游刃有余,灵活应对千变万化的需求!


希望这篇文章能让你在协变与逆变的迷雾中拨云见日,写出让自己满意、同事膜拜的代码!

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表