美文网首页
泛型擦除

泛型擦除

作者: 呆呆李宇杰 | 来源:发表于2017-09-11 22:59 被阅读46次

    擦除的现象

    当开始深入研究泛型的时,会发现其实有些东西是没有意义的。例如,我们可以声明ArrayList.class,但是却无法声明ArrayList<Integer>.class
    这是因为泛型的擦除机制造成的,考虑以下的情况。

    public class ErasedTypeEquivalence {
        public static void main(String[] args) {
            Class c1 = new ArrayList<String>().getClass();
            Class c2 = new ArrayList<Integer>().getClass();
            System.out.println("(c1 == c2) = " + (c1 == c2));
        }
    }
    

    以上代码中,表明ArrayList<String>ArrayList<Integer>是同一类型。不同的类型在行为方面肯定不同。例如,如果试着将一个Integer类型放入ArrayList<String>,所得的行为和Integer类型放入ArrayList<Integer>完全不同,但是它们仍然是同一类型。
    以下的代码是对这个问题的一个补充。

    class Frob {
    }
    
    class Fnorkle {
    }
    
    class Quark<Q> {
    }
    
    class Particle<POSITION, MOMENTUM> {
    }
    
    public class LostInfomation {
        public static void main(String[] args) {
            List<Frob> list = new ArrayList<>();
            Map<Frob,Fnorkle> map = new HashMap<>();
            Quark<Fnorkle> quark = new Quark<>();
            Particle<Long,Double> p = new Particle<>();
            System.out.println(Arrays.toString(
                    list.getClass().getTypeParameters()
            ));
            System.out.println(Arrays.toString(
                    map.getClass().getTypeParameters()
            ));
            System.out.println(Arrays.toString(
                    quark.getClass().getTypeParameters()
            ));
            System.out.println(Arrays.toString(
                    p.getClass().getTypeParameters()
            ));
        }
    }
    
    // Outputs
    [E]
    [K, V]
    [Q]
    [POSITION, MOMENTUM]
    

    Class.getTypeParameters是返回一个TypeVariable对象数组,表示泛型声明所声明的类型参数。但是上例的输出也表明了,这个方法获得的只是做参数占位符的标识符。

    擦除的概念

    在泛型代码内部,无法获得任何有关泛型参数类型的信息。
    Java的泛型是使用擦除来实现的,这就意味着在使用泛型的时候,任何具体的类型信息都会被擦除。写代码时唯一知道就是在使用一个对象。因此,List<String>List<Integer>在运行时事实上是相同的类型。这两种形式都会被擦除成它们的"原生"类型,即List。理解擦除以及应该如何处理它,是在学习Java泛型时候的最大阻碍。

    因此,可以获得类型参数标识符和泛型类型边界这样的信息,但是却无法知道用来创建某个特定实例的实际的类型参数。

    擦除的边界

    class HasF{
        public void f(){
            System.out.println("HasF.f()");
        }
    }
    
    class Manipulator<T> {
        private T obj;
    
        public Manipulator(T obj) {
            this.obj = obj;
        }
    
        public void manipulate(){
            //obj.f() compile error
        }
    }
    
    public class Manipulation {
        public static void main(String[] args) {
            HasF hf = new HasF();
            Manipulator<HasF> manipulator = new Manipulator<>(hf);
            manipulator.manipulate();
        }
    }
    

    由于有了擦除机制,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上,为了调用f(),我们必须协助泛型类,给定泛型类的边界,以便告知编译器只能遵循这个边界的类型。这里重用了extends关键字。并由于有了边界,下面的代码可以编译了。

    class Manipulator2<T extends HasF> {
        private T obj;
    
        public Manipulator2(T obj) {
            this.obj = obj;
        }
    
        public void manipulate(){
            obj.f();
        }
    }
    

    上面的代码中,边界<T extentds HasF>声明T必须具有类型HasF或者从HasF导出来的类型,因为这个约束,所以可以安全地在obj上调用f了。
    这里说泛型的类型参数将擦除到它的第一边界(泛型可能有多个边界)。这里提到了类型参数的擦除,编译器实际上会把类型参数替换成它的擦除,就像上面的示例那样,T擦除到了HasF,就像在类的声明中用HasF替换成T一样。
    如同上文所说,我们可以不使用泛型,直接将T替换回会HasF

    class Manipulator3 {
        private HasF obj;
    
        public Manipulator2(HasF obj) {
            this.obj = obj;
        }
    
        public void manipulate(){
            obj.f();
        }
    }
    

    上面的代码也可以像Manipulator2中那样正常工作。但是这并不意味着带边界的泛型是毫无意义的。
    只有当希望使用的类型参数比某个具体类型(以及它的所有子类型)更加"泛化"时。也就是说,当希望代码能跨多个类工作的时候,使用泛型才有帮助。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换要更为复杂。。但是也不能因为觉得<T extends HasF>的任何东西都是有缺陷的。
    例如,假设某个类有返回T的方法,那么泛型在这里就是有用处的,因为泛型可以返回确切的类型。例子如下。

    class ReturnGenericType<T extends HasF> {
        private T obj;
    
        public ReturnGenericType(T obj) {
            this.obj = obj;
        }
    
        public T getObj() {
            return obj;
        }
    }
    

    所以,必须查看所有的代码。并确定它是否"足够复杂"到必须使用泛型的程度。

    相关文章

      网友评论

          本文标题:泛型擦除

          本文链接:https://www.haomeiwen.com/subject/htdqsxtx.html