Java泛型中的PECS 原则

在 Java 泛型中,使用 extendssuper 关键字来定义通配符的上界和下界,主要是为了保证类型安全,并且能够灵活地处理不同类型的集合。具体来说,使用 extendssuper 的原因可以通过理解 PECS(Producer Extends, Consumer Super)原则来解释。

PECS 原则

  • Producer Extends: 如果需要一个只读的泛型集合(生产者),使用 extends
  • Consumer Super: 如果需要一个只写的泛型集合(消费者),使用 super

当使用泛型通配符 <? extends T> 时,集合被视为生产者,即我们可以从集合中读取数据,但不能向集合中添加数据。让我们深入了解这句话的含义。

理解 extends 用于生产者

使用 <? extends T> 通配符意味着这个集合可以是 T 类型或 T 的子类型。这种设计的目的是为了从集合中读取数据时保证类型安全,但是它也限制了向集合中添加数据的能力。

具体例子
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<? extends Number> list = new ArrayList<Integer>();
        // list.add(1); // 编译错误
        Number num = list.get(0); // 可以读取数据
    }
}

为什么不能添加数据

当我们使用 <? extends T> 通配符时,编译器无法确定集合的具体类型。它只知道集合中的元素类型是 TT 的子类型,但是不能确定具体是哪一个子类型。由于这一点,向集合中添加元素会带来类型安全的问题。

编译器无法确定类型一致性

考虑以下代码:

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<? extends Number> list = new ArrayList<Integer>();
        // 编译器不知道 list 是什么类型的子类型
        // list.add(1); // 编译错误
    }
}

在这个例子中,list 的实际类型是 ArrayList<Integer>,但是编译器只知道它是某种 Number 的子类型。编译器无法确定 list 实际上是 ArrayList<Integer>,所以它也不能保证我们要添加的元素类型是否与集合的实际类型一致。

如果编译器允许我们向 list 中添加元素,可能会导致运行时错误。考虑这种情况:

List<? extends Number> list = new ArrayList<Integer>();
list.add(3.14); // 如果编译器允许这行代码,会导致运行时错误

在这里,list 实际上是一个 ArrayList<Integer> 类型的集合。如果编译器允许我们添加 3.14 这样的 Double 类型元素,这会导致类型不一致的问题,最终导致运行时错误。

阅读数据是安全的

当我们从集合中读取数据时,使用 <? extends T> 是安全的,因为所有的元素至少是 T 类型,因此可以安全地赋值给 T 类型的变量。例如:

List<? extends Number> list = new ArrayList<Integer>();
Number num = list.get(0); // 读取是安全的

在这里,编译器知道集合中的元素是 Number 的子类型,因此可以安全地将元素赋值给 Number 类型的变量。

总结

  • <? extends T> 用于生产者:集合可以是 T 类型或 T 的子类型。这个设计确保从集合中读取数据时是安全的。
  • 不能向集合中添加数据:由于编译器无法确定集合的具体子类型,所以不能添加元素到集合中,防止类型不一致的问题。
  • 读取数据是安全的:因为所有元素都是 T 的子类型,读取操作是安全的,可以保证类型一致性。

通过理解这一点,可以帮助我们编写更加安全和灵活的泛型代码,避免潜在的类型转换问题。


在 Java 泛型中,使用通配符 <? super T> 指定类型的下界,通常用于消费者场景,即只写操作。这种设计可以确保向集合中添加数据是类型安全的,但读取数据时会有一些限制。具体来说,不能假设读取的数据类型,因为集合中可能包含 T 的父类型的对象。

理解 super 用于消费者

当使用 <? super T> 时,表示这个集合可以持有 T 类型及其任何父类型的对象。这种设计适用于将数据添加到集合中的场景(即消费者)。

具体例子
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<? super Integer> list = new ArrayList<Number>();
        list.add(1); // 可以添加 Integer 类型
        list.add(2);

        // 编译器无法确定读取的数据类型
        // Integer num = list.get(0); // 编译错误
        Object obj = list.get(0); // 可以读取为 Object 类型
    }
}

为什么不能假设读取的数据类型

当我们使用 <? super T> 通配符时,编译器知道集合可以包含 T 类型或 T 的任何父类型的对象。这意味着集合中的元素类型可能比 T 更广泛,可能是 T 的任意父类。因此,编译器不能保证从集合中读取的对象类型是 T

编译器无法确定类型一致性

考虑以下代码:

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<? super Integer> list = new ArrayList<Number>();
        list.add(1); // 可以添加 Integer 类型
        list.add(2);

        // 编译器不知道 list 中的元素确切类型
        // Integer num = list.get(0); // 编译错误
    }
}

在这个例子中,list 的实际类型是 ArrayList<Number>,但编译器只知道它是某种 Integer 的父类型的集合。编译器不能确定 list 实际上是 ArrayList<Number>,所以它也不能保证从 list 中读取的元素类型是 Integer

如果编译器允许我们将读取的数据赋值给 Integer,可能会导致类型转换错误。考虑这种情况:

List<? super Integer> list = new ArrayList<Number>();
list.add(1); // 可以添加 Integer 类型
list.add(2);

// 假设编译器允许如下操作:
Integer num = list.get(0); // 如果编译器允许这行代码,可能导致 ClassCastException

在这里,list 实际上是一个 ArrayList<Number> 类型的集合。如果编译器允许我们将 list.get(0) 的返回值赋值给 Integer 类型的变量,而实际存储的是 Number 类型的对象(比如 Double),则会在运行时抛出 ClassCastException

阅读数据的限制

因为集合中可以包含 T 的任意父类型,编译器只能确保从集合中读取的对象是 Object 类型(因为 Object 是所有类的父类)。这意味着我们不能直接将读取的数据赋值给 T 类型的变量。

示例代码
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<? super Integer> list = new ArrayList<Number>();
        list.add(1); // 可以添加 Integer 类型
        list.add(2);

        // 读取数据时只能确保是 Object 类型
        Object obj = list.get(0);
        System.out.println(obj);
    }
}

总结

  • <? super T> 用于消费者:表示集合可以持有 T 类型及其父类型的对象。这个设计确保向集合中添加 T 类型的对象是安全的。
  • 读取数据的限制:由于集合中可以包含 T 的父类型的对象,编译器无法确定从集合中读取的数据类型。因此,读取操作只能返回 Object 类型,不能假设读取的数据类型是 T
  • 类型安全:使用 super 可以保证添加操作的类型安全,但读取操作的类型不确定性需要通过类型检查和转换来处理。

通过理解这一点,可以帮助我们编写更加灵活和类型安全的泛型代码,特别是在处理需要添加和读取数据的集合时。

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青衫客36

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值