泛型

[TOC]

泛型出现的原因:

​ 比如说小明在工作中,因为需要调用数据库的相关数据,就产生了对应的数据对象。猫1,猫2,猫3,而从数据库获取的结果集后,小明需要用主键和对象产生映射关系,于是小明建立了一个容器类,用来装入ID和猫对象的映射关系,小明称之为CatMap。CatMap是基于原生的数组实现Cat[],通过对对应ID作为数组的索引实现了基本的映射关系,在通用类中使用hash值作为元素的键。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package cn.hash.hashcode;
import java.util.Arrays;
public class CatMap {
//设置默认容量
private static final int INIT_CAPTIVE = 1;
//设置当先用数据达到总容量的0.75时进行扩容
private static final double BOOM_INIT_CAPTIVE = 0.75;
private int captive;
private double limit;
private Cat[] cats;
public CatMap(){
this.captive = INIT_CAPTIVE;
this.limit = BOOM_INIT_CAPTIVE;
cats = new Cat[captive];
}
public Cat put(int id, Cat cat){
if(cats == null){
while(captive*limit<id){
captive<<=1;
}
cats=Arrays.copyOf(cats, captive);
cats[id] = cat;
return null;
}else if(cats.length-1<id){
while(captive*limit<id){
captive<<=1;
}
cats=Arrays.copyOf(cats, captive);
cats[id] = cat;
return null;
}else{
Cat oldTarget = cats[id];
cats[id] = cat;
return oldTarget;
}
}
public Cat get(int id){
if(captive<=id){
return null;
}else{
return cats[id];
}
}
public Cat[] values(){
return cats;
}
public static void main(String[] args){
CatMap cats = new CatMap();
Cat cat1 = new Cat("blue");
cats.put(100, cat1);
Cat cat2 = new Cat("black");
cats.put(1000, cat2);
System.out.println(cats.get(100));
System.out.println(cats.get(1000));
}
}

​ 然而在后续的继续实践中,小明发现他还要建立狗,马等映射关系,小明又得继续建立对象的映射关系,DogMap,HorseMap等,这种情况下,小明就试图用统一的方式标示这种映射关系。因为JAVA中所有类都是Object类的子类,于是小明抽象出一个ObjectMap容器类来统一存放这种映射关系,而在取出时再将其转换成对应的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.hash.hashcode;
import java.util.Arrays;
public class ObjectMap {
//设置默认容量
private static final int INIT_CAPTIVE = 1;
//设置当先用数据达到总容量的0.75时进行扩容
private static final double BOOM_INIT_CAPTIVE = 0.75;
private int captive;
private double limit;
private Object[] objects;
public ObjectMap(){
this.captive = INIT_CAPTIVE;
this.limit = BOOM_INIT_CAPTIVE;
objects = new Object[captive];
}
......
}

​ 程序运行了一段时间后,还是出了问题,一方面小明需要使用非整形作为键,这样作为原来使用数组作为处理的方式就不要合适了;另一方面,因为使用Object的方法来书写,导致必须在ObjectMap中对类型进行判断,防止在同一个容器中插入不同的类型对象,当取出Object时又必须转换成真正需要的类型进行操作,于是错误产生了。于是小明想是不是可以将类型给提取出来,作为参数出现,这样既能确保传入参数的类型一致性,也能保证取出的结果已经是转换后的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ObjectMap {
.....
private int captive;
private double limit;
private Class keyType1;
private Class ValueType;
......
//类型检查
//类型转换
public ObjectMap(captive,limit,keyType1,ValueType){
......
}
}

​ 这样在建立对应的类容器时,就需要将类的类型放入到构造函数中,用来确保出传入类的一致性和获取数据时获取到类型转换之后的类型结果。确保结果的一致性。而在小明后续的工作中,出现了其他一些具有统一方法特征但根据不同类需要就建立相同属性和方法的对象,于是提取出了泛型的概念,用来表示类型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
//编译器自动完成类型转换
public class ObjectMap<T,T> {
.....
private int captive;
private double limit;
......
//类型检查
//类型转换
public ObjectMap(captive,limit){
......
}
}

​ 可以认为,泛型对象所映射的类在运行期应该是同一类。但在编译期的确应该具有独立型,泛型可以认为是一次对类的再次抽象。这种情况下,如果一个对象和他的遥控器变量对应的类型参数不同,那意味这在对象接收到某消息后,对消息体中进行类型检查和类型转换可能会出问题。


​ 泛型类是一个类的模板,需要制定泛型类实现一个类,即一张蓝图,而后再实例化这张蓝图,获得一个实际的对象。

​ 一个合适的比喻是这样的,小王是一个电脑城装计算机的,他现在要装配一台主机,但是客户需要一个具备顶尖显卡的机型,但是现在市场上这几个显卡产品都没有,能有什么基本碰运气。但是小王知道这些显卡的基本接口和装配方式基本一样。一旦市场上有任何一种显卡到货,那么计算机的组合也就确认了。果然,不久之后,市场上有货了,小王拿到一个七彩虹的1080GTX,然后根据之前的设计成功完成客户机的装配。

泛型类概述

  • 泛型 术语:适用于多种类型
  • 泛型 本质:类型参数化
  • 泛型 核心:通知编译器需要的类型,编译器处理同意传入,类型转化等信息。
  • 泛型 初衷:

    • 希望类或方法可以具备最广泛的表达能力,即通过解耦类和方法 与 类型 之间的约束;类和方法不再与调用类型绑定;

    • 对于容器类而言,泛型在保证容器类可以存储任何类型对象的同事,又保证了容器类一旦声明自己要保存的元素类型,就不再保存其他类型的元素了。

简单而言: 泛型 = 编译时的类型检查 + 编译时的类型擦除(编译器插入checkcast等)+ 运行时的自动类型转换

类型参数推断,当时用泛型类时,必须在创建对象的时候指定类型参数的值;而是用泛型方法时,通常不必指明参数类型(应该是编译器自动将实参类型作为参数类型),但是如果返回值也是泛型时,一般必须指定返回值的泛型类型才能保证正常的计算过程

泛型的定义和语法

  • 泛型类 (参数化类)

    1
    public class Holder<T>{}
  • 泛型接口 (参数化接口)

    1
    public interface Generator<T>{}
  • 泛型方法 (参数化方法)

    1
    public <T> void f(T x){}

泛型的注意事项

  • 多态和泛型的权衡

    ​ 如果你希望你使用的类型参数只是一个类以及他的子类型,那么使用明确的类型定义即可,多态完全可以实现。如果你想让你的类型参数支持更加广泛的类型时,再使用泛型更加合理。容器类就是典型的使用场景

    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
    public class HasF {
    public void f(){...}
    }
    //泛型实现
    class Manipulator1<T extends HasF>{
    private T obj;
    public Manipulator1(T x){
    this.obj = x;
    }
    public void manipulate(){
    obj.f();
    }
    }
    //多态实现
    class Manipulator2{
    private HasF obj;
    public Manipulator2(HasF x){
    this.obj = x;
    }
    public void manipulate(){
    obj.f();
    }
    }

  • 泛型类误区:

    如果某类继承自某泛型类,但是类名后未跟随泛型类名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 第一段代码
    public class Pair<T> {
    private T first;
    public Pair(T first){
    this.first = first;
    }
    public void setFirst(T first){
    this.first = first;
    }
    public T getFirst(){
    return first;
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 时间间隔类
    public class DateInterval extends Pair<Date> {
    public DateInterval(Date first){
    super(first);
    }
    @Override
    public void setFirst(Date second) {
    super.setFirst(second);
    }
    @Override
    public Date getFirst(){
    return super.getFirst();
    }
    }

    ​ 由泛型类的定义可知,Pair<T> 是一个泛型类,因为在类名后面有类型参数;类DateInterval后面没有跟类型参数列表,因此该类就是一个 T 被替换为 Date 的实体类,其从 Pair<Date>泛型类型 继承得到的方法列表,与泛型彻底无关。

  • 在泛型类中, static 域或方法无法访问泛型类的类型参数;若静态方法需要使用泛型能力,就必须使其成为泛型方法(不与泛型类共享类型参数)

    1
    2
    3
    4
    5
    6
    public class Test2<T> {
    public static T one; //编译错误
    public static T show(T one){ //编译错误
    return null;
    }
    }

    但是下列重新定义另一个

    1
    2
    3
    4
    5
    public class Test2<T> {
    public static <T> T show(T one){//这是正确的
    return null;
    }
    }

    因为这是一个泛型方法,在泛型方法中使用的 类型参数T 是自己在方法中定义的T,而不是泛型类中的 T。

  • 限制泛型可用的类型

    • 如果想要限制使用泛型的类别时,即要求只能使用某个特定类型或者其子类型才能实例化该类型时,使用 extends 关键字指定这个类型必须是继承或者实现某个接口。一般地,当没有指定泛型继承的类型或实现的接口时,默认等价于使用 T extends Object,因此,默认情形下任何类型都可以作为参数插入

    • 特别地,为类型参数设定的第一个边界可以是类类型或接口类型,类型参数的第一个边界之后的任意额外边界都只能是接口类型,同时,一般将标记性接口放到靠后位置,这些类型参数之间有 & 相连接

      1
      2
      3
      publc class MyClass<T extends Number & Serilizable>{
      ...
      }

  • 在调用泛型方法的时候,可以指定泛型,也可以不指定泛型

    ​ 在不指定泛型(类型参数)的情况下,泛型变量的类型为该方法中的几个类型的同一个父类的最小集,直到Object.该方法中的几种类型必须是该泛型实例类型或者其子类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //代码示例
    public class Test2{
    public static void main(String[] args) {
    /**不指定泛型的时候*/
    Integer i = Test2.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
    Number f = Test2.add(1, 1.2); //这两个参数一个是Integer,一个是Double,所以取同一父类的最小级,为Number
    Object o = Test2.add(1, "asd"); //这两个参数一个是Integer,一个是String,所以取同一父类的最小级,为Object
    System.out.println(i.getClass().getName()); //输出: java.lang.Integer
    System.out.println(f.getClass().getName()); //输出: java.lang.Double
    System.out.println(o.getClass().getName()); //输出: java.lang.String
    /**指定泛型的时候*/
    int a = Test2.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类
    int b = Test2.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Double
    Number c = Test2.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Double
    }
    //这是一个简单的泛型方法
    public static <T> T add(T x,T y){
    return y;
    }
    }

    注意,这个例子中的两个输出是java.lang.Double和java.lang.String,而不是java.lang.Number和java.lang.Object:

    1
    2
    3
    System.out.println(f.getClass().getName()); //输出: java.lang.Double
    System.out.println(o.getClass().getName()); //输出: java.lang.String

    实际上,这个问题涉及泛型机制和多态两点。在例子中,类型参数T被编译器用Number替换,这是没问题的,因为无论整形还是浮点型都属于数型,这是由多态机制保证的。但是,无论x还是y,它们本质上还是各自的类型不会发生任何改变。要注意的是,这里的getClass()方法返回的变量的实际类型,即运行时类型而非编译时类型,因此返回y的类型是double而非number。

  • 泛型的兼容性

    • 从泛型类型生成的任何类型的引用都能存储到对应的原生类型的变量中

      1
      2
      3
      LinkedList list = new LinkedList<String>();
      //等价于
      LinkedList list = new LinkedList();
    • 从原生类型生成的引用能存储到任何类型的泛型类型的变量中

      1
      2
      LinkedList<String> list1 = new LinkedList();
      LinkedList<Integer> list2 = new LinkedList();

  • primitive类型不可以作为类型参数

    1
    2
    LinkedList<int> list = new LinkedList<int>();
    // You are trying to pass a primitive object into a generic type declaration whereas generic types always expect a Wrapper Class object.

    泛型 = 编译时的类型检查 + 编译时的类型擦除(编译器插入checkcast等)+ 运行时的自动类型转换.而基础类型无法完成类型的检查,擦除和转换,所以必须使用

  • 若使用泛型方法可以取代将整个类泛型化,那么就应该使用泛型方法

    1
    2
    3
    4
    5
    6
    7
    8
    public class CityManager<K extends Leader,V> {
    public void report(K l){
    }
    public void beat(V v){
    }
    }

    完全等价于

    1
    2
    3
    4
    5
    6
    7
    public class CityManager{
    public void report(Leader l){
    }
    public <V> void beat(V v){
    }
    }

  • 泛型方法与可变参数列表可以很好的共存

    1
    public static <T> void f(T... args){}

通配符及泛型的逆变和协变

  1. 通配符

    无界通配符

    1
    2
    3
    4
    5
    ArrayList<?> list = new ArrayList<String>();
    list = new ArrayList<Double>();
    list.add(e); // e cannot be resolved to a variable
    System.out.println(list1.size()); // OK

    ​ 在 Java 集合框架中,对于参数值是未知类型的容器类,只能读取其中元素,不能向其中添加元素, 因为,其类型未知,所以编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是 NULL(对 Null 而言,无所谓类型)。

    • List : 持有任何Object类型 的 原生List,编译器不会对原生类型进行安全检查;类型会被完全擦除,可以存入任何类型,取出的类型都是Object,都最终指向

      1
      2
      3
      4
      5
      6
      7
      8
      ArrayList list = new ArrayList(); //这种情况下 类型参数 都认为是Object
      ArrayList list = new ArrayList<String>();
      ArrayList list = new ArrayList<Leader>();
      list.add("121");
      list.add(new ArrayList());
      String a = list.get(0); //编译错误,返回类型Object
      System.out.println(list.get(0).getClass().getName()); //输出java.lang.String
      System.out.println(list.get(1).getClass().getName()); //java.util.ArrayList
    • List<?> :具有某种特定类型 的 非原生List,编译器会进行安全检查;

      1
      2
      3
      4
      5
      ArrayList<?> list1 = new ArrayList();
      ArrayList<?> list = new ArrayList<String>();
      list.add(null); //仅可以加入null
      list.add("aa"); //编译错误The method add(capture#2-of ?) in the type ArrayList<capture#2-of ?> is not applicable for the arguments (String)
      Object object = list.get(0);
    • List<Object> : 编译器认为 List<Object>List<?> 的子类型;

  2. 向上转型 / 通配符的上界 / 协变

    在引入通配符的上界这一概念时,我们先看一下数组的一种特殊行为:基类型的数组引用可以被赋予导出类型的数组,如下面的代码所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Fruit {}
    class Apple extends Fruit {}
    class Jonathan extends Apple {}
    class Orange extends Fruit {}
    public class CovariantArrays {
    public static void main(String[] args) {
    Fruit[] fruit = new Apple[10];
    fruit[0] = new Apple(); // 编译期、运行期都 OK
    fruit[1] = new Jonathan(); // 编译期、运行期都 OK
    fruit[3] = new Fruit(); // 编译期 OK、运行期抛出 java.lang.ArrayStoreException(因为 fruit 的运行时类型是 Apple[], 而不是 Fruit[] 或 Orange[])
    // 说明 Fruit[] 是 Apple[] 的父类型
    System.out.println(Fruit[].class.isAssignableFrom(Apple[].class)); // true
    }
    }

    由此可以说明:

    • 由 12 行可知,该行代码编译期正常,则进一步说明:编译器的类型检查是针对引用的(Fruit型数组可以放入Fruit及其子类型对象);但在运行时,由于 fruit引用 实际上指的是一个 Apple数组,而作为 Apple数组 则只可以向其中放入Apple及其子类型对象,因此当放入 Fruit对象时,抛出异常

    • 15 行可知,Fruit[]Apple[] 的父类型,因此根据Java多态特性,前者可以指向后者对象。

    我们知道,泛型的主要目标之一就是将这种错误检查移到编译期,那么,如果我们用泛型容器代替数组,那将会发生什么呢?

    1
    2
    3
    4
    public class NonCovariantGenerics {
    List<Fruit> flist = new ArrayList<Apple>(); // Compile Error: Type Mismatch
    }

      由以上代码可以知道,编译期根本不允许我们这么做。试想,如果编译期允许我们这样做,该容器就允许存入任何类型的对象,只要它是一种Fruit,而不像数组那样会抛出运行时异常,违背了泛型的初衷(泛型保证容器的类型安全检查)。所以,在编译期看来List<Fruit>List<Apple>根本就是两种不同的类型,并无任何继承关系。

    但是,有时你想要在以上两个类型之间建立某种向上转型关系,这就引出了通配符的上界。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class GenericsAndCovariance {
    public static void main(String[] args) {
    // 允许我们向上转型,向数组那样
    List<? extends Fruit> flist = Arrays.asList(new Apple());
    // Compile Error: can’t add any type of object:
    flist.add(new Apple()); // Compile Error
    flist.add(new Fruit()); // Compile Error
    flist.add(new Object()); // Compile Error
    flist.add(null); // Legal but uninteresting
    // We know that it returns at least Fruit:
    Fruit f = flist.get(0);
    Object o = flist.get(0);
    Apple a = flist.get(0); // Compile Error:Type mismatch
    flist.contains(new Apple()); // OK
    flist.indexOf(new Apple()); // OK
    }
    }

    ​ 对于上述例子,flist 的类型就是List<? extends Fruit>了,但这并不意味着可以向这个 List 可以添加任何类型的 Fruit,甚至于不能添加 Apple。虽然编译器知道这个 List 持有的是 Fruit,但并不知道其具体持有哪种特定类型(可能是List<Fruit>List<Apple>List<Orange>List<Jonathan>),所以编译器不知道该添加那种类型的对象才能保证类型安全(add 方法的参数为 ? extends Fruit ),因而编译器杜绝任何添加任何类型的 Fruit。但是,对于诸如get(int index)【我们进行读取操作时,编译器是允许的,而且编译器还知道 List 中的任何一个对象至少具有 Fruit类型】、contains(Object o)indexof(Object o)等操作,由于其参数类型不涉及通配符,因此编译器允许调用这些操作。

    因此,一旦执行这种向上转型,我们就丢掉向其中添加任何对象的能力。更一般地,编译器会直接拒绝对参数列表中涉及通配符的方法的调用。因此,这意味着将由泛型类的设计者来决定哪些调用地安全的,并使用 Object类型 作为其参数类型,例如 contains 方法和 indexof 方法。例如,

    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
    public class Holder<T> {
    private T value;
    public Holder() {
    }
    public Holder(T val) {
    value = val;
    }
    public void set(T val) {
    value = val;
    }
    public T get() {
    return value;
    }
    public boolean equals(Object obj) {
    return value.equals(obj);
    }
    public static void main(String[] args) {
    Holder<Apple> Apple = new Holder<Apple>(new Apple());
    Apple d = Apple.get();
    Apple.set(d);
    Holder<? extends Fruit> fruit = Apple; // OK
    Fruit p = fruit.get();
    d = (Apple) fruit.get(); // Returns ‘Fruit’,类型擦除,返回上界
    // No warning,运行时异常 java.lang.ClassCastException
    Orange c = (Orange) fruit.get();
    // fruit.set(new Apple()); // Cannot call set(),参数列表含通配符
    // fruit.set(new Fruit()); // Cannot call set(),参数列表含通配符
    fruit.equals(d); // OK,参数列表不含通配符
    }
    }
  3. 超类型通配符 / 通配符的下界 / 逆变

    ​ 我们可以使用超类型通配符指定通配符的下界, 方法是<? super MyClass>,甚至可以用在类型参数上<? super MyClass>(尽管我们不能对泛型参数给出一个超类型边界;即不能声明<T super MyClass>)。这使得我们可以安全的传递一个对象到泛型类型中,因此,有了超类型通配符,就可以向 Collection 写入了,如下图所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import java.util.List;
    public class SuperTypeWildCards{
    static void writeTo(List<? super Apple> apples){
    apples.add(new Apple());
    apples.add(new Jonathan());
    apples.add(new Fruit()); //error
    apples.add(new Apple());
    apples.addAll(Collection<? extends Apple>);
    }
    }

    ​  由图片可知,参数 apples 是 Apple 或 Apple的某种基类型 (例如:Fruit,Object,…) 的 List,也就是说,该 List 可以是 List<Apple>, List<Fruit>List<Object>等,但无论具体指的是哪一种,我们向其中添加 Apple 或 Apple的子类型 总是安全的。但编译器不允许向该 List 放入一个 Fruit 对象, 因为 该List 的类型可能是 List<Apple>, 这样将会违背泛型的本意。

    ​ 对于List<? super Apple>,在读取容器元素时,由于该容器所包含的元素可能是 Object类型、 Fruit类型 和 Apple类型,因此,从容器所读取到的元素只能确定是 Object类型的,如下面图片所示:

    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
    public class GenericWriting {
    static <T> void writeExact(List<T> list, T item) {
    list.add(item);
    }
    static List<Apple> apples = new ArrayList<Apple>();
    static List<Fruit> fruit = new ArrayList<Fruit>();
    static void f1() {
    writeExact(apples,new Apple());
    writeExact(fruit,new Apple());
    }
    static <T> void writeWithWildcard(List<? super T> list, T item) {
    list.add(item);
    }
    static void f2(){
    writeWithWildcard(apples, new Apple());
    writeWithWildcard(fruit, new Apple());
    }
    public static void main(String[] args){
    f1();
    f2();
    System.out.println(fruit.size());
    System.out.println(apples.size());
    List<? super Apple> list = new ArrayList<>();;
    list.add(new Apple());
    list.add(new HongFushi());
    Object object = list.get(0);
    }
    }

  4. 协变与逆变

    逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果 A,B 表示类型,f(⋅)表示类型转换,≤ 表示继承关系(比如,A ≤ B 表示A是B的子类);

    • f(⋅) 是逆变(contravariant)的,当 A≤B 时有 f(B)≤f(A) 成立;
    • f(⋅) 是协变(covariant)的,当 A≤B 时有 f(A)≤f(B) 成立;
    • f(⋅) 是不变(invariant)的,当 A≤B 时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系

      接下来,我们看看Java中的常见类型转换的协变性、逆变性或不变性:

    • 泛型

      ​ 令f(A) = ArrayList<A>,那么f(⋅) 是逆变、协变还是不变的呢?如果是逆变,则ArrayList<Integer>ArrayList<Number>的父类型;如果是协变,则ArrayList<Integer>ArrayList<Number>的子类型;如果是不变,二者没有相互继承关系。由于实际上ArrayList<Number>ArrayList<Integer>无关,所以泛型是不变的。

    • 数组

      令f(A) = A[],容易证明数组是协变的;

      1
      Number[] numbers = new Integer[3];

  1. 协变与逆变的实现

    我们知道Java 中的泛型是不变的,可我们有时需要实现泛型的逆变与协变,怎么办呢? 这时,通配符 ? 派上了用场:

    • <? extends>实现了泛型的协变

      比如:

      1
      2
      3
      ArrayList<? extends Apple> l3 = new ArrayList<>();
      ArrayList<? extends Fruit> l4 = new ArrayList<>();
      l4 = l3;

      ​ 对于 ArrayList<? extends Apple> 类型,我们知道其表示某种具体类型(只是没有确定下来),但是无论其具体指的是ArrayList<Apple> 类型还是ArrayList<Jonathan> 类型,都是可以赋给ArrayList<? extends Fruit> 类型的引用的,反之则不可以。因此,我们可以认为ArrayList<? extends Fruit> 类型ArrayList<? extends Apple> 类型的父类型,故<? extends>实现了泛型的协变。

    • <? super>实现了泛型的逆变

      比如:

      1
      2
      3
      ArrayList<? super Apple> l1 = new ArrayList<>();
      ArrayList<? super Fruit> l2 = new ArrayList<>();
      l1 = l2;

      ​ 对于 ArrayList<? super Fruit> 类型,我们知道其表示某种具体类型(只是没有确定下来),但是无论其具体指的是ArrayList<Fruit> 类型还是ArrayList<Object> 类型,都是可以赋给ArrayList<? super Apple> 类型的引用的,反之则不可以。因此,我们可以认为ArrayList<? super Apple> 类型ArrayList<? super Fruit> 类型的父类型,故 <? super>实现了泛型的逆变。

  2. PECS 准则 (producer-extends, consumer-super)

    我们知道 <?> 表示:我想使用 Java泛型 来编写代码,而不是用原生类型;但是在当前这种情况下,我并不能确定下泛型参数的具体类型,因此用?表示任何某种类型*。因此,根据我们对通配符的了解,使用无界通配符的泛型类不能够写数据,而在读取数据时,所赋值的引用也只能是 Object 类型。那么,我们究竟如何向泛型类写入、读取数据呢?

    《Effective Java2》给出了答案: PECS : producer(读取)-extends, consumer(写入)-super。换句话说,如果输入参数表示一个 T 的生产者,就使用<? extends T>;如果输入参数表示一个 T 的消费者,就使用<? super T>。总之,通配符类型可以保证方法能够接受它们应该接受的参数,并拒绝那些应该拒绝的参数。** 比如,一个简单的 Stack API :

    1
    2
    3
    4
    5
    6
    public class Stack<E>{
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
    }

    现在要实现 pushAll(Iterable<E> src) 方法,将实现 Iterable 接口的 src 的元素逐一入栈:

    1
    2
    3
    4
    public void pushAll(Iterable<E> src){
    for(E e : src)
    push(e)
    }

      那么问题就来了:假设有一个实例化 Stack<Number> 的对象 stack(类型参数被实例化为Number),显然, 我们向这个 stack 中加入 Integer型Float型元素都是可以的,因为这些元素本来就是Number型的。因此, src 就包括但不限于 Iterable<Integer>Iterable<Float> 两种可能;这时,在调用上述 pushAll方法 时,编译器就会产生 type mismatch 错误。原因是显而易见的,因为Java中泛型是不变的,Iterable<Integer>Iterable<Float> 都不是 Iterable<Number>及其子类型中的一种。所以,我们对 pushAll方法 的设计就存在逻辑上的问题。因此,应改为

    1
    2
    3
    4
    5
    // Wildcard type for parameter that serves as an E producer
    public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
    push(e);
    }

    这样,我们就可以实现将 实现Iterable接口E类型的容器中的元素读取到我们的 Stack 中。


    那么,如果现在要实现 popAll(Collection<E> dst)方法,将 Stack 中的元素依次取出并添加到 dst 中,如果不用通配符实现:

    1
    2
    3
    4
    5
    // popAll method without wildcard type - deficient!
    public void popAll(Collection<E> dst) {
    while (!isEmpty())
    dst.add(pop());
    }

    ​ 同样地,假设有一个实例化 Stack<Number> 的对象 stack , dst 为 Collection<Object>,显然,这是合理的。但如果我们调用上述的 popAll(Collection<E> dst)方法,编译器会报出 type mismatch 错误,编译器不允许我们进行这样的操作。原因是显而易见的,因为 Collection<Object> 不是 Collection<Number>及其子类型的一种。所以,我们对 popAll方法 的设计就存在逻辑上的问题。因此,应改为

    1
    2
    3
    4
    5
    // Wildcard type for parameter that serves as an E consumer
    public void popAll(Collection<? super E> dst) {
    while (!isEmpty())
    dst.add(pop());
    }
    这样,我们就可以实现将 `Stack<E>` 中的 元素读取到我们的 Collection 中 。
    

    ​ 在上述例子中,在调用 pushAll方法时 src生产了 E实例(produces E instances),在调用 popAll方法时 dst消费了 E实例(consumes E instances)。Naftalin与Wadler 将 PECS 称为 Get and Put Principle

    ​ 此外,我们再来学习一个例子: java.util.Collections 的 copy 方法(JDK1.7),它的目的是将所有元素从一个列表(src)复制到另一个列表(dest)中。显然,在这里,src 是生产者,它负责产生 T类型的实例;dest 是消费者,它负责消费 T类型的实例。这完美地诠释了 PECS :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // List<? extends T> 类型的 src 囊括了所有 T类型及其子类型 的列表
    // List<? super T> 类型的 dest 囊括了所有可以将 src中的元素添加进去的 List种类
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // 将 src 复制到 dest 中
    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());
    }
    }
    }

    故有PECS总结:

    • 输入参数是生产者时,用 ? extends T ;
    • 输入参数是消费者时,用 ? super T ;
    • 输入参数既是生产者又是消费者时,那么通配符类型没什么用了:因为你需要的是严格类型匹配,这是不用任何通配符而得到的;
    • 无界通配符<?> 既不能做生产者(读出来的是Object),又不能做消费者(写不进去);

编译器如何处理泛型

通常情况下,一个编译器处理泛型有两种方式:

  1. Code Specializatio

    在实例化一个泛型类或泛型方法时都产生一份新的目标代码(字节码or二进制代码)。例如,针对一个泛型list,可能需要针对string,integer,float产生三份目标代码。

  2. Code Sharing

    对每个泛型类只生成唯一的一份目标代码;该泛型类的所有实例都映射到这份目标代码上,在需要的时候执行类型检查和类型转换。

    • C++中的模板(template)是典型的Code specialization实现
      C++编译器会为每一个泛型类实例生成一份执行代码。执行代码中integer list和string
      list是两种不同的类型。这样会导致代码膨胀(code bloat),不过有经验的C++程序员可以有技巧的避免代码膨胀。
      另外,在引用类型系统中,这种方式会造成空间的浪费。因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是Java编译器中采用Code
      sharing方式处理泛型的主要原因。
    • Java 是典型的Code sharing实现
      Java编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

类型擦除

  1. 要点
    • 类型擦除: 通过移除泛型类定义的类型参数并将定义中每个类型变量替换成对应类型参数的非泛型上界(第一个边界),得到原生类型(raw type)
    • 类型擦除是 Java 泛型实现的一种折中,以便在不破坏现有类库的情况下,将泛型融入Java,并且保证兼容性。(泛型出现前后的Java类库互相兼容)
    • 类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码(Class 对象)上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法。
    • 擦除是在编译期完成的。类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。泛型类型只有在静态类型检查期间才会出现,在此之后,程序中的所有泛型类型都将被擦除,并替换为它们的非泛型上界。因此,在泛型代码内部,无法获得任何有关泛型参数类型的信息。
  1. 编译器是如何配合类型擦除的?

编译器擦除

  1. 类型擦除的主要过程

    对于Pair<>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //代码示例 A
    class Pair<T> {
    private T value;
    public T getValue() {
    return value;
    }
    public void setValue(T value) {
    this.value = value;
    }
    }

    Pair<>的原始类型为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //代码示例 B
    class Pair {
    private Object value;
    public Object getValue() {
    return value;
    }
    public void setValue(Object value) {
    this.value = value;
    }
    }

    以下是类型擦除示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //代码示例 1
    interface Comparable <A> {
    public int compareTo( A that);
    }
    //类型擦除后
    interface Comparable {
    public int compareTo( Object that);
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //代码示例 2
    final class NumericValue implements Comparable <NumericValue> {
    priva byte value;
    public NumericValue (byte value) { this.value = value; }
    public byte getValue() { return value; }
    public int compareTo( NumericValue that) { return this.value - that.value; }
    }
    //类型擦除后
    final class NumericValue implements java.lang.Comparable{
    //域
    private byte value;
    //构造器
    public NumericValue(byte);
    //方法
    public int compareTo(NumericValue);
    public volatile int compareTo(java.lang.Object); //桥方法
    public byte getValue( );
    }

    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
    //代码示例 3
    class Collections {
    public static <A extends Comparable<A>> A max(Collection <A> xs) {
    Iterator<A> xi = xs.iterator();
    A w = xi.next();
    while(xi.hasNext()) {
    A x = xi.next();
    if(w.compareTo(x) < 0)
    w = x;
    }
    return w;
    }
    }
    //类型擦除后
    class Collections {
    public static Comparable max(Collection xs) {
    Iterator xi = xs.iterator();
    Comparable w = (Comparable) xi.next();
    while (xi.hasNext()) {
    Comparable x = (Comparable) xi.next();
    if (w.compareTo(x) < 0) w = x;
    }
    return w;
    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //代码示例 4
    final class Test {
    public static void main (String[] args) {
    LinkedList<NumericValue> numberList = new LinkedList<NumericValue> ();
    numberList.add(new NumericValue((byte)0));
    numberList.add(new NumericValue((byte)1));
    NumericValue y = Collections.max( numberList );
    }
    }
    //类型擦除后
    final class Test {
    public static void main (String[ ] args) {
    LinkedList numberList = new LinkedList();
    numberList.add(new NumericValue((byte)0)); ,
    numberList.add(new NumericValue((byte)1));
    NumericValue y = (NumericValue) Collections.max( numberList );
    }
    }

      第一个泛型类被擦除后, A被替换为最左边界 Object。由于Comparable是一个泛型接口,所以Comparable的类型参数NumericValue被擦除掉并将相关参数置换为 Object,但是这直接导致 NumericValue 没有实现接口(重写)Comparable的compareTo(Object that)方法,于是编译器充当好人,添加了一个桥方法(由编译器在编译时自动添加)

      第二个示例中限定了类型参数的边界,A必须为Comparable的子类,按照类型擦除的过程,先将所有的类型参数替换为最左边界Comparable,得到最终的擦除后结果。

泛型带来的问题及解决方法

  1. 以参数化类型与原始类型的兼容性说明引用是类型检查所针对的对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Test10 {
    public static void main(String[] args) {
    ArrayList<String> arrayList1=new ArrayList();
    arrayList1.add("1"); //编译通过
    arrayList1.add(1); //编译错误
    String str1=arrayList1.get(0); //返回类型就是 String
    ArrayList arrayList2=new ArrayList<String>();
    arrayList2.add("1"); //编译通过
    arrayList2.add(1); //编译通过
    Object object=arrayList2.get(0); //返回类型就是 Object
    new ArrayList<String>().add("11"); //编译通过
    new ArrayList<String>().add(22); //编译错误
    String string=new ArrayList<String>().get(0); //返回类型就是 String
    }
    }

    ​ 因此我们可以得出结论:类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

  2. 所有动作都发生在边界处(对传递进来的值,编译器进行额外的检查;对真正传递出去的值,编译器自动插入的转型)

    ​ 因为类型擦除的问题,所以所有的泛型类型最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 代码片段1
    public class SimpleHolder {
    private Object obj;
    public void setObj(Object obj) {
    this.obj = obj;
    }
    public Object getObj() {
    return obj;
    }
    public static void main(String[] args) {
    SimpleHolder holder = new SimpleHolder();
    holder.setObj("Item");
    String s = (String)holder.getObj();
    }
    }

    反编译这个类,得到下面代码片段:

    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
    public void setObj(java.lang.Object);
    Code:
    0: aload_0
    1: aload_1
    2: putfield #2; //Field obj:Ljava/lang/Object;
    5: return
    public java.lang.Object getObj();
    Code:
    0: aload_0
    1: getfield #2; //Field obj:Ljava/lang/Object;
    4: areturn
    public static void main(java.lang.String[]);
    Code:
    0: new #3; //class SimpleHolder
    3: dup
    4: invokespecial #4; //Method "<init>":()V
    7: astore_1
    8: aload_1
    9: ldc #5; //String Item
    11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V
    14: aload_1
    15: invokevirtual #7; //Method getObj:()Ljava/lang/Object;
    18: checkcast #8; //class java/lang/String
    21: astore_2
    22: return

    现将泛型应用到上述代码,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 代码片段 2
    public class GenericHolder<T> {
    private T obj;
    public void setObj(T obj) {
    this.obj = obj;
    }
    public T getObj() {
    return obj;
    }
    public static void main(String[] args) {
    GenericHolder<String> holder = new GenericHolder<String>();
    holder.setObj("Item");
    String s = holder.getObj();
    }
    }

    反编译这个类,得到下面代码片段:

    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
    public void setObj(java.lang.Object);
    Code:
    0: aload_0
    1: aload_1
    2: putfield #2; //Field obj:Ljava/lang/Object;
    5: return
    public java.lang.Object getObj();
    Code:
    0: aload_0
    1: getfield #2; //Field obj:Ljava/lang/Object;
    4: areturn
    public static void main(java.lang.String[]);
    Code:
    0: new #3; //class GenericHolder
    3: dup
    4: invokespecial #4; //Method "<init>":()V
    7: astore_1
    8: aload_1
    9: ldc #5; //String Item
    11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V
    14: aload_1
    15: invokevirtual #7; //Method getObj:()Ljava/lang/Object;
    18: checkcast #8; //class java/lang/String
    21: astore_2
    22: return

    在上述应用泛型的代码中,将

    1
    String s = holder.getObj();

    替换为

    1
    holder.getObj();

    反编译后,有代码片段:

    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
    public void setObj(java.lang.Object);
    Code:
    0: aload_0
    1: aload_1
    2: putfield #2; //Field obj:Ljava/lang/Object;
    5: return
    public java.lang.Object getObj();
    Code:
    0: aload_0
    1: getfield #2; //Field obj:Ljava/lang/Object;
    4: areturn
    public static void main(java.lang.String[]);
    Code:
    0: new #3; //class GenericHolder
    3: dup
    4: invokespecial #4; //Method "<init>":()V
    7: astore_1
    8: aload_1
    9: ldc #5; //String Item
    11: invokevirtual #6; //Method setObj:(Ljava/lang/Object;)V
    14: aload_1
    15: invokevirtual #7; //Method getObj:()Ljava/lang/Object;
    18: pop
    19: return
    }
    • 首先,代码片段 1 和代码片段 2 二者所产生的字节码是相同的。看第15,它调用的是getObj()方法,返回值是Object,说明类型擦除了。然后第18,它做了一个checkcast操作,即检查类型#8, 在上面找#8引用的类型,它是一个String类型,即作String类型的强转。所以不是在get方法里强转的,是在你调用的地方强转的。对进入setObj()的类型进行检查是不需要的,因为这将由编译器执行。而对从getObj()返回的值进行转型仍旧是需要的,但这与你自己必须执行的操作是一样的–此处它将由编译器自动插入。也就是说,在泛型中,所有动作都发生在边界处对传递进来的值进行额外的编译器检查,并由编译器自动插入对传递出去的值的转型
    • 其次,在未将 getObj() 的值赋给String时,由代码片段可知,编译器并未自动插入转型代码,可见所谓编译器自动插入对传递出去的值的转型的前提条件是:其必须是真正传递出去,即必须赋值给引用.(当然,虽然 getObj() 的返回值的类型是 Object, 但是其实质上是一个 String, 因此直接进行操作 “ getObj() instanceof String ”时,返回值也是 true.)

    再看一段代码:

    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
    public class GenericArray<T> {
    private T[] array;
    public GenericArray(int sz) {
    array = (T[]) new Object[sz];
    }
    public void put(int index, T item) {
    array[index] = item;
    }
    public T get(int index) {
    return array[index];
    }
    public T[] rep() { return array; }
    public static void main(String[] args) {
    GenericArray<Integer> gai = new GenericArray<Integer>(10);
    gai.put(0, new Integer(4));
    gai.get(0);
    Integer i = gai.get(0);
    // This causes a ClassCastException:
    Integer[] ia = gai.rep();
    // This is OK:
    Object[] oa = (Object[])gai.rep();
    }
    }

    反编译可得

    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
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    public class GenericArray extends java.lang.Object{
    public GenericArray(int);
    Code:
    0: aload_0
    1: invokespecial #1; //Method java/lang/Object."<init>":()V
    4: aload_0
    5: iload_1
    6: anewarray #2; //class java/lang/Object
    9: checkcast #3; //class "[Ljava/lang/Object;"
    12: putfield #4; //Field array:[Ljava/lang/Object;
    15: return
    public void put(int, java.lang.Object);
    Code:
    0: aload_0
    1: getfield #4; //Field array:[Ljava/lang/Object;
    4: iload_1
    5: aload_2
    6: aastore
    7: return
    public java.lang.Object get(int);
    Code:
    0: aload_0
    1: getfield #4; //Field array:[Ljava/lang/Object;
    4: iload_1
    5: aaload
    6: areturn
    public java.lang.Object[] rep();
    Code:
    0: aload_0
    1: getfield #4; //Field array:[Ljava/lang/Object;
    4: areturn
    public static void main(java.lang.String[]);
    Code:
    0: new #5; //class GenericArray
    3: dup
    4: bipush 10
    6: invokespecial #6; //Method "<init>":(I)V
    9: astore_1
    10: aload_1
    11: iconst_0
    12: new #7; //class java/lang/Integer
    15: dup
    16: iconst_4
    17: invokespecial #8; //Method java/lang/Integer."<init>":(I)V
    20: invokevirtual #9; //Method put:(ILjava/lang/Object;)V
    23: aload_1
    24: iconst_0
    25: invokevirtual #10; //Method get:(I)Ljava/lang/Object;
    28: pop
    29: aload_1
    30: iconst_0
    31: invokevirtual #10; //Method get:(I)Ljava/lang/Object;
    34: checkcast #7; //class java/lang/Integer
    37: astore_2
    38: aload_1
    39: invokevirtual #11; //Method rep:()[Ljava/lang/Object;
    42: checkcast #12; //class "[Ljava/lang/Integer;"
    45: astore_3
    46: aload_1
    47: invokevirtual #11; //Method rep:()[Ljava/lang/Object;
    50: checkcast #3; //class "[Ljava/lang/Object;"
    53: astore 4
    55: return
    }

    ​ 结合上面的结论,仔细观察反编译后代码中 checkcast 都用在什么地方,加深对边界就是发生动作的地方自动转型发生在调用处(需要检验两种类型时)的理解.

    • 25显示调用后,直接pop,而31显示在调用处,还要进行 checkcast 操作;
    • 由于类型擦除,操作39之后,进行 checkcast 操作,强转为 Ljava.lang.Integer ,但是由代码array = (T[]) new Object[sz];可知,其 new 的是 Object 数组,是不可能成功强转到 Integer 数组的,就像 Object 对象不能成功强转到 Integer 对象一样,会在运行时抛出 ClassCastException 异常;
    • 由于类型擦除,操作47之后,进行 checkcast 操作,由于 rep() 返回的即为 Object 数组,而其要赋给的引用也是 Object[] ,因此不会抛出任何异常。

  3. 类型擦除与多态的冲突及其解决办法

    先看两段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 第一段代码
    public class Pair<T> {
    private T first;
    private T second;
    public Pair(T first, T second){
    this.first = first;
    this.second = second;
    }
    public void setFirst(T first){
    this.first = first;
    }
    public T getFirst(){
    return first;
    }
    public void setSecond(T second){
    this.second = second;
    }
    public T getSecond(){
    return second;
    }
    }

    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
    // 第二段代码
    public class DateInterval extends Pair<Date> { // 时间间隔类
    public DateInterval(Date first, Date second){
    super(first, second);
    }
    @Override
    public void setSecond(Date second) {
    super.setSecond(second);
    }
    @Override
    public Date getSecond(){
    return super.getSecond();
    }
    public static void main(String[] args) {
    DateInterval interval = new DateInterval(new Date(), new Date());
    Pair<Date> pair = interval; //超类,多态
    Date date = new Date(2000, 1, 1);
    System.out.println("原来的日期:"+pair.getSecond());
    System.out.println("set进新日期:"+date);
    pair.setSecond(date);
    System.out.println("执行pair.setSecond(date)后的日期:"+pair.getSecond());
    }
    }

    原本子类重写父类的方法,无可非议。但是泛型类的类型擦除造成了一个问题,Pair的原始类型中存在方法:

    1
    public void setSecond(Object second);

    DateInterval中的方法:

    1
    public void setSecond(Date second) ;

     我们的本意是想重写父类Pair中的setSecond方法,但是从方法签名上看,这完全是两个不同的方法,类型擦除与多态产生了冲突。而实际情况呢?运行DateInterval的main方法,我们看到public void setSecond(Date second)的确重写了public void setSecond(Object second)方法。这是如何做到的呢?

    使用Java类分析器对其进行分析,结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class DateInterval extends Pair{
    //构造器
    public DateInterval(java.util.Date, java.util.Date);
    //方法
    public void setSecond(java.util.Date);
    public volatile void setSecond(java.lang.Object); //方法 1
    public java.util.Date getSecond( ); //方法 2
    public volatile java.lang.Object getSecond( ); //方法 3,它难道不会和方法 2 冲突?
    public static void main(java.lang.String[]);
    }

      方法1和方法3是我们在源码中不曾定义的,它肯定是由编译器生成的。这个方法称为 桥方法(bridge method),真正覆写超类方法的是它。语句pair.setSecond(date)实际上调用的是方法1[public volatile void setSecond(Object)],通过这个方法再去调用public void setSecond(Date)。这个桥方法的实际内容是:

    1
    2
    3
    public void setSecond(Object second){
    this.setSecond((java.util.Date) second);
    }

      这样的结果就符合面向对象中多态的特性了,实现了方法的动态绑定。但是,这样的做法给我们带来了一种错觉,就认为public void setSecond(Date)覆写了泛型类的public void setSecond(Object)【其实也不是重写,二者方法参数都不同】,如果我们在DateInterval中增加一个方法:

    1
    2
    3
    public void setSecond(Object obj){
    System.out.println("覆写超类方法!");
    }

    ​ 编译器会报如下错误:Name clash: The method setSecond(Object) of type DateInter has the same erasure as setSecond(T) of type Pair<T> but doesn't override it.即,同一个方法不能被重写两次。

    ​ 为了实现多态,我们知道方法3也是由编译器生成的桥方法。方法擦除带来的第二个问题就是:由编译器生成的桥方法public volatile java.lang.Object getSecond()方法和public java.util.Date getSecond() 方法,从方法签名的角度看是两个完全相同的方法,它们怎么可以共存呢? 如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情。

  4. 泛型类型变量不能是基本数据类型

    类型参数不能是基本类型。也就是说,没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

  5. 转型和警告

    使用带有泛型类型参数的转型或 instanceof 不会有任何效果**,例如

    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
    class FixedSizeStack<T> {
    private int index = 0;
    private Object[] storage;
    public FixedSizeStack(int size) {
    storage = new Object[size];
    }
    public void push(T item) {
    storage[index++] = item;
    }
    public T pop() {
    //Warnning: Unchecked cast from Object to T
    return (T) storage[--index];
    }
    }
    public class GenericCast {
    public static final int SIZE = 10;
    public static void main(String[] args) {
    FixedSizeStack<String> strings = new FixedSizeStack<String>(SIZE);
    for (String s : "A B C D E F G H I J".split(" "))
    strings.push(s);
    for (int i = 0; i < SIZE; i++) {
    String s = strings.pop();
    System.out.print(s + " ");
    }
    }
    }

      由于擦除的原因,T 被擦除到它的第一个边界 Object,因此pop()实际上只是将Object转型为Object。换句话说,pop()方法实际上并没有执行任何转型。

  6. 任何在运行时需要知道确切类型信息的操作都将无法工作

    • instanceof操作 的右操作数不能带有泛型类型参数;

    • new 操作 :可以 new 泛型类型(eg: ArrayList,…),但不能 new 泛型参数(T,…);

    • 泛型数组 :不可以创建带有泛型类型参数的数组(若需要收集参数化类型对象,可以直接使用 ArrayList:ArrayList<Pair<String>>最安全且有效。);

    • 转型 :带有泛型类型参数的转型不会有任何效果;

    • 关于由类型擦除引起的 instance of T,new T 和创建数组T 等问题,可以引入类型标签Class来解决

      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
      class Building {}
      class House extends Building {}
      public class ClassTypeCapture<T> {
      Class<T> kind;
      public ClassTypeCapture(Class<T> kind) {
      this.kind = kind;
      }
      public boolean f(Object arg) {
      return kind.isInstance(arg);
      }
      public static void main(String[] args) {
      ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);
      System.out.println(ctt1.f(new Building())); // true
      System.out.println(ctt1.f(new House())); // true
      ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);
      System.out.println(ctt2.f(new Building())); // true
      System.out.println(ctt2.f(new House())); // true
      }
      }

  7. 实现参数化接口

    一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口,例如:

    1
    2
    public Person implements Comparable<Person>{ ... } // OK
    class HonorPerson extends Person implements Comparable<HonorPerson>{ ... } // Error

    ​ HonorPerson 类不能编译,因为擦除会将Comparable<Person>Comparable<HonorPerson>简化为相同的接口 Comparable, 上面的代码意味着重复实现相同的接口。但是,下面的代码可以通过编译:

    1
    2
    public Person implements Comparable{ ... } // OK
    class HonorPerson extends Person implements Comparable{ ... } // OK

    这种差别在于:编译器对泛型的特别处理方式。

  8. 异常中使用泛型的问题

    由于类型擦除的原因,将泛型应用于异常是非常受限的。catch 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。

    • 不能抛出也不能捕获泛型类的对象

      事实上,泛型类扩展Throwable都不合法(Exception是Throwable的子类)。例如:下面的定义将不会通过编译

      1
      public class Problem<T> extends Exception{......}

        为什么不能扩展Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,再看下面的定义:

      1
      2
      3
      4
      5
      6
      try{
      }catch(Problem<Integer> e1){
      ...
      }catch(Problem<Number> e2){
      ...
      }

      ​ 在运行时,类型信息被擦除后,那么两个地方的catch都变为原始类型Object,那么也就是说,这两个地方的catch变的一模一样,就“相当于”下面的这样:

      1
      2
      3
      4
      5
      6
      try{
      }catch(Problem<Object> e1){
      ...
      }catch(Problem<Object> e2){
      ...
      }

       这当然就是不行的, 就好像catch了两个一模一样的普通异常,编译器就不能通过编译一样。


    • 不能再catch子句中使用泛型变量

      1
      2
      3
      4
      5
      6
      7
      public static <T extends Throwable> void doWork(Class<T> t){
      try{
      ...
      }catch(T e){ //编译错误
      ...
      }
      }

      ​ 因为泛型信息在编译的时候已经变为原始类型,也就是说上面的 T 会变为原始类型Throwable,那么如果可以再catch子句中使用泛型变量,那么,下面的定义呢:

      1
      2
      3
      4
      5
      6
      7
      8
      public static <T extends Throwable> void doWork(Class<T> t){
      try{
      ...
      }catch(T e){ //编译错误
      ...
      }catch(IndexOutOfBounds e){
      }
      }

      ​ 根据异常捕获的原则,一定是子类在前面,父类在后面,那么上面就违背了这个原则。所以java为了避免这样的情况,禁止在catch子句中使用泛型变量。


    • 类型变量可以使用在异常声明中

      1
      2
      3
      4
      5
      6
      7
      public static<T extends Throwable> void doWork(T t) throws T{
      try{
      ...
      }catch(Throwable realCause){
      t.initCause(realCause);
      throw t;
      }

      此时,虽然T也会被擦除为Throwable,但由于用在声明中,因此是合法的。

  9. 类型擦除后的冲突

    当泛型类型被擦除后,创建条件不能产生冲突:

    1
    2
    3
    4
    5
    class Pair<T> {
    public boolean equals(T value) {
    return null;
    }
    }

    考虑Pair<>:

    1
    public boolean equals(T value){}

    擦除后变成了

    1
    boolean equals(Object)

    这与 Object.equals 方法是冲突的!当然,补救的办法是重新命名引发错误的方法。

  10. 动态类型安全

    先看以下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class CheckedList {
    @SuppressWarnings("unchecked")
    static void oldStyleMethod(List probablyDogs) { //原生List
    probablyDogs.add(new Cat());
    }
    public static void main(String[] args) {
    List<Dog> dogs1 = new ArrayList<Dog>();
    oldStyleMethod(dogs1); // Quietly accepts a Cat
    List<Dog> dogs2 = Collections.checkedList(
    new ArrayList<Dog>(), Dog.class);
    try {
    oldStyleMethod(dogs2); // Throws an exception
    } catch(Exception e) {
    System.out.println(e);
    }
    // Derived types work fine:
    List<Pet> pets = Collections.checkedList(
    new ArrayList<Pet>(), Pet.class);
    pets.add(new Dog());
    pets.add(new Cat());
    }
    } /* Output:
    java.lang.ClassCastException: Attempt to insert class typeinfo.pets.Cat
    element into collection with element type class typeinfo.pets.Dog

    ​ 使用 Collections 的静态方法:checkedCollection( ), checkedList( ), checkedMap( ), checkedSet( ), checkedSortedMap( ) 和 checkedSortedSet( )可以在运行时便知道罪魁祸首在哪里,而不必等到将对象从容器中取出时。