泛型擦除

擦除的现象

当开始深刻研究泛型的时,会发现其实有些东西是没有意义的。例如,咱们能够声明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泛型时候的最大阻碍。ui

所以,能够得到类型参数标识符和泛型类型边界这样的信息,可是却没法知道用来建立某个特定实例的实际的类型参数。this

擦除的边界

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关键字。并因为有了边界,下面的代码能够编译了。spa

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替换回会HasFcode

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;
    }
}

因此,必须查看全部的代码。并肯定它是否"足够复杂"到必须使用泛型的程度。ip