在我们开发中,经常会用到集合,我们也知道集合是用来存储元素的,但它却没有规范存储元素的具体类型,为了是我们的程序更规范化,就应当使用泛型了。
泛型是提供给javac编辑器使用的,可以限定集合中的输入类型,让编译器挡住源程序的非法输入,编辑器编译带类型说明的集合时会去除掉“类型”信息,是程序运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样,由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其他类型的数据,例如:用反射得到集合,在调用其add方法即可。
ArrayList<String> list1 = new ArrayList<String>(); ArrayList<Integer> list2= new ArrayList<Integer>(); System.out.println(list1.getClass() == list1.getClass()); 打印true,就表示泛型只提供给编译器使用,运行期是不受影响,那我们想在list2中添加String类型的对象,如果我们直接add方法,编译器会报错。那我们怎么办?我们可以使用反射得到集合,然后调用其add方法就可以添加String类型的对象 list2.getClass().getMethod("add", Object.class).invoke(list2, "abc"); System.out.println(collection3.get(0)); 这样我们就在list2中添加了一个String类型的对象。 |
下面我们来简单了解一下泛型
l 术语,例如ArrayList<E>类定义和ArrayList<Integer>类定义
n 整个称为ArrayList<E>泛型类型
n ArrayList<E>中的E称为类型变量或类型参数
n 整个ArrayList<Integer>称为参数化的类型
n ArrayList<Ingerger>中的Integer称为类型参数的实例或实际类型参数
n ArrayList<Integer>中<>念着typeof
n ArrayList称为原始类型
l 参数化类型与原始类型的兼容性
n 参数化类型可以引用一个原始类型的对象,编译报告警告,但没有错,例如:Collection<String> c = new Vector();
n 原始类型可以引用一个参数化类型的对象,编译报告警告,但没有错,例如:Collection c = new Vector<String> ();
l 参数化类型不考虑类型参数的继承关系:
n Vector<String> v = new Vector<Object>(); 错误
n Vector< Object > v = new Vector<String>(); 错误
l 在创建数组实例时,数组的原始不能使用参数化的类型,例如:
Vector<Integer> v[] = new Vector<Integer>[10];错误
练习:定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,该方法如何定义?
错误方式:
public void printCollection(Collection<Object> collection){ for(Object obj : collection){ System.out.println(obj); } } 为什么错呢? 由于泛型不考虑类型参数的继承关系,如果我们调用这个方式传递的是 Collection<Integer> list2= new ArrayList<Integer>(); 那肯定会报错,这个方法是不能打印任意参数化类型的集合 |
正确方式:
public void printCollection(Collection<?> collection){ for(Object obj : collection){ System.out.println(obj); } } 这个方法能打印出任意参数化类型的集合,但是collection不能调用add方法,为什么? 因为使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法,例如add方法就是与参数化有关的方法。 |
使用?通配符表示可以使用任意参数化类型,如果我们需要一个范围,那该怎么办呢?在?通配符中还有两个上边界与下边界,分别为:限定通配符上边界和限定通配符下边界。
l 限定通配符上边界
Vector<? extends Number> x = new Vector<Integer>(); 正确
Vector<? extends Number> x = new Vector<String>(); 错误
l 限定通配符下边界
Vector<? super Integer> x = new Vector<Number>(); 正确
Vector<? super Integer> x = new Vector<Byte>(); 错误
我们了解泛型的一些知识后,现在我们来自己定义泛型,泛型方法与泛型类
l 自定义泛型方法
//将任意类型的数组中的所有元素填充为相应类型的某个对象 private <T> void fillArray(T[] a,T obj){ for(int i=0;i<a.length;i++){ a[i] = obj; } } //将Object对象转化成任意对象 private <T> T autoConvert(Object obj){ return (T)obj; } //将任意类型的数组调换位置 private <T> void swap(T[] a,int i,int j){ T tmp = a[i]; a[i] = a[j]; a[j] = tmp; }
private <T> T add(T x,T y){ return null; } 调用代码: add(3,5); Number x1 = add(3.5,3); Object x2 = add(3,"abc"); 返回类型必须为我们传递参数的最小公共类型 String[] strArray=new String[]{"a","b","c"}; swap(strArray,1,2); //这样就可以实现数组交换 但是下面这种情况是不行 int[] intArray=new int[]{1,2,3}; swap(intArray,1,2); 因为只有引用类型才能作为泛型方法的实际参数,那为什么上面add怎么就可以呢?因为add方法中传递int值会自动装箱和拆箱,而我们这里是一个int[]类型是不会自动装箱和拆箱,因此编译器会报错 |
我们在定义泛型是可以使用extends限定符,我们可以参考java.lang.Class类的一个方法:
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
并且可以用&来指定多个边界,如:<V extends Serializable&Thread>
普通方法、构造方法和静态方法中都可以使用泛型;也可以抛出异常泛型,例如:
public <T extentds Exception> exceptionTest throws T{ try{
}catch(Exception e){ throw (T)e; }
} |
在泛型中可以同时有多个类型参数,在定义他们的尖括号中用逗号分,例如:
public <K,V> V getValue(K key){} |
l 自定义泛型类型
如果类的实例对象的多处都要用到同一个泛型参数,即这些地方引用的泛型类型要保持同一个实际类型时,这时候就要采用泛型类型的方式进行定义,也就是类级别的泛型。例如:
public class GenericDao<E> { public void add(E x){ } public E findById(int id){ return null; } public void delete(E obj){ } public void delete(int id){ } public void update(E obj){ } public static <E> void update2(E obj){ } public E findByUserName(String name){ return null; } public Set<E> findByConditions(String where){ return null; } } |
注意:
n 在对泛型类型进行参数化时,类型参数的实例必须是引用类型,不能是基本类型
n 当一个变量被声明为泛型时,只能被实例变量和方法调用(还有内嵌类型),而不能被静态变量和静态方法调用。因为静态成员是被所有参数化的类所共享,所以静态成员不应该有类级别的类型参数。