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

文章目录

  • 1 为什么需要<? super T>,<? extend T>这种写法
    • 1.1 简单的理解
    • 1.2 泛型不是协变的
    • 1.3 类型擦除
    • 1.4 实例分析
    • 1.5 类型系统
  • 2 Java 泛型 <? super T> <? extend T> 的通俗理解
    • 2.1 **重点 限定上界<? extend T>
    • 2.2 **重点 限定下界Box
  • 3 注意事项
    • 3.1 “?”不能添加元素
    • 3.2 “? extends T”也不能添加元素
    • 3.3 “? super T”能添加元素
    • 结论
    • PECS原则总结

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

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

1.1 简单的理解

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

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

举个例子:
理解Java泛型的复杂写法<? super T>,<? extend T>_第1张图片
虽然Object 是 String 的父类,但是整体来说,容器List并不是List的父类。

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

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 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的ListList 等类型,在编译之后都会变成 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());
    }
}

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

java.util.ArrayList
java.util.ArrayList

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

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

当类型信息被擦除之后,上述类的声明变成了 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); // 编译错误 
}  
  • 这段代码中,inspect 方法接受 List 作为参数,当在 test 方法中试图传入 List 的时候,会出现编译错误。

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

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

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

1.5 类型系统

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

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

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

}

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

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

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

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

上界通配符(Upper Bounds Wildcards)

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

Box

一个只能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());

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

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

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

Box

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

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

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

小的那一个
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

你可能感兴趣的:(java,java,开发语言)