Java 泛型相关

来源:互联网 发布:淘宝一千零一夜从哪看 编辑:程序博客网 时间:2024/06/05 21:10

Java 泛型介绍

Java 泛型(generics) 是 JDK 5 中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在 JDK 5 中的新集合类框架中。对于泛型概念的引入,开发社区的观点是褒贬不一。从好的方面来说,泛型的引入可以解决之前的集合类框架在使用过程中通常会出现的运行时刻类型错误,因为编译器可以在编译时刻就发现很多明显的错误。而从不好的地方来说,为了保证与旧有版本的兼容性,Java 泛型的实现上存在着一些不够优雅的地方。当然这也是任何有历史的编程语言所需要承担的历史包袱。后续的版本更新会为早期的设计缺陷所累。

开发人员在使用泛型的时候,很容易根据自己的直觉而犯一些错误。比如一个方法如果接收 List<Object> 作为形式参数,那么如果尝试将一个 List<String> 的对象作为实际参数传进去,却发现无法通过编译。虽然从直觉上来说,Object 是 String 的父类,这种类型转换应该是合理的。但是实际上这会产生隐含的类型转换问题,因此编译器直接就禁止这样的行为。

各种语言中的编译器是如何处理泛型的

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

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

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

C++ 中的模板 (template) 是典型的 Code specialization 实现。C++ 编译器会为每一个泛型类实例生成一份执行代码。执行代码中 integer list 和 string list 是两种不同的类型。这样会导致代码膨胀 (code bloat)。 C# 里面泛型无论在程序源码中、编译后的 IL 中(Intermediate Language,中间语言,这时候泛型是一个占位符) 或是运行期的 CLR 中都是切实存在的,List<int> 与 List<String> 就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型被称为真实泛型。 Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原生类型 (Raw Type,也称为裸类型) 了,并且在相应的地方插入了强制转型代码,因此对于运行期的 Java 语言来说,ArrayList<int> 与 ArrayList<String> 就是同一个类。所以说泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。

C++ 和 C# 是使用 Code specialization 的处理机制,前面提到,他有一个缺点,那就是会导致代码膨胀。另外一个弊端是在引用类型系统中,浪费空间,因为引用类型集合中元素本质上都是一个指针。没必要为每个类型都产生一份执行代码。而这也是 Java 编译器中采用 Code sharing 方式处理泛型的主要原因。

Java 编译器通过 Code sharing 方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除 (type erasue) 实现的。


类型擦除

什么是类型擦除

研究泛型的过程中,发现了一个比较令我意外的情况,Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。 其实编译器通过Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。

类型擦除的官方解释 JavaDoc

Type Erasure Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure to: Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods. Insert type casts if necessary to preserve type safety. Generate bridge methods to preserve polymorphism in extended generic types. Type erasure ensures that no new classes are created for parameterized types; consequently, generics incur no runtime overhead.

类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型 Java 代码转换为普通 Java 代码,只不过编译器更直接点,将泛型 Java 代码直接转换成普通 Java 字节码。 类型擦除的主要过程如下:

  1. 将所有的泛型参数用其最左边界 (最顶级的父类型) 类型替换。

  2. 移除所有的类型参数。

Java 编译器处理泛型的过程

code 1:

public static void main(Stirng[] args){    Map<String, String> map = new HashMap<String, String>();    map.put("name", "hollis");    map.put("age", "22");    System.out.println(map.get("name"));    System.out.println(map.get("age"));}

反编译后的 code 1:

public static void main(String[] args){    Map map = new HashMap();    map.put("name", "hollis");    map.put("age", "22");    System.out.println((String)map.get("name"));    System.out.println((String)map.get("age"));}

我们发现泛型都不见了,程序又变回了 Java 泛型出现之前的写法,泛型类型都变回了原生类型。

code 2:

interface Comparable<A>{    public int compareTo(A that);}public final class NumericValue implements Comparable<NumericValue>{    private byte value;    public NumericValue(byte value)    {        this.value = value;    }    public byte getValue()    {        return value;    }    public int compareTo(NumericValue that)    {        return this.value - that.value;    }} 

反编译后的 code 2:

interface Comparable{    public int compareTo(Object this);}public final class NumericValue implements Comparable{    private byte value;    public NumericValue(byte value)    {        this.value = value;    }    public byte getValue()    {        return value;    }    public int compareTo(NumericValue that)    {        return value - that.value;    }    public volatile int compareTo(Object obj)    {        return compareTo((NumericValue)obj);    }}

泛型类 Comparable <A> 擦除后 A被替换为最左边界 ObjectComparable<NumericValue> 的类型参数 NumericValue 被擦除掉,但是这直接导致 NumericValue 没有实现接口 ComparablecompareTo(Object that) 方法,于是编译器充当好人,添加了一个桥接方法。

code 3:

public class Collections{    public static <A extends Comparable<A>> A max(Collections<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;    }}

反编译后的 code 3:

public class Collections{    public 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.comparaTo(x) < 0) w = x;        }        return w;    }}

示例中限定了类型参数的边界<A extends Comparable<A>>AA必须为Comparable<A>的子类,按照类型擦除的过程,先讲所有的类型参数换为最左边界Comparable<A>,然后去掉参数类型A,得到最终的擦除后结果。

总结

  1. 虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的 Class 类对象。比如并不存在 List<String>.class 或是 List<Integer>.class,而只有 List.class。

  2. 创建泛型对象时请指明类型,让编译器尽早的做参数检查 (Effective Java,第23条:请不要在新代码中使用原生态类型)

  3. 不要忽略编译器的警告信息,那意味着潜在的 ClassCastException 等着你。

  4. 静态变量是被泛型类的所有实例所共享的。对于声明为 MyClass<T> 的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过 new MyClass<String> 还是 new MyClass<Integer> 创建的对象,都是共享一个静态变量。

  5. 泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除, JVM 是无法区分两个异常类型 MyException<String> 和 MyException<Integer> 的。对于 JVM 来说,它们都是 MyException 类型的。也就无法执行与异常对应的 catch 语句。


Java 泛型中 extends 和 super 的理解

? 通配符类型

<? extends T> 表示类型的上界,表示参数化类型的可能是T 或是 T的子类

<? super T> 表示类型下界(Java Core中叫超类型限定),表示参数化类型是此类型的超类型(父类型),直至Object。

在Java的类型擦除我们提到过:类型擦除中第一步——将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。这里的左边界可以通过extends来体现。

当生成泛型类的字节码时,编译器用类型参数的擦除替换类型参数。对于无限制类型参数 (),它的擦除是 Object。对于上限类型参数(>),它的擦除是其上限(在本例中是 Comparable)的擦除。对于具有多个限制的类型参数,使用其最左限制的擦除。

extends

上界用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

比如,我们现在定义:List<? extends T>首先你很容易误解它为继承于T的所有类的集合,你可能认为,你定义的这个 List 可以用来 put 任何 T 的子类,那么我们看一下下面的代码:

import java.util.LinkedList;import java.util.List;public class testGeneric{    public static void main(String[] args)    {        List<? extends Season> seasonList = new LinkedList<>();        seasonList.add(new Spring());    }}class Season{}class Spring extends Season{}

seasonList.add(new Spring());这行会报错:The method put(Spring) is undefined for the type List<capture#1-of ? extends Season>。这是因为List<? extends Season> 表示 “具有任何从 Season 继承类型的列表”,编译器无法确定 List 所持有的类型,所以无法安全的向其中添加对象。可以添加 null,因为 null 可以表示任何类型。所以 List 的 add 方法不能添加任何有意义的元素,但是可以接受现有的子类型 List 赋值。你也许试图这样做:

List<? extends Season> seasonList = new LinkedList<Spring>();seasonList.add(new Spring());

但是,即使指明了Spring,也不能用add方法添加一个Spring对象。list中为什么不能加入Season类和Season类的子类呢,原因是这样的:List<? extends Season>表示上限是Seasonu,下面这样的赋值都是合法的:

List<? extends Season> list1 = new ArrayList<Season>();List<? extends Season> list2 = new ArrayList<Season>();List<? extends Season> list3 = new ArrayList<Season>();

如果List<? extends Season>支持add方法的方法合法的话,那么 list1 可以 add Season 和所有 Season 的子类;list2 可以 add Spring 和所有 Spring 的子类;list3 可以 add Winter 和所有 Winter 的子类。

这样的话,问题就出现了

List<? extends Season>所应该持有的对象是 Season 的子类,而且具体是哪一个子类还是个未知数,所以加入任何 Season 的子类都会有问题,因为如果 add Spring 的话,可能List<? extends Season>持有的对象是new ArrayList()Spring的加入肯定是不行的,如果add Winter的话,可能List<? extends Season>持有的对象是new ArrayList<Jonathan的子类>()Winter的加入又不合法,所以List<? extends Season> list不能进行add。但是,这种形式还是很有用的,虽然不能使用add方法,但是可以在初始化的时候一个Season指定不同的类型。比如:

List<? extends Season> list1 = getSeasonList();//getSeasonList方法会返回一个Season的子类的list

另外,由于我们已经保证了List中保存的是Season类或者他的某一个子类,所以,可以用get方法直接获得值:

List<? extends Season> seasonList = new LinkedList();Spring spring = (Spring) seasonList.get(0);Season season = seasonList.get(1);

super

下界用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至Object。

如:

List<Fruit> fruit = new ArrayList<Fruit>();List<? super Apple> fruitss = fruit;fruits.add(new Apple());    // workfruits.add(new RedApple()); // workfruits.add(new Fruit());    // compile errorfruits.add(new Object());   // compile error

这里的fruits是一个Apple的超类(父类,superclass)的List。同样地,出于对类型安全的考虑,我们可以加入Apple对象或者其任何子类(如RedApple)对象,但由于编译器并不知道List的内容究竟是Apple的哪个超类,因此不允许加入特定的任何超类型。

而当我们读取的时候,编译器在不知道是什么类型的情况下只能返回Object对象,因为Object是任何 Java 类的最终祖先类。

PECS 原则

如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)

如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)

如果既要存又要取,那么就不要使用任何通配符。

参考:Java泛型中的PECS原则


类型系统

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

引入泛型之后的类型系统增加了两个维度:一个是类型参数自身的继承体系结构,另外一个是泛型类或接口自身的继承体系结构。第一个指的是对于List<String>List<Object>这样的情况,类型参数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<?>的子类型,因此传递参数时不会发生错误。


总结

在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。

在代码中避免泛型类和原始类型的混用 (Effective Java中建议不要在代码中使用原始类型)。这样会产生一些编译器警告和潜在的运行时异常。当需要利用 JDK 5 之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。 不要忽视编译器给出的警告信息。


References

Java Generics FAQs by Angelika Langer

0 0