参考文章:1.《JAVA编程思想 第4版》 2.java泛型详解http://luckykapok918.blog.163.com/blog/static/2058650432012102341548827/
Java泛型(generics)是JDK
5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(
类型擦除
正确理解泛型概念的首要前提是理解类型擦除(type erasure)。 Java中的泛型基本上都是在编译器这个层次来实现的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。在泛型代码内部,无法获得任何有关参数内型的信息。这是一个很残酷的现实。如在代码中定义的List<很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:
-
泛型类并没有自己独有的Class类对象。
比如并不存在List<String>. class或是List<Integer>.class, 而只有List.class。由于这一个擦除的特性,不难理解下面的代码为何错误。 -
public class Erased<T>{ private final int SIZE=100; public static void f(Object arg){ T var=new T();//Error T[] array=new T[SIZE];//Error T[] array=(T) new Object[SIZE];//Unchecked warning } }
-
静态变量是被泛型类的所有实例所共享的。
对于声明为MyClass<T>的类, 访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象, 都是共享一个静态变量。 -
泛型的类型参数不能用在Java异常处理的catch语句中。
因为异常处理是由JVM在运行时刻来进行的。 由于类型信息被擦除, JVM是无法区分两个异常类型MyException< String>和MyException<Integer>的。 对于JVM来说,它们都是 MyException类型的。 也就无法执行与异常对应的catch语句。 - 任何基本类型都不能作为类型参数。JAVA的基本数据类型有byte,short,int,long,float,double,char,boolean。不能创建ArrayList<int>之类的东西。如果要解决的话,JAVA自动包装机制是解决之道,比如可以创建一个ArrayList<Integer>,然后将Int用于容器即可。
- 一个类不能实现同一个泛型接口的两种变体。这是因为由于擦除,两种变体会变成完全一样的接口。例如下面代码就会造成冲突。
-
interface Payable<T>{} class Employee implements Payable<Employee>{} class Hourly extends Employee implements Payable<Hourly>{}
Hourly将无法编译,因为擦除会将Payable<Employee>和Payable<Hourly>简化成相同的类Payable。这样上面的代码就是重复的两次实现相同的接口。 - 方法重载时候注意:当被擦除的参数不能产生唯一的参数列表时,必须提供明显有区别的方法名。例如,下面的代码擦除以后,重载方法会产生相同的类型签名,这样编译器就会报错。
-
public class list2<W,T>{ void f(List<T> v){} void f(List<W> v){} }
类型擦除的基本过程也比较简单,
- class MyString implements Comparable<String> {
- public int compareTo(String str) {
- return 0;
- }
- }
实例分析
问题1:类型参数向上转型。
假设Apple类是Fruit类的子类,很多人会常常写这样一句代码:List<Fruit> flist=new ArrayList<Apple>();这句代码是错误的!!!
有人可能认为这不是向上转型么?事实上这不是向上转型。Apple的List不是Fruit的List,因为Fruit的List将会持有各种类型的Fruit,比如Orange,Banana。但是Apple的List持有的只是Apple和它的子类。因此,这种代码完全就是错误的。
我们需要注意,List<Fruit> flist=new ArrayList<Fruit>();flist.add(new Apple());这样的代码才是大家所谓的向上转型。
问题2:泛型对象调用方法。
看看下面的代码:
class HasF{ public void f(); } class Manipulator<T>{ private T tt; public Manipulator(T tt){this.tt=tt;} public void mani(){tt.f();}//Error public T get(){return tt;} } public class Erase(){ public static void main(String []args){ HasF hf=new HasF(); Manipulator<HasF> mad=new Manipulator<HasF>(hf); mad.mani(); } }这段代码为什么会错呢,因为JAVA并不知道泛型的参数类型,对于T,它可能是各种类型,如String,Integet。不是这些类都有f()方法,这么调用f()方法当然是错误的。
问题3:创建类型实例。
对于以下代码:
public class Erased<T>{ private final int SIZE=100; public static void f(Object arg){ T var=new T();//Error T[] array=new T[SIZE];//Error } }
创建new T()或者new T[]无法实现,原因就是因为擦除,同时也因为编译器无法验证T具有默认(无参)构造器。
泛型问题的解决---边界
以上实例中出现的问题就是因为擦除以后,编译器无法识别类型参数,因为解决的方法就是为类型参数指定一个大概的范围,这个就是边界。
在使用泛型类的时候,既可以指定一个具体的类型,如List<
如上所示,试图对一个带通配符的泛型类进行操作的时候,
在有了边界以后我们再来看上述问题的解决之道。
问题1:我们只需将List<Fruit> filist=new ArrayList<Apple>();换成List<? entends Fruit> filist=new ArrayList<Apple>();即可。此时flist是”具有任何从Fruit继承的类型的列表“。
问题2:将代码改成:
class HasF{ public void f(); } class Manipulator<T extends HasF>{ private T tt; public Manipulator(T tt){this.tt=tt;} public void mani(){tt.f();} public T get(){return tt;} } public class Erase(){ public static void main(String []args){ HasF hf=new HasF(); Manipulator<HasF> mad=new Manipulator<HasF>(hf); mad.mani(); } }
问题3:将代码改成:
class ClassFactory<T>{ T x; public ClassFactory(Class<T> kind){ try{ x-kind.newInstance(); }catch(Exception e){ new RuntimeException(e); } } }
类型系统
在Java中,引入泛型之后的类型系统增加了两个维度:
- 相同类型参数的泛型类的关系取决于泛型类自身的继承体系结构。
即List<String>是Collection< String> 的子类型,List<String> 可以替换Collection<String>。 这种情况也适用于带有上下界的类型声明。 - 当泛型类的类型声明中使用了通配符的时候, 其子类型可以在两个维度上分别展开。如对Collection<
? extends Number>来说, 其子类型可以在Collection这个维度上展开, 即List<? extends Number>和Set<? extends Number>等;也可以在Number这个层次上展开, 即Collection<Double>和 Collection<Integer>等。如此循环下去, ArrayList<Long>和 HashSet<Double> 等也都算是Collection<? extends Number>的子类型。 - 如果泛型类中包含多个类型参数,
则对于每个类型参数分别应用上面的规则。
开发自己的泛型类和泛型方法
泛型类与一般的Java类基本相同,只是在类和接口定义上多出来
class ClassTest<X extends Number, Y, Z> { private X x; private static Y y; //编译错误,不能用在静态变量中 public X getFirst() { //正确用法 return x; } public void wrong() { Z z = new Z(); //编译错误,不能创建对象 ,因为泛型类并没有自己独立的Class类对象 } }2.使用泛型方法
要定义泛型方法,只需要将泛型参数列表置于返回值之前,如同下面:
public <T> T f(T x){ return x; }这里面说明一点:当使用泛型类时,必须在创建对象的时候指定参数类型的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型,这叫做"类型参数推断“。我们可以像调用普通方法一样调用f()。泛型方法它独立于类,对于这个方法的使用,一般只要可能的话,尽量使用泛型方法。
3.容器类的使用
BruceEckel说过:"使用泛型类关于类型机制最吸引人的地方在于容器类的使用。”如下代码:
public class ListMaker<T>{ private Class<T> kind; T[] create(){ return (T[])Array.newInstance(kind, 100); } }由于擦除,kind将被存储为Class,没有任何参数,因此,使用它创建数组时候,Array.newInstance()并没有拥有kind所蕴含的类型信息,这不会产生具体的结果,所以要转型,这会产生一条警告,但是如果创建的是一个容器,编译器将不会给出任何警告,如下:
public class ListMaker<T>{ List<T> create(){ return new ArrayList<T>(); } }
尽管在create()内部的new ArrayList<T>中的<T>被除去了,看起来没有任何意义,但是如何将表达式改成new ArrayList();编译器就会给出警告。事实上,尽管编译器无法知道有关create()中的T的任何信息,但是它仍旧可以确保返回的对象具有T类型,使其适合ArrayList<T>,因此,即使擦除在方法或者类内部移除了有关实际类型的信息,但是编译器仍旧可以确保在方法或类中使用的类型的内部一致性。
由于容器在泛型机制上的优势,将在后面的部分对容器进行实例操作。
最佳实践
在使用泛型的时候可以遵循一些基本的原则,-
在代码中避免泛型类和原始类型的混用。比如List<
String>和List不应该共同使用。 这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时, 也尽可能的隔离相关的代码。 -
在使用带通配符的泛型类的时候,
需要明确通配符所代表的一组类型的概念 。由于具体的类型是未知的,很多操作是不允许的。 - 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List<String>[10]这样的。
这限制了数组的使用能力,而且会带来很多费解的问题。因此, 当需要类似数组的功能时候,使用集合类即可。 - 不要忽视编译器给出的警告信息。