Java泛型——擦除

本章涉及到:

 

  • 擦除的效果
  • 擦出后编译器保证类型的正确性
  • 擦除的由来

需要了解的朋友可以看一下。

    这篇文章先对比了一下C++的泛型代码,能让你更清楚的感受到擦除的效果(请放心,只是简单的c++代码,不了解c++的同学也能看的懂)。

先看下C++的泛型:

#include

using namespace std;

template  class Manipulator{

        T obj;

        public:Manipulator(T x){ obj=x; }

        void manipulate(){ obj.f(); }

};

class HasF{

        public:void f(){ cout<<"HasF::f()"<< endl; }

};

int main(){

        HasF hf;

        Manipulator manipulator(hf); 

        manipulator.manipulate();

}

/*Output: HasF::f() */

    Manipulator类存储了一个类型T的对象,有意思的地方是manipulate()方法,这个方法中obj调用了方法f()。在C++中当你实例化这个模板时,C++编译器将进行检查,因此在Manipulator被实例化的这一刻,它看到HasF拥有一个方法f()。如果情况并非如此,就会得到一个编译期的错误,这样类型安全就得到保障了。       ——————以上摘自Thinking in Java (事实上这篇文章大部分内容都会摘自Thinking in Java   >.

接下来看一下Java中的代码:

publicclass HasF {

    public void f() {System.out.println("HasF.f()");}}

}

publicclass Manipulator {

    private T obj;

    public Manipulator(T obj) {

        this.obj = obj;

    }

    public void manipulate() {

        // obj.f();//此处报错

    }

}

public class Manipulation {

    public static void main(String[] args) {

        HasF hasF = new HasF();

        Manipulator manipulator = new Manipulation(hasF);

        manipulator.manipulate();

    }

}

可以看出Java和C++泛型代码很明显区别就是C++中的泛型有实际类型信息,而Java中的实际类型信息不管是编译时期还是在运行时期都被擦除了,这就是擦除的效果。由于有了擦除,Java编译器无法将obj调用f()这一需求映射到HasF拥有f()这一事实上。(事实上擦除是将泛型类型信息擦除到了它的第一个边界,默认不设置的边界是Object,你可以调用Object的方法,可以这样设置边界——,设置边界后就可以调用f()了。这篇文章不讲边界的概念)

通过上面的对比应该能明显的感受到Java擦除的存在了吧?在Java中,当你使用泛型时,任何具体的类型信息都会被擦除,你唯一知道的就是你在使用一个对象。

那这样问题就来了——实际的泛型类型信息被擦除了,Java是怎么保障类型信息的正确性呢?看下一段Java代码:

public class FilledListMaker {

    List create(T t, int n) {

        List result = new ArrayList();

        for (int i = 0; i < n; i++) {publicresult.add(t);}

    }

    public static void main(String[] args) {

        FilledListMaker stringMaker = new FilledListMaker();

        List list = stringMaker.create("hello", 4);

        System.out.println(list);    

        //System.out.println(Arrays.toString(list.getClass().getTypeParameters()));

    }

}

/*[hello,hello,hello,hello]/*

/* [E] /*

    在这段代码中看起来好像是拥有了String参数类型信息,但实际并非如此。最后一行代码的意思是输出一个TypeVariable对象数组,数组表示泛型所声明的类型信息。但是,正如你看见的,输出的只是用作参数占位符的标识符,并非有用的信息。 

    在这上面这段代码中编译器无法知道有关create()中T的任何信息,但是它仍旧可以在编译期确保你放置到result中的对象具有T类型,使其适合ArrayList。因此,即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。(记住编译器无法知道类型信息,但它可以保障类型信息安全)

   擦除在方法体中移除了类型信息,所以在运行时的问题就是边界,即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。(这段话中的边界和上方设置边界不是一个概念,别搞混了。)再看看下面的两段代码:

public class SimpleHolder {

    private Object obj;

    public Object getObj() { return obj; }

    public void setObj(Object obj) { this.obj = obj; }    

    public static void main(String[] args) {

        SimpleHolder simpleHolder = new SimpleHolder();

        simpleHolder.setObj("Item");

        String s = (String) simpleHolder.getObj();

    }

}

public class GenericHolder{

    private T obj;

    public T getObj() { return obj; }

    public void setObj(T obj) { this.obj = obj; }

    public static void main(String[] args) {

        GenericHolder genericHolder = new GenericHolder();

        genericHolder.setObj("Item");

        String s = genericHolder.getObj();

    }

}

如果用Java -c SimpleHolder反编译这个类,就可以得到下面的内容(直接上图了实在是懒得写了):

Java泛型——擦除_第1张图片

SimpleHolder

可以看出set()和get()方法直接存储和产生值,而转型是在调用get()的时候接受检查的。接下来看一下GenericHolder:

Java泛型——擦除_第2张图片

GenericHolder

    可以看出GenericHolder产生的字节码操作和SimpleHolder产生的字节码操作是相同的。对于进入set()的类型进行检查是不需要的,因为这将由编译器执行。而对从get()返回的值进行转型仍旧是需要的,但这与你自己必须执行的操作是一样的——此处它将由编译器自动插入。

    那么问题又来了——为什么要有擦除?

    擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”。(因为泛型的概念是JavaSE5之后提出的,所以为了兼容以前的客户端和类库提出了擦除的概念,擦除也使得非泛化代码向着泛型的迁移成为了可能。)

    擦除存在的主要理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言中。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备好用泛型重写这些代码。擦除的代价也是比较明显的,实际类型的信息丢失,无法使用转型、instanceof操作、和new表达式。当你在编写泛型代码时,必须要提醒自己你只是看起来好像拥有了有关参数类型信息而已,实际上这个泛型类型的实际类型信息将被擦除到第一个边界,你唯一知道的就是你只是在操作一个对象。

    那么总结一下,擦除是擦掉了泛型类型的实际类型信息(擦除到了第一个边界),而编译器保障了类型的安全性,擦除的存在是为了实现迁移的兼容性。

    有需要改进的地方,望指出,谢谢。

 

你可能感兴趣的:(Java基础)