• 理解Java泛型的复杂写法<? super T>,<? extend T>


    1 为什么需要<? super T>,<? extend T>这种写法

    “<? extends T>和<? super T>“是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
    “<? extends T>”:是指 上界通配符(Upper Bounds Wildcards)
    “<? super T>”:是指 下界通配符(Lower Bounds Wildcards)

    1.1 简单的理解

    开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收List<Object>作为形式参数,那么如果尝试将一个List<String> 的对象作为实际参数传进去,却发现无法通过编译。

    虽然从直觉上来说,Object 是 String 的父类,这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。

    举个例子:
    在这里插入图片描述
    虽然Object 是 String 的父类,但是整体来说,容器List<Object >并不是List<String>的父类。

    所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系。

    1.2 泛型不是协变的

    在 Java 语言中,数组是协变的,也就是说,如果 Integer 扩展了 Number,那么不仅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以传递或者赋予 Integer[]。(更正式地说,如果 Number是 Integer 的超类型,那么 Number[] 也是 Integer[]的超类型)。

    您也许认为这一原理同样适用于泛型类型 —— List< Number> List< Integer> 的超类型,那么可以在需要 List< Number>的地方传递List< Integer>。不幸的是,情况并非如此。为啥呢?这么做将破坏要提供的类型安全泛型。

    1.3 类型擦除

    正确理解泛型概念的首要前提是理解类型擦除(type erasure)。Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List<Object> List<String> 等类型,在编译之后都会变成 List。JVM 看到的只是 List,而由泛型附加的类型信息对 JVM 来说是不可见的。Java 编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是 Java 的泛型实现方式与C++ 模板机制实现方式之间的重要区别。

    public class Test {
        public static void main(String[] args) {
            List<String> strList = new ArrayList<>();
            List<Integer> intList = new ArrayList<>();
            System.out.println(strList.getClass().getName());
            System.out.println(intList.getClass().getName());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面这一段代码,运行后输出如下,可知在运行时获取的类型信息是不带具体类型的:

    java.util.ArrayList
    java.util.ArrayList
    
    • 1
    • 2

    很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:

    • 泛型类并没有自己独有的 Class 类对象。比如并不存在List<String>.class或是 List<Integer>.class,而只有 List.class,因此在运行时无法获得泛型的真实类型信息。
    • 静态变量是被泛型类的所有实例所共享的。对于声明为 MyClass 的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过 new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
    • 泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型 MyException<String> MyException<Integer>的。对于 JVM 来说,它们都是 MyException 类型的。也就无法执行与异常对应的 catch 语句。

    类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是 Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get();List< String> 就变成了 List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。比如考虑下面的代码:

    class MyString implements Comparable<String> {
        public int compareTo(String str) {        
            return 0;    
        }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    当类型信息被擦除之后,上述类的声明变成了 class MyString implements Comparable。但是这样的话,类 MyString 就会有编译错误,因为没有实现接口 Comparable 声明的 int compareTo(Object) 方法。这个时候就由编译器来动态生成这个方法。

    1.4 实例分析

    了解了类型擦除机制之后,就会明白编译器承担了全部的类型检查工作。编译器禁止某些泛型的使用方式,正是为了确保类型的安全性。以上面提到的 List< Object> 和 List< String> 为例来具体分析:

    public void inspect(List<Object> list) {    
        for (Object obj : list) {        
            System.out.println(obj);    
        }    
        list.add(1); // 这个操作在当前方法的上下文是合法的。 
    }
    public void test() {    
        List<String> strs = new ArrayList<String>();    
        inspect(strs); // 编译错误 
    }  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 这段代码中,inspect 方法接受 List 作为参数,当在 test 方法中试图传入 List 的时候,会出现编译错误。

    • 假设这样的做法是允许的,那么在 inspect 方法就可以通过 list.add(1) 来向集合中添加一个数字。这样在 test 方法看来,其声明为 List 的集合中却被添加了一个 Integer 类型的对象。这显然是违反类型安全的原则的,在某个时候肯定会抛出ClassCastException。因此,编译器禁止这样的行为。

    • 编译器会尽可能的检查可能存在的类型安全问题。对于确定是违反相关原则的地方,会给出编译错误。当编译器无法判断类型的使用是否正确的时候,会给出警告信息。

    为了让泛型用起来更舒服,Sun的大师们就想出了<? extends T>和<? super T>的办法,来让”水果盘子“和”苹果盘子“之间发生正当关系。

    1.5 类型系统

    在 Java 中,大家比较熟悉的是通过继承机制而产生的类型体系结构。比如 String 继承自 Object。根据Liskov 替换原则,子类是可以替换父类的。当需要 Object 类的引用的时候,如果传入一个 String 对象是没有任何问题的。但是反过来的话,即用父类的引用替换子类引用的时候,就需要进行强制类型转换。编译器并不能保证运行时刻这种转换一定是合法的。这种自动的子类替换父类的类型转换机制,对于数组也是适用的(数组是协变的)。 String[] 可以替换 Object[]。但是泛型的引入,对于这个类型系统产生了一定的影响。正如前面提到的 List<String>是不能替换掉List<Object>的。

    引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于 List 和 List 这样的情况,类型参数 String 是继承自 Object 的。而第二种指的是 List 接口继承自 Collection 接口。对于这个类型系统,有如下的一些规则:

    • 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。即 List< String> 是 Collection< String> 的子类型,List< String> 可以替换 Collection< String>。这种情况也适用于带有上下界的类型声明。
    • 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对 Collection<? extends Number> 来说,其子类型可以在 Collection 这个维度上展开,即 List<? extends Number> 和 Set<? extends Number> 等;也可以在 Number 这个层次上展开,即 Collection< Double> 和 Collection< Integer> 等。如此循环下去,ArrayList< Long> 和 HashSet< Double> 等也都算是 Collection<? extends Number> 的子类型。
    • 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则。
      理解了上面的规则之后,就可以很容易的修正实例分析中给出的代码了。只需要把 List< Object> 改成 List<?> 即可。List< String> 是 List<?> 的子类型,因此传递参数时不会发生错误。

    2 Java 泛型 <? super T> <? extend T> 的通俗理解

    假设,我们有下面几个类:

    class Box<T> {
    
        public Box() {
        }
    
        private T item;
    
        public Box(T t) {
            item = t;
        }
    
        public void set(T t) {
            item = t;
        }
    
        public T get() {
            return item;
        }
    
    }
    
    class Food {
    
    }
    
    class Meat extends Food {
    
    }
    
    class Fruit extends Food {
    
    }
    
    class Apple extends Fruit {
    
    }
    
    class RedApple extends Apple {
    
    }
    
    class GreenApple extends Apple {
    
    }
    
    
    • 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
    • 45

    泛型通常用于容器,假如我们有一些箱子:

    可以装肉的箱子 new Box< Meat>();

    可以装水果的箱子 new Box< Fruit>();

    2.1 **重点 限定上界<? extend T>

    上界通配符(Upper Bounds Wildcards)

    不能set值,只能get值,所以作为生产者,后面会说为什么

    Box<? extends Food>

    一个只能new Food以及一切是Food子类的箱子(小于等于关系)
    (也就是只能new Food以及Food的子类)

    Box<? extends Food> box1 = new Box<Food>();
    Box<? extends Food> box2 = new Box<Fruit>();
    Box<? extends Food> box3 = new Box<Meat>();
    
    作为生产者Producer,可以取出
    // 上界
    Box<? extends Food> box1 = new Box<Food>(new Fruit());
    Box<? extends Food> box2 = new Box<Fruit>(new Apple());
    Box<? extends Food> box3 = new Box<Meat>(new Meat());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    box1 只能get Food的实现类以及一切具体实现类(Food)子类的箱子
    box2 只能get具体Food和Fruit之中小的那一个实现类以及一切具体实现类(Fruit)子类的箱子
    box3 只能get具体Food和Meat之中小的那一个实现类以及一切具体实现类(Meat)子类的箱子

    box1:上界是Food,new了一个new Box< Food>(),所以box1 可以get到的范围是 Food以及Food的子类
    box2:上界是Food,new了一个new Box< Fruit>(),所以box2 可以get到的范围是 Fruit以及Fruit的子类
    box3:上界是Food,new了一个new Box< Meat>(),所以box1 可以get到的范围是 Meat以及Meat的子类

    2.2 **重点 限定下界Box<? super T>

    相对应的下界通配符(Lower Bounds Wildcards)

    不影响往里存,但往外取只能放在Object对象里,后面会说为什么

    Box<? super Apple>

    一个只能new Apple以及一切是 Apple父类的箱子(大于等于关系)

    只能set具体Apple和Fruit之中小的那一个实现类以及一切具体实现类子类的箱子

    Box<? super Apple> box11 = new Box<Fruit>();
    Box<? super Apple> box22 = new Box<Food>();
    
    • 1
    • 2

    小的那一个
    box11 只能get或set具体Apple和Fruit之中小的那一个实现类以及一切具体实现类(Fruit)子类的箱子
    box22 只能get或set具体Apple和Food之中小的那一个实现类以及一切具体实现类(Apple)子类的箱子

    box11:下界是Apple,new了一个new Box< Fruit>(),Fruit>=Apple
    box22:下界是Apple,new了一个new Box< Food>(),Food>=Apple

    3 注意事项

    3.1 “?”不能添加元素

    3.2 “? extends T”也不能添加元素

    3.3 “? super T”能添加元素

    结论

    JAVA泛型通配符的使用规则就是赫赫有名的“PECS”(生产者使用“? extends T”通配符,消费者使用“? super T”通配符)。

    PECS原则总结

    从上述两方面的分析,总结PECS原则如下:

    • 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
    • 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
    • 如果既要存又要取,那么就不要使用任何通配符。

    参考1:Java 泛型 <? super T> <? extend T> 的通俗理解
    参考2:Java泛型解惑之 extends T>和 super T>上下界限
    参考3:JAVA泛型通配符PECS原则Producer Extends Consumer Super

  • 相关阅读:
    leetcode(力扣) 134. 加油站 (贪心 & 两种情况,老司机逻辑题)
    实现集中式身份认证管理的案例
    Webrtc丢包率的计算
    JS代码保护,如何保护js代码呢?
    WPS vbe6ex.olb 不能加载
    测试流程||接口测试
    React中memo()、useCallback()、useMemo() 区别使用详解
    softmax交叉熵损失函数深入理解(二)
    解决 docker 容器无法正常解析域名
    最全元宇宙概念分析!元宇宙为何发展于区块链?
  • 原文地址:https://blog.csdn.net/weixin_43702146/article/details/125596808