技术开发 频道

JAVA泛型浅析


IT168技术文档】 
    JAVA泛型和C++泛型的区别: 
    Java的泛型被定义成擦除,而C++的泛型则是扩展; 
    对于C++模板,当参数类型为不同的类型时,生成的模板实例也是不同的类型,如:定义类模板 
    Template <typename T> class A : public B<T>; 
    当实例化模板时 
        A<int> a; 
        A<std::string> b; 
    这里的a和b是两种不同的类型的实例; 
    Java则不是这样的,如泛化List<E>,分别用Integer和String来实例化, 
        List<Integer> l; 
        List<String> s; 
    通过反射机制看l和s的class,他们都是List!所有的参数类型都被编译器擦除了! 
    这样造成的结果是以往用C++模板可以实现某种契约(contract )的功能在Java中变得很另类了,举一个例子: 
    C++代码:
Template <typename T > class A{ Private: T x; Public: Void func(){ int ret = x.foo(12); } }
    上面这段代码在C++中经常能够看到,它暗中就设定了一个契约:凡是能否实例化这个模板的类型T,其必须具有一个公共的函数foo,这个函数返回整数,并且接受一个整数做为参数。 
    Java的代码:
public class A<E>{ private E e; public void func(){ int ret = e.foo(12); //编译错误啊… } }
     编译器给出的错误原因是foo函数对于类型中E是未定义的!!造成这样的问题的原因有两个: 
    1、 C++的编译器直到模板被使用(实例化)的时候才去编译模板,如果你不去使用模板C++编译器不会编译模板;而实例化的时候编译器已经能够确定具体的参数类型,所以能够检测契约是否符合;Java的编译器不是这样工作的,所以它在编译模板类型的时候不能够确定E到底有没有这个foo函数; 
    2、 类型擦除的结果;修改一下上面的程序,我们看看在func中到底能够调用什么函数,一看只能调用Object对象中的函数。所有的类型E都被擦除成Object了! 
    如果真的要想实现类似C++的契约,就必须确保参数类型E不被擦除成Object!需要如下修改代码:
public class A<E extends B>{ private E e; public void func(){ int ret = e.foo(12); } } Class B{ Public int foo(int param){…}; }
    这样虽然可以实现我们期望的形式,但是约束的程度要比C++的强很多,C++中,只要任意类型,其具有一个符合契约的函数,就可以实例化模板,而Java中,则要求所有的类型必须是给定类型的子类才可以实例化模板;

IT168技术文档】 
    擦除的原则: 
    1、 所有参数化容器类都被擦除成非参数化的(raw type);如List<E>、List<List<E>>都被擦除成List; 
    2、 所有参数化数组都被擦除成非参数化的数组;如List<E>[],被擦除成List[]; 
    3、 Raw type的容器类,被擦除成其自身,如List 被擦除成List; 
    4、 原生类型(int,String还有wrapper类)都擦除成他们的自身; 
    5、 参数类型E,被擦除成Object; 
    6、 所有约束参数如<? Extends E>、<X extends E>都被擦除成E; 
    7、 如果有多个约束,擦除成第一个,如<T extends Object & E>,则擦除成Object; 
    例如: 
    泛化代码:
List<String> words = new ArrayList<String>(); words.add("Hello "); words.add("world!"); String s = words.get(0)+words.get(1);
    擦除后就变成了:
List words = new ArrayList(); words.add("Hello "); words.add("world!"); String s = ((String)words.get(0))+((String)words.get(1))
    擦除后的代码和以前没有泛型时候写的代码没有任何区别! 
    再例如: 
    泛化代码:
public class textReader<T>{ private T a; public textReader(T b){ this.a = b; } public T getA(){ return a; } public static void main(String[] agrvs){ String in = "1234567890"; textReader<String> test = new textReader<String>(in); String out = test.getA(); System.out.println(out); } }
    擦除后(所有类型参数都被去掉,T被擦除成Object)就变成(注意红色部分):
public class textReader{ private Object a; public textReader(Object b){ this.a = b; } public Object getA(){ return a; } public static void main(String[] agrvs){ String in = "1234567890"; textReader test = new textReader (in); String out = (String)test.getA(); System.out.println(out); } }

IT168技术文档】 
    擦除所带来的问题: 
    1、 静态成员共享问题 
        List<Integer> ints = Arrays.asList(1,2,3); 
        List<String> strings = Arrays.asList("one","two"); 
        assert ints.getClass() == strings.getClass(); 
    ints和strings两个对象最终被擦除成具有相同类型的(List)的对象,于是这两个对象共享List的静态成员,于是就可以得出这样的结论,所有泛化类型的静态成员被其所有的实例化对象共享,因此也就要求所有静态成员不能够是泛化的!
class Foo<T> { private final T value; private static List<T> values = new ArrayList<T>(); //非法 public T getValue() { return value; } public static List<Object> values = new ArrayList<Object>() // ok }
    2、 过载(overload)冲突问题 
    函数过载的定义这样的:在一个类的范围内,如果两个函数具有相同的函数名称,不同的参数(返回值不考虑)就互相称为过载函数。看一个例子:
Class A{ Public int foo(int a){}; Public int foo(float f){}; // 是过载,编译没有问题 Public int foo(int a){}; Public float foo(int f){}; // 报错 Public static int foo1(List<Integer> a){} Public static int foo1(List<String> s){} //编译有错误,
//因为所有的List<T>都被擦除成List,这样两个函数重复定义,报错;
Public static int foo1(List<Integer> a){} Public static String foo1(List<String> s){} //没有问题,编译器不会报错! }
    3、 接口实现 
    一个类不能同时实现具有相同擦除效果的接口,例如:
class Foo implements Comparable<Integer>, Comparable<Long>
Comparable<Integer>, Comparable<Long>都被擦除成Comparable。

IT168技术文档】 
    继承关系: 
    1、 原始继承:就是我们经常提到的继承关系,如ArrayList是List的子类; 
    2、 泛化继承: 
        a) 泛化后的ArrayList<T>依旧是List<T>的子类;其中T是参数化类型 
        b) 如果类型T是类型B的子类,那么List<T>不是List<B>的子类 
        c) List<T>是List<? extends T>的子类 
        d) List<T>是List<? extends B>的子类 
        e) List<T>是List<? super T>的子类 
        f) List<B>是List<? super T>的子类 
        g) 如果类型T是类型B的子类,那么T[]是B[]的子类 
    3、 关于协变式(covariant)、不变式(invariant)和反变式(contravariant): 
        a) 数组和扩展类(extends)的泛化是协变式,即如果类型T是类型B的子类,那么T[]是B[]的子类;List<T>是List<? extends B>的子类 
        b) 非扩展类泛型是不变式,即如果类型T是类型B的子类,那么List<T>不是List<B>的子类 
        c) Super类泛型是反变式,即如果B是T的超类,则List<B> 是List<? super T>的子类 

    Get和Put原则: 
    当从一个泛化的结构中取数据的时候请使用extends通配,当往一个泛化的结构中放数据的时候请使用super通配; 
    当需要同时从一个泛化结构中读取和写入数据是,请不使用通配符号; 
    为什么会这样,我们简单的分析一下:假定类型T继承自A和B,类型C和D又从T类型继承,那么List<? extends T>中存放的只能是T类型或是C或是D类型,里面存放的类型都可以向上cast到T,所以从List<? extends T>中取东西编译器能够正确处理,只要映射到T就可以了(T是他们的父类)!往List<? extends T>放东西就不一样的,原来里面放的是T/C还是D都被擦除成T,所以编译器不知道原来到底存放的是什么类型,无法保证类型安全,所以这个操作被禁止! 
    List<? super T>中存放的是A/B或是T,往List<? super T>放T是允许的,因为T总是可以向上转换成A或是B,但是从里面取东西就有问题了,编译器还是不能够确定List里面放的是什么类型,可能是A也可能是B。 

    具体化: 
    当一个类型能够在运行时态被完整的表现,我们就称为其是可以具体化的,举一个例子: 
    List<Integer>就不是一个可以具体化的类型,因为它在运行时态被擦除成List!所以当处理一些需要运行时检测类型的操作的时候(如instanceof)就要特别注意。 
    究竟哪些类型是可具体化的呢; 
        (1)、原始类型,如int; 
        (2)、非参数化的类和接口; 
        (3)、非限定的参数化类型,如List<?>, Map<?,?> 
        (4)、Raw类型,如 List,ArrayList等 
        (5)、所有由可具体化类型组成的数组,如Number[],List<?>[]等 
    哪些是不可具体化的类型呢 
        (1)、类型变量,如T; 
        (2),参数化类型,如List<Number>等 
        (3),有限定的参数化类型,如List<? extends Number>; 
    具体哪些操作需要注意区分是否是具体化类型: 
        (1)、实例检测和投射的时候; 
        Java中判断一个对象是否是一个类型的实例的时候,都使用instanceof整个运算符号,这个运算符号是需要利用运行时信息的,这样就要求其右边的类型必须是可以具体化的类型,如:o instanceof Integer 但是不能是 o instanceof List<T>,因为List<T>不是具体化类型,T已经在编译的时候被擦除了,所以没有足够的信息判断前面的表达式是否成功。 
        同样的道理也适用于类型转换,(Integer)o是可以的,但是(List<T>)o就会报为检测错! 
        (2)、catch子句: 
        在捕获异常的时候,catch子句也是运用运行时信息来判定一个异常是不是其所关心的,这就要求写在catch子句后面的类型必须是可具体化的类型,同时有必须是Throwable的子类!同时Java也不允许创建一个参数化的Throwable子类。
Try{… }catch(FooException<T> e){…} //编译错误 Public class FooExceptioin<T> extends Exception //编译错误
        (3)、创建数组: 
        任何参与创建数组的类型必须是可具体化类型!所以下面的语句肯定是错误的: 
        T[] a = new T[size]; 其中T是参数类型。原因很简单,数组是需要运用运行时信息来检测类型的完整性,如: 
        Integer[] ints = new … 
        Number[] nums = ints //Integer[] 是Number[]的子类 
        Nums[0] = 8.9 //编译不会报错,但运行时异常,正说明这个问题 
        注意:这里说的是不可以创建,而不是不能声明,你还是可以声明,如在函数的参数中,如: 
         public static<T> void foo(T[] a){…} 
        (4)、变长参数: 
        变长参数是JDK1.5新引入的功能,它允许一个方法接受不确定数目的参数,并最终将这些参数放入一个数组。这里既然是数组,同样需要满足上面3)所提到的约束,如:
public static<E> void addAll(List<E> list, E... arr){ for(E itor : arr){ list.add(itor); } } public static<E> List<E> foo(E a){ List<E> list = new ArrayList<E>(); addAll(list,a); // 编译器会给出警告 return list; }

IT168技术文档】 
    反射: 
    由于泛型的引入,导致原来的反射机制增加了很多新的接口,如:下图所示,图中黄色部分是新增加的接口(仅仅给出一部分),用于支持泛型,绿色部分是已有的部分;



    Java编译器在编译的时候,同时生成针对擦除类型和泛化类型的两套字节码,而在运行时,JVM只使用擦除类型的字节码,泛化类型的字节码仅仅被用于反射! 
    看一个简单的例子:
List<Integer> ints = new ArrayList<Integer>(); Class<?> cls = ints.getClass(); out.println(cls.getName()); TypeVariable<?>[]tv = cls.getTypeParameters(); for(TypeVariable<?> it : tv){ out.println(it.getName()); }
        输出: 
        java.util.ArrayList 
       
    这里首先是输出实例ints的类型名字,然后输出ints的类型参数的名字,按照大多数人的设想,这个类型参数的名字应该是java.lang.Integer,然而输出的是“E”。原因就是上面的那句话,运行时使用的时擦除字节码,Integer都变成了Object,反射字节码就用于反射操作,原代码中的变量E,将被编译到反射字节码中,所以… 
    由于引入了泛型,原来的Class在泛型中都变成了Class<T>,其中T是可以具体化的类型;因为class永远代表一个具体化的类型,所以不允许使用非具体化类型来参数化Class!如:
Class<Integer> c1 = Integer.class; // OK Class<? Extends Number> c2= Integer.class
//OK Integer.class的类型是Class<Integer>而Class<Integer>刚好是Class<? Extends Number>的子类 Class<?> c3 = List<Integer>.class //错误,违背上面的原则 Class<?> c4 = Integer.class; //OK
    如果一个表达式Exp其返回的结果类型是T,那么Exp.class的类型就是Class<? Extends |T|>,其中|T|是T的擦除类型!
List<Integer> ints = new ArrayList<Integer>(); Class<? extends List> k = ints.getClass(); assert k == ArrayList.class;
    总结: 
    本文主要列举了在使用Java泛型时应该注意的问题。Java泛型是Java5的一个重要特性,它和自动装箱、变长参数等新特性一起,提升了Java代码的健壮性和易用性,但SUN本身过分强调向前的兼容性,也引入了不少问题和麻烦。
0
相关文章