[简单粗暴]一文彻底搞懂Java泛型中的PECS原则(在坑里躺了多年终于爬出来了)

在Java编程中,泛型是一个强大的工具,能够提高代码的通用性和类型安全性。然而,许多开发者在使用泛型时会遇到困难,特别是关于PECS原则的理解。PECS,即 “Producer Extends, Consumer Super”,是使用泛型通配符的核心法则。本文将深入探讨PECS原则,通过详尽的代码示例和实战应用,帮助你彻底掌握泛型中的 <? extends T> 和 <? super T> 的用法。我们将解释为什么生产者(只读)使用 extends,消费者(只写)使用 super,并展示如何在实际开发中应用这一原则。通过本文,你将学会编写更灵活、安全和高效的Java代码。

彻底搞懂Java泛型中的PECS原则

在Java中,泛型(Generics)是一个强大的工具,它允许我们在编写代码时定义类、接口和方法时使用类型参数。泛型使得代码更具通用性、类型安全性和可读性。在泛型中,PECS原则是一个非常重要的概念。本文将详细讲解PECS原则,帮助大家更好地理解和应用。

什么是PECS原则?

PECS 是 “Producer Extends, Consumer Super” 的缩写。这句话的意思是:生产者(Producer)用 extends,消费者(Consumer)用 super。这个原则在使用泛型限定通配符时非常有用,可以帮助我们决定何时使用 extendssuper

两种限定通配符

  1. 类型的上界<? extends T> 表示类型必须为 T 类型或者 T 的子类。
  2. 类型的下界<? super T> 表示类型必须为 T 类型或者 T 的父类。

上代码

为了更好地解释这些概念,我们先定义一些类:

public class Fruit {
}

public class Apple extends Fruit {
}

public class Banana extends Fruit {
}

List<? extends Fruit> 的理解

正如字面意思,<? extends Fruit> 表示泛型的类型是 Fruit 或者 Fruit 的子类。也就是说,我们给 list 赋值时,泛型可以是 Fruit 或者 Fruit 的子类,比如 new ArrayList<Fruit>()new ArrayList<Apple>() 或者 new ArrayList<Banana>()

private static List<? extends Fruit> getExtendsList() {
    List<? extends Fruit> list;
    list = new ArrayList<Fruit>();
    list = new ArrayList<Apple>();
    list = new ArrayList<Banana>();
    return list;
}

具体来说,List<? extends Fruit> 允许我们将 List<Fruit>List<Apple>List<Banana> 都赋值给它。然而,虽然我们可以从这个列表中获取元素(因为我们知道它们至少是 Fruit 类型),但我们不能向其中添加元素(除了 null),因为编译器无法确定我们实际的列表类型。

private static void m1(List<? extends Fruit> list) {
    Fruit fruit = list.get(0); // 安全的,因为我们知道列表中至少是Fruit类型
    // list.add(new Apple()); // 错误,编译器不允许,因为list可能是new ArrayList<Banana>()
}

List<? super Fruit> 的理解

<? super Fruit> 表示泛型的类型是 Fruit 或者 Fruit 的父类。也就是说,我们给 list 赋值时,泛型可以是 Fruit 或者 Fruit 的父类,比如 new ArrayList<Fruit>() 或者 new ArrayList<Object>()

private static List<? super Fruit> getSuperList() {
    List<? super Fruit> list;
    list = new ArrayList<Fruit>();
    list = new ArrayList<Object>();
    return list;
}

具体来说,List<? super Fruit> 允许我们向列表中添加 Fruit 或者 Fruit 的子类,但我们不能安全地获取特定类型的元素,只能获取 Object 类型的元素。

private static void m2(List<? super Fruit> list) {
    list.add(new Apple()); // 可以,因为Apple是Fruit的子类
    list.add(new Banana()); // 可以,因为Banana是Fruit的子类
    Object object = list.get(0); // 只能是Object类型,因为我们不知道具体的类型
}

总结

  1. List<? extends Fruit> list:表示泛型的类型是 FruitFruit 的子类,一般用于只获取元素。
  2. List<? super Fruit> list:表示泛型的类型是 FruitFruit 的父类,一般用于只添加元素(获取出来的元素是 Object 类型,泛型意义不大)。
  3. List list:明确的泛型,可以获取元素,也可以添加元素,是最常用的泛型。

JDK中的PECS

在JDK中,PECS原则被广泛应用。例如,java.util.Collections 类中的 copy 方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i = 0; i < srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di = dest.listIterator();
        ListIterator<? extends T> si = src.listIterator();
        for (int i = 0; i < srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

在这个方法中,dest 是一个消费者,只接受元素,所以使用 super;而 src 是一个生产者,只提供元素,所以使用 extends

深入理解泛型通配符

为了更深入理解,我们可以考虑以下几个方面:

  1. 协变(Covariance)和逆变(Contravariance)

    • <? extends T> 是协变的,这意味着如果 AppleFruit 的子类,那么 List<Apple> 也是 List<? extends Fruit> 的子类型。
    • <? super T> 是逆变的,这意味着如果 FruitApple 的父类,那么 List<Fruit> 也是 List<? super Apple> 的父类型。
  2. 泛型方法中的PECS

    • 在定义泛型方法时,PECS原则同样适用。通过明确指定泛型参数的上下界,可以更好地控制方法的输入和输出。
public static <T> void addToList(List<? super T> list, T item) {
    list.add(item);
}

public static <T> void copyList(List<? extends T> source, List<? super T> destination) {
    for (T item : source) {
        destination.add(item);
    }
}
  1. 实战应用
    • 在实际开发中,我们常常需要根据需求选择合适的泛型通配符。例如,在处理只读取数据的列表时,可以使用 <? extends T>;而在处理需要写入数据的列表时,可以使用 <? super T>

结语

理解和应用PECS原则是掌握Java泛型的重要一步。通过正确使用泛型通配符,我们可以编写出更为通用、灵活和类型安全的代码。希望这篇文章能够帮助你彻底搞懂Java泛型中的PECS原则,让你在编程的道路上少踩坑,多收获。

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,关于Java泛型类型的参数传入,我们需要先了解一下Java泛型的基本概念。 Java泛型是一种参数化类型的概念,即在定义类、接口或方法时,使用一个或多个类型参数来表示其的某些类型,这些类型参数在使用时再被具体化。通过使用泛型,可以使代码更加通用、安全和可读性更强。 Java泛型类型参数可以用于类、接口和方法的定义。在使用时,需要将具体的类型参数传递给它们,以指定其泛型类型。 下面以一个简单的例子来说明Java参数传入泛型类型的用法。 ``` public class Box<T> { private T data; public Box(T data) { this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } } ``` 在这个例子,我们定义了一个泛型类Box,其的类型参数T可以在类的定义被指定。在Box类的构造函数和getData、setData方法,我们使用了泛型类型T来表示其的某些类型。 现在我们可以创建一个Box对象,并将一个具体的类型参数传递给它,以指定其泛型类型。例如: ``` Box<Integer> box = new Box<Integer>(new Integer(10)); ``` 在这个例子,我们创建了一个Box对象,并将Integer类型作为泛型类型参数传递给它。这样一来,我们就可以在Box对象存储和获取Integer类型的数据了。 同样地,我们也可以创建其他类型的Box对象,例如: ``` Box<String> box = new Box<String>("Hello World!"); Box<Double> box = new Box<Double>(new Double(3.14)); ``` 通过这种方式,我们可以方便地定义、使用和重用泛型类型,从而使代码更加通用和灵活。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值