泛型-擦除的神秘之处

        当你开始更深入的专研泛型时,会发现有大量的东西看起来是没有意义的,例如尽管可以声明ArrayList.class,但是不能声明ArrayList<String>.class

ArrayList<String>().getClass() == ArrayList<Integer>().getClass() //true

        ArrayList<String>和ArrayList<Integer>很容易被认为是不同的类型。不同的类型在行为方式上不同,如果尝试将一个Integer放入ArrayList<String>,所得到的行为(将失败)与将一个Integer放入ArrayList<Integer>(将成功)所得到的行为完全不同。但是上面的程序会认为它们是相同的类型。

class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION,MOMENTUM> {}

class LostInformation {
    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> particle = 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(particle.getClass().getTypeParameters()));
    }
}


执行结果:

[E]
[K, V]
[Q]
[POSITION, MOMENTUM]

        根据jdk文档描述,Class.getTypeParameters()将"返回一个TypeVariable对象数组,表示有泛型声明所声明的类型参数......",这好像是在暗示你可能发现参数类型的信息,但是,正如你从输出中所看到的,你能够发现的只是用作参数占位符的标识符,这并非有用的信息。

        因此,残酷的现实是:

        在泛型代码内部,无法获取任何有关泛型参数的信息

        因此,你可以知道诸如类型参数标识符和泛型类型边界这类的信息——你却无法知道用来创建某个特定实例的实际的类型参数。如果你曾经是C++程序员,那么这个事实肯定让你觉得很沮丧,在使用Java泛型工作时它是必须处理的最基本的问题。

        Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都会被擦除了,你唯一知道的就是你在使用一个对象。因此 List<String>和List<Integer>在运行时事实上是相同的类型。这两种形式都被擦除成它们的"原生"类型,即List。理解擦除以及应该如何处理它,是你学习Java泛型时面临的最大障碍,这也是我们在本节将要讨论的内容。

       

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

class Manipulator<T> {
    private T obj;
    public Manipulator(T x){
        obj = x;
    }
    //Error: cannot find symbol method f()
    public void manipulator(){
    obj.f();
    }

}

class  Manipulation {
    public static void main(String[] args){
    HasF hasF = new HasF();
    Manipulator<HasF> manipulator = new Manipulator<>(hasF);
    manipulator.manipulator();
    }

}

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

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

}

        边界<T extends HasF>声明T 必须具有类型HasF或者从HasF导出的类型。如果情况确实如此,那么就可以安全地在obj上调用f()了。

        我们说泛型类型参数将擦除到它的第一个边界(它可能有多个边界,稍候你就会看到),我们还提到了类型参数的擦除,编译器实际上会把类型参数替换为它的擦除,就像上面的示例一样,T擦除到了HasF,就好像在类中声明用HasF替换了T一样。

        你可能已经正确的观察到,在Manipulation2.java中,泛型没有贡献任何好处。只需很容易地自己去执行擦除,就可以创建出没有泛型的类:

class Manipulator3 {
    private HasF obj;
    public Manipulator3(HasF x){
        obj = x;
    }
    public void manipilate(){
        obj.f();
    }
}

        这提出了很重要的一点:只有当你希望使用的类型比某个具体类型(以及它的所有子类型)更加"泛化"时——也就是说,当你希望代码能够跨多个类工作时,使用泛型才有所帮助。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换要更复杂。但是,不能因此而认为<T extends HasF>形式的任何东西而都是有缺陷的。例如,如果某个类有一个返回T的方法,那么泛型就有所帮助,因为他们之后将返回确切的类型:

class ReturnGenericType<T extends HasF> {
    private T obj;

    public ReturnGenericType(T x) {
        obj = x;
    }

    public T get() {
        return obj;
    }
}

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

        我们将在本章稍后介绍有关边界的更多细节。

迁移的兼容性

        为了减少潜在的关于擦除的混淆,你必须清楚地认识到这不是一个语言特性。它是Java的泛型实现中的一种折中,因为泛型不是Java语言出现时就有的组成部分,所以这种折中是必需的。这种折中会使你痛苦,因此你需要习惯它,并了解为什么它为什么会这样。

        如果泛型在Java1.0就已经是其一部分了,那么这个特性将不会使用擦除来实现——它将使用具体化,使类型参数保持为第一类实体,因此你就能够在类型参数上执行基于类型的语言操作和反射操作。你将在本章稍后看到,擦除减少了泛型的返回性。泛型在Java中任旧是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。

        在基于擦除的实现中,泛型类型被当作第二类型处理,即不能再某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型的上界。例如,诸如List<T> 这样的类型注解将被擦除为List,而普通的类型变量在未指定边界的情况下将被擦除为Object。

        擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为”迁移兼容性“。在理想的情况下,当所有事物都可以同时被泛化时,我们就可以专注于此,在现实中,即使程序员只编写泛型代码,他们也必须处理在JavaSE5之前编写的非泛型类库。那些类库的作者可能从没有想过要泛化他们的代码,或者可能刚刚开始接触泛型。

        因此Java泛型不仅必须支持向后兼容性,即现有的代码和类任旧合法,并且继续保持其之前的含义;而且还要支持迁移的兼容性,使得类库按照它自己的步调变为泛型的,并且当某个类库变为泛型时,不会破坏依赖于它的代码和应用程序。在决定这就是目标之后,Java设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。通过允许非泛型代码与泛型代码共存,擦除使得这种向着泛型的迁移成为可能。   

      为了实现迁移的兼容性,每个类库和应用程序都必须与其他所有的部分是否使用了泛型无关。这样,它们必须不具备探测其他类库是否使用了泛型的能力。因此,某个特定的类库使用了泛型这样的证据必须被“擦除”。

        如果没有某种类型的迁移类型,所有已经构建了很长时间的类库就需要与希望迁移到Java泛型上的开发者说再见了。但是,类库是编程语言无可争议的一部分,它们对生产效率会产生最重要的影响,因此这不是一种可以接受的代价。擦除是否是最佳的或者唯一的迁移途径,还需要时间来证明。

擦除的问题

        擦除的代价是显著的。泛型不能用于显式地引用运行时的类型的操作之中,例如转型、instanceof操作和new表达式。因为所有关于参数的类型信息都丢失了,无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。因此,如果你编写了下面这样的代码段:

class Foo<T> {
    T var;
}

Foo<Cat> f = new Foo<Cat>();

        那么,看起来 当你在创建Foo的实例时,class Foo 中的代码应该知道现在工作于Cat之上,而泛型语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换。但是事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,他只是一个Object。”

        当你希望将类型参数不要仅仅当作Object处理时,就需要付出额外努力来管理边界,并且与在C++、Ada和Eiffel这样的语言中获取参数化类型相比,你需要付出多得多的努力来获得少的多的回报。这并不是说,对于大多数的编程问题而言,这些语言通常都会比Java更得心应手;这只是说,它们的参数化类型机制比Java的更灵活,更强大。

边界处的动作

        正因为有了擦除,我发现泛型最令人困惑的方面源自这样一个事实,既可以表示没有任何意义的事物。例如:        

        即使编译器无法知道有关create()中的T的任何信息,但是它依旧可以在编译期确保放置在result中的对象具有T类型,使其适合ArrayList<T>。因此,即使擦除在方法或类内部移除了有关实际类型的信息,编译器任旧可以确保在方法或类中使用的类型的内部一致性。

        因为擦除在方法体中移除了类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。

        代码一:

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

set() 和 get() 方法将直接存储和产生值,而转型是在调用get()的时候接受检查的。

          现在将泛型合并到上面的代码中:

          代码二:

class GenericHolder<T>{
    private T obj;

    public T getObj() {
        return obj;
    }

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

    public static void main(String[] args) {
        GenericHolder<String> genericHolder = new GenericHolder<String>();
        genericHolder.setObj("item");
        String s = genericHolder.getObj();
    }
}

        从get() 返回之后的转型消失了,但是我们还知道传递给set()的值在编译期会接受检查。代码一和代码二所产生的字节码是相同的。对进入set()的类型进行类型检查是不需要的,因为这是由编译器执行的。而对从get()返回的值进行转型任旧是需要的,但这与你自己必须执行的操作是一样的——此处它将由编译器自动插入,因此你写入(和读取)的代码的噪声将更小。

        由于所产生的get()和set()的字节码相同,所以在泛型中的所有动作都发生在边界处——对传递进来的值进行额外的编译期检查,并插入对传递出去的转型。这有助于澄清对擦除的混淆,记住,“边界就是发生动作的地方。”

        

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值