1介绍
Java
泛型编程是JDK1.5版本后引入的。泛型让编程人员能够使用类型抽象,通常用于集合里面。下面是一个不用泛型例子:
1 List myIntList=new LinkedList(); 3 myIntList.add(newInteger(0)); 5 Integer x=(Integer)myIntList.iterator().next();
这种转换不仅显得混乱,而且导致转换异常ClassCastException,运行时异常往往让人难以检测到。保证列表中的元素为一个特定的数据类型,这样就可以取消类型转换,减少发生错误的机会,这也是泛型设计的初衷。下面给出一个泛型的例子:
1 List<Integer> myIntList=newLinkedList<Integer>; 3 myIntList.add(newInteger(0)); 5 Integer x=myIntList.iterator().next();
2 定义简单的泛型
下面是一个引用java.util包中的借口List和Iterator的定义,其中用到了泛型技术。
1 public interface List<E>{ 3 void add(E,x); 5 Iterator<E> iterator(); 7 } 9 public interface Iterator<E>{ 11 E next(); 13 boolean hasNext(); 15 }
也许可以这样认为,List<Integer>表示List中的类型参数E会被替换成Integer。
1 public interface List<Integer>{ 2 void add(Integer x); 3 terator<Integer> iterator(); 4 }
类型擦除指的是通过类型参数的合并,将泛型类型实例关联到同一个字节码上,编译器只为泛型类型生成一个字节码,并将其关联到这上面,因此泛型类型中的静态变量是所有实例共享的。此外需要注意,一个static方法,无法访问泛型类的类型参数,因为类还没有实例化,所以若static方法需要使用泛型,必须使其成为泛型方法。
类型擦除的关键在于从泛型类中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。使用泛型时,任何具体的类型都被擦除,唯一知道的是你在使用一个对象。比如List<String>和List<Integer>是相同的类型。它们都被擦除成原始类型,即List。
因为编译的时候会有类型擦除,所以不能通过一个泛型类的实例来区分方法,如下面的例子编译会出错,因为类型擦除后,两个方法都是List类型的参数。因此不能根据泛型类的类型来区分方法。
1 /*会导致编译时出错*/ 2 public class Erasure{ 3 public void test(List<String> ls){ 4 System.out.println("String"); 5 } 6 public void test(List<Integer> ls){ 7 System.out.println("Integer"); 8 } 9 }
擦除在方法中的类型信息,所以在运行时的问题是边界:
即对象进入和离开方法的地点,这正是编译器在编译期执行类型检查并出入转换代码的地点。泛型中的所有动作都发生在边界处:对传递进来的值进行额外的编译器检查,并插入对传递出去的值的转换。
3 泛型和子类型
为了彻底理解泛型,看个例子,(Apple为Fruit的子类)
1 List<Apple> apples=new ArrayList<Apple>(); 2 List<Fruit> fruits=apples;
通常来说,如果Foo是Bar的子类型,G是一种带泛型的类型,则G<Foo>不是G<Bar>的子类型。这是容易混淆的地方。
4 通配符
4.1 通配符?
先看一个打印集合所有元素的代码。
1 //不使用泛型 2 void printCollection(Collection c){ 3 Iterator i=c.iterator(); 4 for(k=0;k<c.size();k++){ 5 System.out.println(i.next()); 6 } 7 }
1 //使用泛型 2 void printCollection(Collection c)<Object> c{ 3 for(Object e:c){ 4 System.out.println(e); 5 } 6 }
1 void printCollection(Collection c)<?> c{ 2 for(Object e:c){ 3 System.out.println(e); 4 } 5 }
这里使用了通配符?指定可以使用任何类型的集合作为参数。读取元素使用了Objectect类型表示,这是安全的,因为所有的类都是Object的子类。
又有另一个问题,如下面代码所示,如果试图往使用通配符?的集合中加入对象,会出错。需要注意,不管加入什么类型的对象都会出错。这是因为统配符表示该集合存储的元素类型未知,可以是任意的。
1 Collection<?> c=new ArrayList<String>(); 2 c.add(newObject());//编译出错,不管加入什么对象都出错,除了null外。 3 c.add(null);//OK!
4.2 边界通配符
1)?extends通配符
假定有一个画图的应用,可以活各种形状。为了在程序里面表示,定义如下的类层次。
1 public abstract class Shape{ 2 public abstract void draw(Canvas c); 3 } 4 public class Circle extends Shape{ 5 private int x,y; 6 public void draw(Canvas c){...}; 7 } 8 public class Rectangle extends Shape{ 9 private int x,y,width,height; 10 public void draw(Canvas c){...} 11 } 12 13 //原始版本 14 public void drawAll(List<Shape> shapes){ 15 for(Shapes :shapes){ 16 s.draw(this); 17 } 18 } 19 20 //使用边界通配符的版本 21 public void drawALL(List<? extends Shape> shapes){ 22 for(Shapes :shapes){ 23 s.draw(this); 24 } 25 }
shapes.add(0, new Rectangle());//编译出错。
原因是:我们只知道shapes中的元素时Shapes类型的子类型。具体是什么子类不知道。所以不能加入任何类型的对象。不过我们在取出对象时,可以用Shape类型来取值,因为虽然不知道列表中元素是什么类型,但是它一定是Shape类的子类型。
2)?super通配符
这里还有一种边界通配符?super。如:
1 List<Shape> shapes=new ArrayList<Shape>(); 2 List<? super Cicle> cicleSupers=shapes; 3 circleSupers.add(new Cicle());//OK,subclss of Cicle also OK 4 cicleSupers.add(new Shape());//ERROR
3)边界通配符总结
<!--[if !supportLists]-->l <!--[endif]-->如果你想从一个数据类型里获取数据,使用 ? extends 通配符
<!--[if !supportLists]-->l <!--[endif]-->如果你想把对象写入一个数据结构里,使用 ? super 通配符
<!--[if !supportLists]-->l <!--[endif]-->如果你既想存,又想取,那就别用通配符。
5 泛型方法
考虑实现一个方法,该方法拷贝一个数组中的所有对象到集合中。下面是初始的版本。
1 static void fromArrayToCollection(Object[]a,Collection<?>c){ 2 for(Object o:a){ 3 c.add(o);//编译错误 4 } 5 }
1 static<T> void fromArrayToCollection(T[] a,Collection<T>c){ 2 for(T O:a){ 3 c.add(o);//OK 4 } 5 }
1 Object[] oa=new Object[100]; 2 Collection<Object>co=new ArrayList<Object>(); 3 fromArrayToCollection(oa,co);//T inferred to be Object 4 String[] sa=new String[100]; 5 Collection<String>cs=new ArrayList<String>(); 6 fromArrayToCollection(sa,cs);//T inferred to be String 7 fromArrayToCollection(sa,co);//T inferred to be Object 8 Integer[] ia=new Integer[100]; 9 Float[] fa=new Float[100]; 10 Number[] na=new Number[100]; 11 Collection<Number>cn=new ArrayList<Number>(); 12 fromArrayToCollection(ia,cn);//T inferred to be Number 13 fromArrayToCollection(fa,cn);//T inferred to be Number 14 fromArrayToCollection(na,cn);//T inferred to be Number 15 fromArrayToCollection(na,co);//T inferred to be Object 16 fromArrayToCollection(na,cs);//编译错误
1 public <T> void go(T t){ 2 System.out.println("generic function"); 3 } 4 public void go(String str){ 5 System.out.println("normal function"); 6 } 7 public static void main(String[] args){ 8 FuncGenric fg=new FuncGenric(); 9 fg.go("haha");//打印normal function 10 fg.<String>go("haha");//打印normal function,String是多余的,并且不报错。 11 fg.go(new Object());//打印generic function 12 fg.<Object>go(new Object());//打印generic function 13 }
6 需要注意的地方
1)方法重载
在JAVA里面方法重载是不能通过返回值类型来区分的,比如代码一中一个类中定义两个如下的方法是不容许的。但是当参数为泛型类型时,确实可以的,如代码二,虽然形参经过类型擦除后都以List类型,但是返回类型不同,这是可以的。
1 //代码一:编译出错 2 public class Erasure{ 3 public void test(int i){ 4 System.out.printlnl("String"); 5 } 6 public void test(int i){ 7 System.out.println("Integer"); 8 } 9 }
1 //代码二:正确 2 public class Erasure{ 3 public void test(List<String>ls){ 4 System.out.println("String"); 5 } 6 public void test(List<Integer>li){ 7 System.out.println("Integer"); 8 } 9 }
所有泛型类型的实例都共享同一个运行时类,类型参数信息会在编译时被擦除。因此考虑如下代码,虽然ArrayList<String>和ArrayList<Integer>类型参数不同,但是它们都共享ArrayList类,所以结果会是true。
1 List<Sting>l1=mew ArrayList<String>(); 2 List<Integer>l2=mew ArrayList<Integer>(); 3 System.out.println(l1.getClass()==l2.getClass());//True
3)instanceof
不能对确切的泛型类型使用instanceof()操作,下面代码是违法的。
1 Collection cs=new CollectionList<String>(); 2 if (cs instanceof Collection<String>){....}//编译出错,如果改成instanceof Collection<?>则正确。
不能创建一个确切反省类型的数组,否则出错。
1 List<String>[] lsa=new ArrayList<String>[10];//错误。因为如果可以这样,那么考虑如下代码,会导致运行时错误。 2 List<String>[] lsa=new ArrayList<String>[10];//实际上并不允许这样创建数组 3 Object 0=lsa; 4 Object[] oa=(Object[])o; 5 List<Integer>li=new ArrayList<Integer>(); 6 li.add(new Integer(3)); 7 oa[1]=li;//unsound,but passes run time store check 8 String s=lsa[1].get[0];//run-time error-classCastException
因此只能创建带通配符的泛型数组,如下面例子所示,这回可以通过编译,但倒数第二行代码必须显示的转型才行,即便如此,最后还是会抛出类型转换异常,因为存储在lsa中的List<Integer>类型的对象,而不是List<String>类型。最后一行代码是正确的,类型匹配,不会抛出异常。
1 List<?>[] lsa=new List<?>[10]; 2 Object 0=lsa; 3 Object[] oa=(Object[])o; 4 List<Integer>li=new ArrayList<Integer>(); 5 li.add(new Integer(3)); 6 oa[1]=li;/OK 7 String s=lsa[1].get(0);//run-time error,but cast is explicit 8 Integer it=(Integer)lsa[1].get(0);//OK