天天看点

泛型总结

泛型总结

 1介绍

  Java泛型编程是JDK1.5版本后引入的。泛型让编程人员能够使用类型抽象,通常用于集合里面。下面是一个不用泛型例子:

  注意第三行的代码,让人很不爽,因为程序员肯定知道自己存储在List里面的对象类型是Integer,但是在返回的时候,列表中元素必须强制转换,这是为什么呢?原因在于,编译器只能保证迭代器的next()方法返回的是Object类型的对象,为保证Interger变量的类型安全,必须强制转换。

这种转换不仅显得混乱,而且导致转换异常ClassCastException,运行时异常往往让人难以检测到。保证列表中的元素为一个特定的数据类型,这样就可以取消类型转换,减少发生错误的机会,这也是泛型设计的初衷。下面给出一个泛型的例子:

  在第一行代码中指定List中存储的对象类型是Integer,这样在获取列表中的对象时,不必强制类型转换了。

 2 定义简单的泛型

  下面是一个引用java.util包中的借口List和Iterator的定义,其中用到了泛型技术。

  这跟原生态类型没有什么区别,只是在接口后面加入了一个尖括号,尖括号里面是一个类型参数(定义时就是一个格式化的类型参数,在调用时会使用一个具体的类型来替换该类型)。

  也许可以这样认为,List<Integer>表示List中的类型参数E会被替换成Integer。

  类型擦除指的是通过类型参数的合并,将泛型类型实例关联到同一个字节码上,编译器只为泛型类型生成一个字节码,并将其关联到这上面,因此泛型类型中的静态变量是所有实例共享的。此外需要注意,一个static方法,无法访问泛型类的类型参数,因为类还没有实例化,所以若static方法需要使用泛型,必须使其成为泛型方法。

  类型擦除的关键在于从泛型类中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。使用泛型时,任何具体的类型都被擦除,唯一知道的是你在使用一个对象。比如List<String>和List<Integer>是相同的类型。它们都被擦除成原始类型,即List。

  因为编译的时候会有类型擦除,所以不能通过一个泛型类的实例来区分方法,如下面的例子编译会出错,因为类型擦除后,两个方法都是List类型的参数。因此不能根据泛型类的类型来区分方法。

  那么有问题了,既然编译时会在方法和类中擦除实际类型的信息,那么返回对象时又是如何知道具体类型的呢?如List<String>编译后会擦除String信息,那么在运行时通过迭代器返回List中的对象时,又是如何知道List中存储的是String类型的对象的呢?

擦除在方法中的类型信息,所以在运行时的问题是边界:即对象进入和离开方法的地点,这正是编译器在编译期执行类型检查并出入转换代码的地点。泛型中的所有动作都发生在边界处:对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转换。

3 泛型和子类型

  为了彻底理解泛型,看个例子,(Apple为Fruit的子类)

  这里第一行显然是对的,但是第2行是否对呢?我们知道Fruit fruit = new Apple(),这样肯定是对的,即苹果肯定是水果,但是第2行在编译的时候会出错。这会让人比较纳闷的是一个苹果是水果,为什么一箱苹果就不是一箱水果了呢?可以这样考虑,我们假定第2行代码没有问题,那么我们可以使用语句fruits.add(new Strawberry())(Strawberry为Fruit的子类)在fruits中加入草莓了,但是这样的话,一个List中装入了各种不同类型的子类水果,这显然是不可以的,因为我们在取出List中的水果对象时,就分不清楚到底该转型为苹果还是草莓了。(因为擦除后,苹果和草莓都变成了Fruit类型,具体返回对象的时候,不能区分哪个是苹果的对象,哪个是草莓的对象。)

  通常来说,如果Foo是Bar的子类型,G是一种带泛型的类型,则G<Foo>不是G<Bar>的子类型。这是容易混淆的地方。

4 通配符

4.1 通配符?

  先看一个打印集合所有元素的代码。

  很容易发现,使用泛型的版本只接受类型为Object类型的集合,如ArrayList<Object>();如果是ArrayList<String>,则会出错。因为前面说过,Collection<Object>并不是所有集合的超类。而老版本可以打印任意类型的集合,那么改造新版本以便能接受所有类型的集合呢?这个问题可以通过通配符解决。修改后的代码如下:

   这里使用了通配符?指定可以使用任何类型的集合作为参数。读取元素使用了Objectect类型表示,这是安全的,因为所有的类都是Object的子类。

 又有另一个问题,如下面代码所示,如果试图往使用通配符?的集合中加入对象,会出错。需要注意,不管加入什么类型的对象都会出错。这是因为统配符表示该集合存储的元素类型未知,可以是任意的。

  另一方面,我们可以从List<?>lists中获取值,如for(Object obj:lists),这是合法的,因为可以肯定存储类型一定是Object的子类型,所以可以用Object类型来获取。

4.2 边界通配符

1)?extends通配符

  假定有一个画图的应用,可以活各种形状。为了在程序里面表示,定义如下的类层次。

  有一个问题,如果我们希望List<? extends Shapes> shapes中加入一个矩形对象,如下所示:

shapes.add(0, new Rectangle());//编译出错。

  原因是:我们只知道shapes中的元素时Shapes类型的子类型。具体是什么子类不知道。所以不能加入任何类型的对象。不过我们在取出对象时,可以用Shape类型来取值,因为虽然不知道列表中元素是什么类型,但是它一定是Shape类的子类型。

2)?super通配符

  这里还有一种边界通配符?super。如:

  这里cicleSupers列表中元素是Cicle的超类,因此,我们可以往其中加入Cicle对象或者是Cicle子类的对象,但是不能加入Shape对象。这里的原因在于列表cicleSupers存储的是Cicle的超类,但具体类型未知。

3)边界通配符总结

<!--[if !supportLists]-->l        <!--[endif]-->如果你想从一个数据类型里获取数据,使用 ? extends 通配符

<!--[if !supportLists]-->l        <!--[endif]-->如果你想把对象写入一个数据结构里,使用 ? super 通配符

<!--[if !supportLists]-->l        <!--[endif]-->如果你既想存,又想取,那就别用通配符。

5 泛型方法

  考虑实现一个方法,该方法拷贝一个数组中的所有对象到集合中。下面是初始的版本。

  可以看到显然会出现错误,原因在于之前讲过,因为集合c中的类型未知,所以不能往其中加入任何的对象(当然,null除外)。解决该问题的好方法是使用泛型方法。

  泛型方法的格式,类型参数<T>要放在函数返回值之前,然后参数和返回值中就可以使用泛型参数了,具体一些调用方法的实例如下:

  注意到我们调用该方法时并不需要传递类型参数,系统会自动判断参数并调用合适的方法。当然在某些情况下需要制定传递类型参数,比如当存在与泛型方法相同的方法的时候(方法参数不一样),如下面的例子:

  当不指定类型参数时,调用的是普通的方法,如果指定了类型参数,则调用泛型方法。可这样理解,因为泛型方法编译后类型擦除,如果不指定类型参数,则泛型方法此时相当于是public void go(Object t)。而普通的方法接收参数为String类型,因此String类型的实参调用函数,肯定会调用形参为String的普通方法了。如果是以Object类型的实参调用函数,肯定会调用泛型方法。

6 需要注意的地方

1)方法重载

  在JAVA里面方法重载是不能通过返回值类型来区分的,比如代码一中一个类中定义两个如下的方法是不容许的。但是当参数为泛型类型时,确实可以的,如代码二,虽然形参经过类型擦除后都以List类型,但是返回类型不同,这是可以的。

2)泛型类型是被所有调用共享的

  所有泛型类型的实例都共享同一个运行时类,类型参数信息会在编译时被擦除。因此考虑如下代码,虽然ArrayList<String>和ArrayList<Integer>类型参数不同,但是它们都共享ArrayList类,所以结果会是true。

3)instanceof

  不能对确切的泛型类型使用instanceof()操作,下面代码是违法的。

4)泛型数组问题

  不能创建一个确切反省类型的数组,否则出错。

  因此只能创建带通配符的泛型数组,如下面例子所示,这回可以通过编译,但倒数第二行代码必须显示的转型才行,即便如此,最后还是会抛出类型转换异常,因为存储在lsa中的List<Integer>类型的对象,而不是List<String>类型。最后一行代码是正确的,类型匹配,不会抛出异常。

当神已无能为力,那便是魔渡众生