• Java 泛型中的通配符


    🚀 优质资源分享 🚀

    学习路线指引(点击解锁)知识定位人群定位
    🧡 Python实战微信订餐小程序 🧡进阶级本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
    💛Python量化交易实战💛入门级手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

    本文内容如下:

    1、 什么是类型擦除
    2、常用的 ?, T, E, K, V, N的含义
    3、上界通配符 < ?extends E>
    4、下界通配符 < ?super E>
    5、什么是PECS原则
    6、通过一个案例来理解 ?和 T 和 Object 的区别

    一、什么是类型擦除?

    我们说Java的泛型是伪泛型,那是因为泛型信息只存在于代码编译阶段,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程为类型擦除。

    泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是因为类型擦除特性,让泛型代码能够很好地和之前版本的代码兼容。

    我们来看个案例

    (图1)

    因为这里泛型定义为Integer类型集合,所以添加String的时候在编译时期就会直接报错。

    那是不是就一定不能添加了呢?答案是否定的,我们可以通过Java泛型中的类型擦除特点及反射机制实现。

    如下

        public static void main(String[] args) throws Exception {
            ArrayList list = new ArrayList();
     list.add(6);
     //反射机制实现
     Class <span class="hljs-keyword"extends ArrayList> clazz = list.getClass();
     Method add = clazz.getDeclaredMethod("add", Object.class);
     add.invoke(list, "欢迎关注:后端元宇宙");
     System.out.println("list = " + list);
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行结果

    list = [6, 欢迎关注:后端元宇宙]
    
    
    • 1
    • 2

    二、案例实体准备

    这里先建几个实体,为后面举例用

    Animal类

    @Data
    @AllArgsConstructor
    public class Animal {
    
        /**
     * 动物名称
     */
        private String name;
    
        /**
     * 动物毛色
     */
        private String color;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    Pig类 :Pig是Animal的子类

    public class Pig  extends  Animal{
        public Pig(String name,String color){
            super(name,color);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Dog类: Dog也是Animal的子类

    public class Dog extends Animal {
    
        public Dog(String name,String color){
            super(name,color);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    三、常用的 ?, T, E, K, V, N的含义

    我们在泛型中使用通配符经常看到T、F、U、E,K,V其实这些并没有啥区别,我们可以选 A-Z 之间的任何一个字母都可以,并不会影响程序的正常运行。

    只不过大家心照不宣的在命名上有些约定:

    • T (Type) 具体的Java类
    • E (Element)在集合中使用,因为集合中存放的是元素
    • K V (key value) 分别代表java键值中的Key Value
    • N (Number)数值类型
    • ? 表示不确定的 Java 类型

    四、上界通配符 < ? extends E>

    语法: extends E

    举例: extends Animal 可以传入的实参类型是Animal或者Animal的子类

    两大原则

    • add:除了null之外,不允许加入任何元素!
    • get:可以获取元素,可以通过E或者Object接受元素!因为不管存入什么数据类型都是E的子类型

    示例

      public static void method(List extends Animal lists){
            //正确 因为传入的一定是Animal的子类
            Animal animal = lists.get(0);
            //正确 当然也可以用Object类接收,因为Object是顶层父类
            Object object = lists.get(1);
            //错误 不能用?接收
            ? t = lists.get(2);
            // 错误
            lists.add(new Animal());
            //错误
            lists.add(new Dog());
            //错误 
            lists.add(object);
            //正确 除了null之外,不允许加入任何元素!
            lists.add(null);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    五、下界通配符 < ? super E>

    语法: super E

    举例 : super Dog 可以传入的实参的类型是Dog或者Dog的父类类型

    两大原则

    • add:允许添加E和E的子类元素!
    • get:可以获取元素,但传入的类型可能是E到Object之间的任何类型,也就无法确定接收到数据类型,所以返回只能使用Object引用来接受!如果需要自己的类型则需要强制类型转换。

    示例

      public static void method(List <span class="hljs-built\_in"super Dog> lists){
            //错误 因为你不知道?到底啥类型
            Animal animal = lists.get(0);
            //正确 只能用Object类接收
            Object object = lists.get(1);
            //错误 不能用?接收
            ? t = lists.get(2);
            //错误
            lists.add(object);
            //错误
            lists.add(new Animal());
            //正确
            lists.add(new Dog());
            //正确 可以存放null元素
            lists.add(null);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    六、什么是PECS原则?

    PECS原则:生产者(Producer)使用extends,消费者(Consumer)使用super。

    原则

    • 如果想要获取,而不需要写值则使用" ? extends T "作为数据结构泛型。
    • 如果想要写值,而不需要取值则使用" ? super T "作为数据结构泛型。

    示例-

    public class PESC {
        ArrayList <span class="hljs-keyword"extends Animal> exdentAnimal;
        ArrayList <span class="hljs-built_in"super Animal> superAnimal;
        Dog dog = new Dog("小黑", "黑色");
    
        private void test() {
            //正确 
            Animal a1 = exdentAnimal.get(0);
            //错误 
            Animal a2 = superAnimal.get(0);
    
            //错误 
            exdentAnimal.add(dog);
            //正确 
            superAnimal.add(dog);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    示例二

    Collections集合工具类有个copy方法,我们可以看下源码,就是PECS原则。

       public static  void copy(List <span class="hljs-built\_in"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; ielse {
     ListIterator <span class="hljs-built\_in"super T> di=dest.listIterator();
     ListIterator <span class="hljs-keyword"extends T> si=src.listIterator();
     for (int i=0; i
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们按照这个源码简单改造下

    public class CollectionsTest {
        
        /**
     * 将源集合数据拷贝到目标集合
     *
     * @param dest 目标集合
     * @param src 源集合
     * @return 目标集合
     */
        public static  void copy(List <span class="hljs-built\_in"super T> dest, List extends T src) {
     int srcSize = src.size();
     for (int i = 0; i < srcSize; i++) {
     dest.add(src.get(i));
     }
     }
     
     public static void main(String[] args) {
     ArrayList animals = new ArrayList();
     ArrayList pigs = new ArrayList();
     pigs.add(new Pig("黑猪", "黑色"));
     pigs.add(new Pig("花猪", "花色"));
    
     CollectionsTest.copy(animals, pigs);
     System.out.println("dest = " + animals);
     }
     
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    运行结果

    dest = [Animal(name=黑猪, color=黑色), Animal(name=花猪, color=花色)]
    
    
    • 1
    • 2

    七、通过一个案例来理解 ?和 T 和 Object 的区别

    1、实体转换

    我们在实际开发中,经常进行实体转换,比如SO转DTO,DTO转DO等等,所以需要一个转换工具类。

    如下示例

    /**
     * 实体转换工具类
     * 
     * TODO 说明该工具类不能直接用于生产,因为为了代码看去清爽点,我少了一些必要检验,所以如果直接拿来使用可以会在某些场景下会报错。
     */
    public class EntityUtil {
    
      
        /**
     * 集合实体转换
     *
     * @param target 目标实体类
     * @param list 源集合
     * @return 装有目标实体的集合
     */
        public static  List changeEntityList(Class target, List list) throws Exception {
     if (list == null || list.size() == 0) {
     return null;
     }
     List resultList = new ArrayList();
     //用Object接收
     for (Object obj : list) {
     resultList.add(changeEntityNew(target, obj));
     }
     return resultList;
     }
    
     /**
     * 实体转换
     *
     * @param target 目标实体class对象
     * @param baseTO 源实体
     * @return 目标实体
     */
     public static  T changeEntity(Class target, Object baseTO) throws Exception{
     T obj = target.newInstance();
     if (baseTO == null) {
     return null;
     }
     BeanUtils.copyProperties(baseTO, obj);
     return obj;
     }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    使用工具类示例

      private void  changeTest() throws Exception {
            ArrayList pigs = new ArrayList();
     pigs.add(new Pig("黑猪", "黑色"));
     pigs.add(new Pig("花猪", "花色"));
     //实体转换
     List animals = EntityUtil.changeEntityList(Animal.class, pigs);
     }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这是一个很好的例子,从这个例子中我们可以去理解 ?和 T 和 Object的使用场景。

    我们先以集合转换来说

     public static  List changeEntityListNew(Class target, List list);
     
    
    
    • 1
    • 2
    • 3

    首先其实我们并不关心传进来的集合内是什么对象,我们只关系我们需要转换的集合内是什么对象,所以我们传进来的集合就可以用List 表示任何对象的集合都可以。

    返回呢,这里指定的是Class,也就是返回最终是List集合。

    再以实体转换方法为例

    public static  T changeEntityNew(Class target, Object baseTO)
    
    
    • 1
    • 2

    同样的,我们并不关心源对象是什么,我们只关心需要转换的对象,只需关心需要转换的对象为T

    那为什么这里用Object上面用?呢,其实上面也可以改成List list,效果是一样的,上面List list在遍历的时候最终不就是用Object接收的吗

    ?和Object的区别

    ?类型不确定和Object作用差不多,好多场景下可以通用,但?可以缩小泛型的范围,如:List extends Animal,指定了范围只能是Animal的子类,但是用List,没法做到缩小范围。

    总结

    • 只用于功能时,泛型结构使用 extends T
    • 只用于功能时,泛型结构使用 super T
    • 如果既用于,又用于操作,那么直接使用
    • 如果操作与泛型类型无关,那么使用

    声明: 公众号如需转载该篇文章,发表文章的头部一定要 告知是转至公众号: 后端元宇宙。同时也可以问本人要markdown原稿和原图片。其它情况一律禁止转载!

  • 相关阅读:
    cf 交互题
    讲解 CSS 过渡和动画 — transition/animation (很全面)
    利用PaddleDetection 训练自定义VOC数据集进行目标检测
    Python 的数据类型有哪些?(融合面试基础知识)
    【Java】对象的实例化
    当 xxl-job 遇上 docker → 它晕了,我也乱了!
    内网渗透学习-环境搭建
    JVM实用参数(一)JVM类型以及编译器模式
    MySQL 创建用户并设置权限
    LIN总线
  • 原文地址:https://blog.csdn.net/www_xuhss_com/article/details/125419492