泛型

泛型,即泛化类型。本质是将数据类型指定为参数——参数化类型。泛型程序设计(Generic Programming)意味着编写的代码可以被很多不同类型的对象所重用。

【类型参数的理解】

类似于函数中的形参和实参一样,当一个泛型声明被调用,实际类型参数(actual type arguments)取代形式类型参数。

与c++中的Template的重要区别是,java的泛型是通过擦除实现的!所以,并不是直接把所有的类型参数简单地替换成实际类型。在java里,一个泛型类型的声明只被编译一次,并且得到一个class文件,就像普通的class或者interface的声明一样。而类型参数就是这个class的一个参数。譬如ArrayList会是一个class文件,ArrayList<String>和ArrayList<Integer>的对象都共用ArrayList.class。

值得注意的是,泛型的限制之一是:泛型与继承没有关系!!!!尽管String是Object的子类,但ArrayList<Object>和ArrayList<String>之间没有继承关系!!所以不能把ArrayList<String>的变量赋给ArrayList<Object>。

** 引入泛型之后,继承这个概念存在两个维度:一个是类型参数本身的继承体系,譬如String和Object。另一个是泛型类或接口自身的继承体系,譬如List和ArrayList。

一、引入泛型的意义

在Java1.5之前,只能通过Object是所有类型的父类和类型强制转换来共同实现类型泛化。但这要求程序员清晰地知道具体类型,编译器无法检查类型强制转换是否正确。只有运行期的JVM才知道是否正确,大量ClassCastException的风险被转嫁到程序运行期。而有了泛型,编译器就能通过类型参数来保证类型转换的正确性,增强了程序的可读性和稳定性

image

二、Java的泛型是伪泛型(语法糖)

Java 语言中的泛型基本上完全在编译器中实现,由编译器执行类型检查和类型推断,然后生成普通的非泛型的字节码。——擦除技术erasure(编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除)。

Java的泛型只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码。——不需要改动原来的JVM就支持泛型,因此泛型其实是Java的一颗语法糖。对于JVM来说,ArrayList<Integer>和ArrayList<String>是同一个类型,String和Integer已在编译期被擦除,只剩下ArrayList。

如果通过反射来调用list的add方法,可以绕过编译器的类型检查!!!

image

image

三、类型擦除

3.1 类型擦除的过程

首先是找到用来替换类型参数的具体类。这个具体类一般是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)方法。这个时候就由编译器来动态生成这个方法。

3.2 类型擦除引起的一些问题

java.lang.Class是泛型化的。Class有一个类型参数T,代表Class对象代表的类型。但是一个具体的泛型类是没有独享的Class类对象的。List<String>和List<Integer>的Class对象都是List.class。

因此,静态成员和方法、静态代码块都是由泛型类的所有实例共享的,无论是new MyClass<String>还是new MyClass<Integer>创建的对象,都共享静态成员和方法、静态代码块,所以不允许在静态成员和方法、静态代码块中使用类型参数!——容易产生冲突。

类型参数不能用于异常类型,因为异常处理是由JVM在运行时刻进行的,由于类型擦除,JVM无法区分catch语句中的两个异常类型MyException<String>和MyException<Integer>。

由于大多数情况下,擦除后的类型就会是Object,而Object是不能存储基本类型的,所以类型参数不允许是基本类型

四、通配符

前面提到,泛型的限制之一是不能维持继承关系。通配符用于支持灵活的子类化。

4.1 无限定通配符(?)

Collection<?>表示一个集合,它的元素类型可以匹配任意类型。

注意区分Collection<Object>,该集合的元素类型只能是Object,不含子类。尽管写入时,可以接受子类向上转型为Object再写入,但读出时一律是Object类型的,需要根据实际类型进行强制转换。

image

image

在上例中,List使用通配符后无法写入null以外的对象,原因在于,Collection接口的定义中,add()方法的入参使用的是在类上声明的类型参数,而该List对象的类型参数是通配符,add方法无法判断传入的具体类型,这是不安全的操作,所以禁止。但读出元素时,由于一定会是个Object,这是安全的。

image

4.2 上限通配符(? extends Parent)

List<? extends Parent>表示元素类型是Parent类及其子类。同样存在只读限制,因为传入类型虽然有界,但具体类型仍是未知的。改进是读出元素时,可以把边界缩小到Parent。

image

4.3 下限通配符(? super Child)

List<? super Child>表示元素类型是Child及其父类。只允许写入下界对应的类型,因为其祖先类型是未知的,有可能接口不一致,如果允许写入会存在安全隐患。上界未知,所以上界实际上还是Object,读出时只能用Object来接收。

image

4.4 原始类型(raw type)

原始类型就是擦除了泛型信息,最终在字节码中的真实类型。

用于与泛型之前的老代码兼容。类似于通配符,但类型检查更宽松,使用时会产生未检查警告(unchecked warning, rawtypes)。

对于无界通配符和下限通配符,擦除后类型为Object,对于上限通配符,擦除后类型为上界。所谓的界定,其实只在编译器进行类型检查时有效,在运行时全部无效。所以在运行期使用类型参数,就会产生错误。

五、泛型和数组

java中的数组是协变的,即:如果Integer扩展了Number,那么不仅Integer是Number,Integer[]也是Number[]。在要求Number[]的地方,可以传入一个Integer[]型的引用。也就是说,当Number是Integer的超类型时,Number[]是Integer[]的超类型。

java的泛型不是协变的。因为List<Number>并不是List<Integer>的超类型。原因在于,这是不安全的。Number可能有多个子类,譬如Integer和Float。如果允许,就会允许把List<Integer>的引用赋给List<Number>,同时允许将Float类型的元素写入该list中。但实际上该List中应存放Integer,编译器完全无法检查这种情况,问题抛给了运行期。

数组能协变而泛型不能协变的后果就是,不能实例化泛型类型的数组。如果允许泛型类型的数组,就可能把ArrayList<String>[]的引用赋给Object[],这样就可能把ArrayList<Integer>引用放入该Object数组中,这时再使用ArrayList<String>的引用去读取元素时,就会发现类型转换错误,因为泛型是不协变的,ArrayList<Integer>不能转为ArrayList<String>。

成功创建泛型数组的唯一方式是创建一个类型擦除的数组,然后转型:

image

不能实例化用类型参数表示的类型数组。编译器不知道 V到底表示什么类型,因此不能实例化 V数组。

Collections 类通过一种别扭的方法绕过了这个问题,在 Collections 类编译时会产生类型未检查转换的警告。

class ArrayList<V> { 
  private V[] backingArray; 
  public ArrayList() { 
    backingArray = (V[]) new Object[DEFAULT_SIZE]; 
  } 
 }

因为泛型是通过擦除实现的,backingArray的类型实际上就是 Object[],因为 Object代替了 V。这意味着:实际上这个类期望 backingArray是一个 Object数组,但是编译器要进行额外的类型检查,以确保它包含 V类型的对象。所以这种方法很奏效,但是非常别扭,因此不值得效仿。

六、泛型和构造函数

不能使用类型参数来访问构造函数,譬如:new T(param)。因为在编译时并不知道要构造什么类,因此调用哪个构造函数是不确定的,也许该类中并没有对应的构造函数。

不能用通配符类型的参数调用泛型构造函数,即使知道存在这样的构造函数也不行,譬如:new HashSet<?>(set);  // illegal

七、泛型和多态

7.1 擦除与多态的冲突

假设有以下父类:

class Pair<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}

子类:

class DateInter extends Pair<Date> {
    @Override
    public void setValue(Date value) {
        super.setValue(value);
    }
    @Override
    public Date getValue() {
        return super.getValue();
    }
}

父类经过擦除后,类型参数用Object替换。这样一来,子类中“覆盖”的方法,实际上是重载,而不是重写,也就是说,子类中应该有4个方法。这与本意相悖。泛型和多态出现了冲突。

7.2 桥方法

事实上,子类中的两个方法确实是重写了。JVM采用了桥方法来完成这一功能。而最终编译的字节码中也确实存在了4个方法,只不过多出来的那两个方法就是桥方法,它们的作用是去调用我们重写的那两个方法。

public Object getValue() {
        return super.getValue();
} //桥方法

public Date getValue() {
        return super.getValue();
} //实际重写的方法

注意到这两个方法的签名完全一样,由JVM来区别哪个是桥方法……

八、泛型方法

在泛型方法中,类型参数的重要作用是用来表示多个参数之间的依赖关系。如果没有依赖关系,而是用于多态,那么应使用通配符。

在很多泛型方法中,类型参数和通配符配合使用。

image

image

image

 

 

 

参考资料

http://www.ibm.com/developerworks/cn/java/j-jtp01255.html

http://www.infoq.com/cn/articles/cf-java-generics

http://blog.csdn.net/lonelyroamer/article/details/7868820

posted @ 2016-07-30 23:29  流年素心  阅读(866)  评论(0编辑  收藏  举报