深究Java泛型编程中的PECS原则

目录

一、Java泛型

二、泛型通配符

三、类型擦除

四、PECS原则

Producer Extends

Consumer Super

PECS的应用


           倘若是应用程序员,对这些知识点是一扫而过,但库程序员对此可马虎不得。

一、Java泛型

        Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

        泛型的本质是“ 参数化类型 ”,也就是说把数据类型看作参数

        泛型的引入是为了解决Java集合类在JDK 1.5之前只能存储Object类型,导致在取出元素时需要进行显式类型转换的问题。通过泛型,可以在编译时检查类型匹配,避免运行时ClassCastException

        为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。

二、泛型通配符

泛型通配符 用于泛型的上下文中,增加了代码的灵活性。

  • 无界通配符< ? >表示可以接受任何类型;
  • 上界通配符< ? extends T >表示可以接受类型T或其子类型;
  • 下界通配符< ? super T >表示可以接受类型T或其父类型。

不受限制的通配符类型<?>意味着任何东西都可以在这里出现。但这也意味着你不能在这里添加任何东西,因为编译器不知道里面是什么。 

注意:只能对不可类型转化的类和接口进行instanceof检查,这在类型转化的类和接口中是不可行的,因为类型信息在运行时已经不存在了。 

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

class Main {
    public static void main(String[] args) {
        // 创建一个List<Integer>,由于类型擦除,运行时它实际上是List<Object>
        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);

        /* 下面这段代码会产生编译错误,因为编译时类型擦除使得List<Integer>和List<String>
         在运行时都被看作是List<Object>,而不能用这种方式检查instanceof */
        if (integerList instanceof List<String>) {  // 这行编译不通过
            System.out.println("This won't work as expected due to type erasure");
        }else {
            System.out.println("This will work as expected");
        }
    }
}

三、类型擦除

        Java泛型在编译时会进行类型擦除,这意味着泛型类型参数在运行时会被擦除,并用它们的边界(通常是Object)替代。因此,泛型信息不会存在于字节码中,但类型检查是在编译时完成的。

泛型的类型擦除原则是:

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object;如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

示例如下: 

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<>();
        list1.add("hello");

        ArrayList<Integer> list2 = new ArrayList<>();
        list2.add(1);
        
        System.out.println(list1.getClass()); // class java.util.ArrayList
        System.out.println(list2.getClass()); // class java.util.ArrayList
        System.out.println(list1.getClass() == list2.getClass()); // true
    }
}

泛型类型String和Integer都被擦除掉了,只剩下原始类型 java.util.ArrayList 。 

原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。

四、PECS原则

        PECS原则是Java泛型编程中的一个重要概念,它是"Producer Extends, Consumer Super"的缩写,由Joshua Bloch在《Effective Java》一书中提出。这个原则指导开发者如何在使用泛型时选择合适的通配符上限< ? extends T >和下限< ? super T >,以确保类型安全和代码的灵活性。

《Effective Java》中的描述:

cb14dbb53cd4408ea7ab7417de9a683f.png

Producer Extends

只取,不存。这个列表 list 对我而言是个生产者、取款机。

List<? extends 动物> list = new ArrayList<猫>();

取出来的猫一定是动物,但无法保证它存进去的动物是猫。

比如下面这个例子,编译器看左边<? extends Cat>是个猫笼,可以是白猫笼、花猫笼等,但不知道它的实例具体是哪种猫笼,为了保持数据的一致性,禁止往猫笼中添加其它元素。

ArrayList<WhiteCat> whiteCats = new ArrayList<>(); // 白猫笼
        whiteCats.add(new WhiteCat());
        List<? extends Cat> cats = whiteCats; // 猫笼
        cats.add(null);
//        cats.add(new Object()); // 编译错误
//        cats.add(new WhiteCat()); // 编译错误
//        cats.add(new Cat()); // 编译错误
//        cats.add(new Animal()); // 编译错误
        System.out.println(cats.size()); // 2
        System.out.println(cats.get(0).getClass()); // WhiteCat
        WhiteCat whiteCat = (WhiteCat) cats.get(0); // 强制类型转换
        Cat cat = cats.get(0);
        Animal animal = cats.get(0);

Consumer Super

只存,不取。这个列表 list 对我而言是个消费者、收款机。

List<? super 猫> list = new ArrayList<动物>();

取出来到动物不一定是猫,但存进去的猫一定是动物。

为什么只能添加猫类及其子类?

该实例 new ArrayList<动物>() 可以存放动物类及其子类,但<?super 猫>中可以是动物、生物及Object,而编译器只知道 list 持有的是某种未知类型的猫或其父类对象,如果存生物类或Object类,在运行时会报错ClassCastException。

添加的一定是猫及其子类,因为你如果添加它的父类的话,只能保证里面的都是Object,没啥实际意义,但赋予它的实例里面可以包含父类,比如:

List<Animal> animal1 = new ArrayList<>();
        animal1.add(new Dog());
        animal1.add(new Cat());
        animal1.add(new Animal());

        List<? super Cat> animal2 = animal1;
        animal2.add(new Cat());
        animal2.add(new WhiteCat());
//        animal2.add(new Animal()); // 编译错误
//        animal2.add(new Dog()); // 编译错误
//        animal2.add(new Object()); // 编译错误
        System.out.println(animal2.get(0).getClass()); // Dog
//        Cat cat = (Cat) animal2.get(0); // ClassCastException
        Object obj = animal2.get(0);

这里的animal2是一个持有猫或其超类对象的列表,其实例animal1中有猫、狗、动物。但是,由于类型擦除,编译器只知道animal2持有的是某种未知类型的猫或父类对象,而不知道具体是什么类型。因此,为了保持类型安全,编译器不允许你向这个列表中添加父类元素。

如果既是生产者又是消费者,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

可以添加到列表中的对象类型 ,以及访问列表时返回的对象类型:

写入读取
对象类型CatAnimalObjectCatAnimalObject
List<>
List<Animal>
List<Cat>
List<?>
List<? extends Animal>
List<? extends Cat>
List<? super Animal>
List<? super Cat>

 

PECS的应用

以下是辅助类Collections中的copy()方法,其中List<? extends T>用于读取列表,List<? super T>用于复制列表。

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());
            }
        }
    }


参考资料:

Java泛型中的PECS 原则_java pecs-CSDN博客

深入理解java泛型------PECS法则_泛型pecs原则-CSDN博客

Java 泛型 | 菜鸟教程 (runoob.com)

Java 基础 - 泛型机制详解 | Java 全栈知识体系 (pdai.tech)

《Java面向对象程序设计》李金忠 杨德石 编著  清华大学出版社

《Comic Guide to Java Swift Mastery》【德】菲利普·阿克曼/著 北京理工大学出版社

《Core Java, Volume I : Fundamentals》【美】Cay S. Horstmann 机械工业出版社

《Effective Java》【美】Joshua Bloch 机械工业出版社

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

你要飞

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

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

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

打赏作者

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

抵扣说明:

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

余额充值