泛型全面分析和应用(一)
概述
Java SE5的重大变化之一就是提出了泛型的概念。泛型实现了参数化类型的概念,使代码可以应用于多种类型中。有很多原因促成了泛型的出现,而最主要的原因,就是为了构建容器类。我们很多时候在写代码的过程中,经常是遇到了同一个方法如果传入或者传出的参数类型过于单一,同样的功能的方法我们要写两次甚至是多次,原因就是由于参数类型不能适应其他类型的应用。那么为了解决这个问题,Java设计者也是煞费苦心。
- 一个泛型的小例子
- 泛型中使用注意的方法特性
- 擦除与其解决方法
- 泛型技术实例
一、一个泛型的小例子
在Java SE5之前,为了写一个通用的方法,也许得这样子。用Object来作为一个标准,反正大家都是Object的子类。
1 package Generic; 2 3 public class Holder { 4 5 private Object a; 6 public Holder(Object a){ 7 this.a = a; 8 } 9 public void set(Object a){ 10 this.a = a; 11 } 12 public Object get(){ 13 return a; 14 } 15 public static void main(String args[]){ 16 Holder h1 = new Holder(new String()); 17 h1.set("ss"); 18 Holder h2 = new Holder(new Integer(1)); 19 h2.set(“asdf”); 20 } 21 }
大家发现这个问题没有,既然Hodler h2的set方法的传入参数是一个Object,那么这个参数就可以是String,Integer,Double,h2其实是想保存的一个Integer类型的数据,但由于Object的泛用性,导致了一个String类型的数据保存了进去。这么来说,这就是一个潜在的隐患了。
在有些情况下,我们确实希望容器能够同时持有多种类型的对象。但是,通常来说,我们只会使用容器来存储一种类型的对象。泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且有编译器来保证类型的正确性。因此,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类的时候,再用实际的类型代替此类型参数。
1 package Generic; 2 3 public class Holder2<T> { 4 5 private T a; 6 public Holder2(T a){ 7 this.a = a; 8 } 9 public void set(T a){ 10 this.a = a; 11 } 12 public T get(){ 13 return a; 14 }15 }
二、泛型中使用注意的方法特性
由于Object的包容性,导致了参数可能的混乱。所以为了解决这个问题,泛型的提出是一个很好的方案。下面就来介绍下泛型的一些用法和特点,这些特性大都由于泛型的擦除机制所导致,擦除机制是指在运行过程中任何和类型有关的信息都会被擦除,所有在运行中集合的具体信息都被擦除成它们的原生类型。下面通过一个例子来了解:
1 public class TestF { 2 3 public static void main(String[] args) { 4 Class a1 = new ArrayList<Integer>().getClass(); 5 Class a2 = new ArrayList<String>().getClass(); 6 System.out.println(a1 == a2); 7 //结果是true 8 } 9 10 }
ArrayList<Integer>和ArrayList<String>的class其实在擦除后都是ArrayList。
1.不能有继承的关系泛型的
Vector<Object> v = new Vector<String>(); //Type mismatch: cannot convert from Vector<String> to Vector<Object>
不能有继承的关系泛型。声明了一个Vector<Object>与Vector<String>这对组合和Object与String这对组合是完全不同的。Object和String这对的确有关系,Object是String的父类,从java的多态Object obj = new String()这个语句是没有关系的。但是Vector<Object>和Vector<String>的组合就不是继承关系,在编译时,其实、Vector<Object> v =new Vector<String>实际上是Vector c = new Vector()这种效果,其实就是java泛型的擦除效果。
其实从另外一个角度去看,Vector<Object>其实就是告诉编译器,Vector里面要放的是Object,但是在实例化的时候却放入了String,虽然是继承关系,但是还是不同的两个class。当涉及到了集合,看问题的角度提升到 集合 的层面去观察,里面的泛型类型就一定要一致了(在没有使用通配符?的情况下要一致,使用了?后面详细讲)。
2.不能有声明数组类型的泛型
为什么不能有声明数组类型的泛型呢?我看了一下Java SE的官方文档 http://docs.oracle.com/javase/tutorial/extra/generics/fineprint.html
里面有两段代码很清楚的阐释了这个问题。
1 // Not really allowed. 2 List<String>[] lsa = new List<String>[10]; 3 Object o = lsa; 4 Object[] oa = (Object[]) o; 5 List<Integer> li = new ArrayList<Integer>(); 6 li.add(new Integer(3)); 7 // Unsound, but passes run time store check 8 oa[1] = li; 9 10 // Run-time error: ClassCastException. 11 String s = lsa[1].get(0);
假设泛型数组允许被建立,如果此时有一个List<String>的数组,但是经过一系类转换后,居然把List<Integer>给赋给了 List<String>数组,这是明显的错误,但是由于java泛型的擦除机制,程序代码没有报错,此时已经有问题了。
解决这个问题,就是不要声明数组类型的泛型,用通配符声明。
1 // OK, array of unbounded wildcard type. 2 List<?>[] lsa = new List<?>[10]; 3 Object o = lsa; 4 Object[] oa = (Object[]) o; 5 List<Integer> li = new ArrayList<Integer>(); 6 li.add(new Integer(3)); 7 // Correct. 8 oa[1] = li; 9 // Run time error, but cast is explicit. 10 String s = (String) lsa[1].get(0);
由于通配符,程序一开始不知道List<?>具体是什么类型的的数组,这个时候我们将List<Integer>放入进去,编译器还好能接受。最后一句,故意从List<Integer>去拿String,这是报了java.lang.ClassCastException的错误。
3.泛型类不能有static修饰方法
这个更加不用说了,泛型编译后其实是擦除了很多信息,很多时候都没有确定好参数类型,到了运行调用的时候才进行的添加的。而static是静态关键字,被修饰的变量和方法都是在运行一开始就被加载到了虚拟机中,无需要再new对象,一直通过对象引用就好了。这两种状态是不一致的,所以不能在泛型类上面加上static。
4.泛型参数会由于参数的合集类型、个数、顺序一样而导致方法不能重载。
1 public static void applyVecotr(Vector<Integer> v,Integer i){ 2 //泛型里的参数是不能作为区分参数去overload(重载) 3 } 4 public static void applyVecotr(Vector<String> v,Integer i){ 5 //泛型里的参数是不能作为区分参数去overload(重载) 6 }
此时报错 Erasure of method applyVecotr(Vector<Integer>, Integer) is the same as another method in type GenericTest
5.通配符?和泛型类型参数代表符号<T>、<K>、<V>等的关系和区别
在一个代码中通配符?用于泛型代表任何的类型,而<T>、<K>、<V>也是一样功能。但是<T>、<K>、<V>等符号有一个好处,就是可以运用到在整个方法甚至整个类中去,用来区分多个泛型。表示<T>所代表的符号都是同一个类型的参数,而<K>是另外的一种参数。但是对于泛型参数则需要在方法或者类的开头声明,而通配符?则不需要的。
1 public class DiffGenericParameter<K,V> { 2 3 private K k;//代表K为key值 4 private V v;//代表V为value值 5 public DiffGenericParameter(K k,V v){ 6 this.v = v; 7 this.k = k; 8 }; 9 }
三、擦除与其解决方法
在《java编程思想》中对擦除的核心动机做出了解释:它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”。在理想情况下,当所有事物都可以同时被泛化时,我们就可以专注于此。在现实中,即使程序员只编写泛化代码,他们也必须处理在Java SE5之前编写的非泛型类库。那些类库的作者可能从没有想过要泛化它们的代码,或者可能刚刚开始接触泛型。
因此Java泛型不仅必须支持向后兼容性,即现有的代码和类文件仍旧合法,并且继续保持其之前的含义;而且还要支持迁移兼容性,使得类库按照它们自己的步调变为泛型的,并且当某个类库变为泛型时,不会破坏依赖于它的代码和应用程序。通过允许非泛型代码与泛型代码共存,擦除使得这种向着泛型的迁移成为可能。
尽管如此,但是我们还是可以通过反射得到一些蛛丝马迹的。通过反射的getGenericParameterTypes去拿到泛型中的参数类型。
1 package Generic; 2 3 import java.lang.*; 4 import java.util.*; 5 6 public class GenericTest { 7 8 public static void main(String[] args) throws Exception { 9 10 /** 11 * 通过反射去获取方法的泛型参数化类型,通过其他方式是无法知道泛型的参数类型,因为在泛型中 12 * 编译后去掉了泛型化。 13 */ 14 Method method = GenericTest.class.getMethod("applyVector", Vector.class,Map.class); 15 Object[] ParameterTypes = method.getParameterTypes(); 16 //System.out.println(ParameterTypes); 17 /*for(Object ParameterType : ParameterTypes ){ 18 System.out.println(ParameterType); 19 }*/ 20 ParameterizedType type = (ParameterizedType) method.getGenericParameterTypes()[0]; 21 System.out.println("type.getRawType() = "+type.getRawType()); 22 System.out.println("type.getActualTypeArguments = "+type.getActualTypeArguments()[0]); 23 } 24 25 public static void applyVector(Vector<Date> v, Map<String,String> map){ 26 27 } 28 29 }
得到的结果是:
type.getRawType() = class java.util.Vector type.getActualTypeArguments = class java.util.Date
由于有了这个反射能得到泛型的参数类型后,还是挺兴奋的。但是我们不能经常去使用反射,这毕竟还是一个耗时耗力的事情。所以在泛型的很多代码编写上就有新的尝试了。
- 泛型的定义加上了上边界和下边界
- 加入Class类型的代码
1.泛型的定义加上了上边界和下边界
因为擦除移除了类型信息,如果使用无界泛型参数调用的方法只是那些可以用Object调用的方法。如果能够将某个参数限制为某个类型子集,我们就能调用这个类型子集去调用方法了。所以在Java泛型中用了extends这个关键字,但是此时extends和在普通情况下面所具有的意义是完全不同的。
1 package Generic; 2 3 import java.awt.Color; 4 5 interface HasColor{java.awt.Color getColor();} 6 7 class Colored<T extends HasColor>{ 8 T item; 9 Colored(T item){ this.item = item;} 10 T getItem(){ 11 return item; 12 } 13 java.awt.Color color(){return item.getColor();} 14 } 15 16 class Dimension{public int x , y, z;} 17 18 //this won't work -- class must be first, then interface; 19 //calss coloredDimension<T extends HasColor & Dimension> 20 21 class ColoredDimension<T extends Dimension & HasColor>{ 22 T item; 23 ColoredDimension(T item){this.item = item;} 24 T getItem(){return item;} 25 java.awt.Color getColor(){return item.getColor();} 26 int getX(){return item.x;} 27 int getY(){return item.y;} 28 int getZ(){return item.z;} 29 } 30 31 interface Weight{int weight();} 32 33 class Solid<T extends Dimension & HasColor & Weight>{ 34 T item; 35 Solid(T item){this.item = item;} 36 T getItem(){return item;} 37 java.awt.Color color(){return item.getColor();} 38 int getX(){return item.x;} 39 int getY(){return item.y;} 40 int getZ(){return item.z;} 41 int weight(){return item.weight();} 42 } 43 44 class Bounded extends Dimension implements HasColor ,Weight { 45 46 @Override 47 public int weight() { 48 // TODO Auto-generated method stub 49 return 0; 50 } 51 52 @Override 53 public Color getColor() { 54 // TODO Auto-generated method stub 55 return null; 56 } 57 58 } 59 60 public class BasicBounds { 61 62 public static void main(String[] args) { 63 Solid<Bounded> solid = new Solid<Bounded>(new Bounded()); 64 System.out.println(solid.weight()); 65 System.out.println(solid.color()); 66 System.out.println(solid.getY()); 67 } 68 69 }
所以你会发现,原来extends在泛型使用中不仅仅可以被用来限定上边界class,还可以用来限定上边界interface。而且,在此之后我们可以使用class或者interface中的类型变量或者是方法,这样子我们就可以很好的利用Java提供的这个机制去构建一些泛型关系。
2.加入Class类型的代码
既然被擦除了,那就在调用的时候再将这个类型传入就好了。这个也是挺不错的方法之一。
1 package Generic; 2 3 class ClassAsFactory<T>{ 4 T x; 5 public ClassAsFactory(Class<T> kind){ 6 try { 7 x= kind.newInstance(); 8 } catch (InstantiationException e) { 9 // TODO Auto-generated catch block 10 e.printStackTrace(); 11 } catch (IllegalAccessException e) { 12 // TODO Auto-generated catch block 13 e.printStackTrace(); 14 } 15 } 16 } 17 18 class Employee{} 19 20 public class InstantiateGenericType { 21 22 public static void main(String[] args) { 23 ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class); 24 System.out.print("ClassAsFactory<Employee> succeed"); 25 ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class); 26 27 } 28 }
发现了没有,在new实例化这个泛型的时候,我们再将这个实例化的类型传入,接着再将这个newInstance就好了。
下一篇,将介绍泛型用于程序设计中的实例,不错的实例。