Java基础加强总结(二)——泛型
一、体验泛型
JDK1.5之前的集合类中存在的问题——可以往集合中加入任意类型的对象,例如下面代码:
1 package cn.gacl.generic.summary; 2 3 import java.util.ArrayList; 4 5 public class GenericTest { 6 7 public static void main(String[] args) { 8 /** 9 * 不使用泛型之前ArrayList容器可以存储任意类型的对象 10 */ 11 ArrayList collection1 = new ArrayList(); 12 collection1.add(1);//存储Integer对象 13 collection1.add(1L);//存储Long对象 14 collection1.add("xdp");//存储String对象 15 /** 16 * 这里会报异常: JAVA.LANG.CLASSCASTEXCEPTION: 17 * JAVA.LANG.LONG CANNOT BE CAST TO JAVA.LANG.INTEGER 18 * 19 */ 20 int i = (Integer) collection1.get(1); 21 } 22 }
JDK1.5之后的集合类希望你在定义集合时,明确表示你要向集合中装哪种类型的数据,无法加入指定类型之外的数据,例如下面的代码:
/** * 使用泛型限定ArrayList容器只能存储字符串类型的对象 */ ArrayList<String> collection2 = new ArrayList<String>(); collection2.add("孤傲苍狼"); //collection2.add(1);//报错,因为限制了collection2只能存储String类的对象,不能加入Integer类型的对象 //collection2.add(1L);//报错,因为限制了collection2只能存储String类的对象,不能加入Long类型的对象 //由于已经指明集合中存储的是字符串类型的对象,因此这里不用再强制转型了 String element = collection2.get(0);
泛型是提供给Javac编译器看的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译带参数类型说明的集合时会去去除掉“类型”信息,使程序运行不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样,由于编译生成的字节码会去掉泛型的类型信息,因此只要能跳过编译器,就可以往某个泛型集合中加入其他类型的数据。
例如下面的代码就演示了"使用反射得到集合,然后调用add方法往原本只能存储Integer对象的集合中存储一个String类型的对象"
1 ArrayList<Integer> collection3 = new ArrayList<Integer>(); 2 //对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样 3 System.out.println(collection3.getClass());//结果为:java.util.ArrayList 4 System.out.println(collection3.getClass() == collection2.getClass());//结果为true 5 //使用反射得到集合,然后调用add方法往原本只能存储Integer对象的集合中存储一个String类型的对象 6 collection3.getClass().getMethod("add", Object.class).invoke(collection3, "abc"); 7 System.out.println(collection3.get(0));//输出的结果为:abc,这证明字符串对象确实是存储到了原本只能存储Integer对象的集合中
备注:
- 泛型是JDK1.5的所有新特性中最难深入掌握的部分,没有使用泛型时,只要是对象,不管是什么类型的对象,都可以存储进同一个集合中,使用泛型集合,可以将一个集合中的元素限定为一个特定类型,这样集合中就只能存储同一类型的对象,这样更安全;并且当从集合中获取一个对象时,编译器也知道这个对象的类型,不需要对对象进行强制类型转换,这样更方便。
- 在JDK1.5之后,你还可以按原来的方式将各种不同类型的数据放到同一个集合中,但是编译时会报一个unChecked警告
- 泛型中的类型参数严格说明集合中装载的数据类型是什么和可以加入什么类型的数据,记住:Collection<String>和Collectin<Object>是两个没有转换关系的参数化的类型
二、了解泛型
- ArrayList<E>类定义和ArrayList<Integer>类引用中涉及如下术语:
- 整个称为ArrayList<E>泛型类型
- ArrayList<E>中的E称为类型变量或类型参数
- 整个ArrayList<Integer>称为参数化类型
- ArrayList<Integer>中的Integer称为类型参数的实例或实际类型参数
- ArrayList<Integer>中的<>是“typeof”
- ArrayList称为原始类型
- 参数化类型与原始类型的兼容性:
- 参数化类型可以引用一个原始类型的对象,编译时编译器会报警告,例如:Collection<String> c = new Vector();
- 原始类型可以引用一个参数化类型的对象,编译时编译器会报警告,例如:Collection c = new Vector<String>();
- 参数化类型不考虑类型参数的继承关系:
- Vector<String> v = new Vector<Object>();//错误,语法上不通过
- Vector<Object> v = new Vector<String>();//错误,语法上不通过
假设Vector<String> v = new Vector<Object>;可以的话,那么以后从v中取出的对象当作String用,而v实际指向的集合中可以加入任意类型的对象,
假设Vector< Object > v = new Vector< String >;可以的话,那么以后可以向v中加入任意类型的对象,而v实际指向的集合中只能装String类型的对象
思考:下面的代码会报错吗?(不会报错)
- Vector v1 = new Vector<String>();//参数化类型的对象可以给原始类型的引用
- Vector<Object> v=v1;//参数化类型的引用可以指向原始类型的对象
三、泛型中的?通配符
问题:定义一个方法,该方法可以打印出任意参数化类型的集合中的所有数据,该方法如何定义呢?
错误的定义:
1 /** 2 * Collection<Object>中的Object只是说明Collection<Object>实例对象中的方法接收的参数是Object 3 * Collection<Object>是一种具体的类型,new HashSet<Date>也是一种具体的类型,两者没有兼容性问题 4 * @param collection 5 */ 6 public static void printCollection(Collection<Object> collection){ 7 for(Object obj:collection){ 8 System.out.println(obj); 9 } 10 collection.add("abc");//没错 11 collection=new HashSet<Date>();//会报告错误 12 }
正确的定义:
1 /**这里的Collection<?>中的?表示可以传人任意的类型参数 2 * Collection<?> cols可以匹配任意参数化的类型,但是到底匹配的是什么类型,只有以后才知道 3 * 所以 cols=new ArrayList<Integer>和cols = new ArrayList<String>都可以 4 * 但是cols.add("abc")或cols.add(new Date())都不行 5 */ 6 public static void printCollection(Collection<?> collection){ 7 for(Object obj:collection){ 8 System.out.println(obj); 9 } 10 //collection.add("abc");//报错,因为collection不知道未来匹配的一定是String类型 11 collection.size();//不报错,此方法与参数类型没有关系 12 collection=new HashSet<Date>();//这是可以的 13 }
总结:使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要用作引用,可以调用与参数无关的方法,不能调用与参数有关的方法
四、泛型中的?通配符的扩展
1.限定通配符?的上边界
- 正确的写法:Vector<? extends Number> x = new Vector<Integer>();
这里指的是?所代表的参数化类型必须是继承Number类的,如这里的?所代表的Integer类型就是继承Number类的
- 错误的写法:Vector<? extends Number> x = new Vector<String>();
2.限定通配符?的下边界
- 正确的写法:Vector<? super Integer> y = new Vector<Number>();
这里指的是?所代表的参数化类型必须是Integer类的父类,如这里的?所代表的Number类型就是Integer类的父类
- 错误的写法:Vector<? super Integer> y = new Vector<Byte>();
五、泛型的综合应用
1 package cn.itcast.day2; 2 import java.util.HashMap; 3 import java.util.HashSet; 4 import java.util.Map; 5 import java.util.Set; 6 /** 7 * 此类是用来演示泛型的应用的 8 * 9 * @author 孤傲苍狼 10 * 11 */ 12 public class GenericCase { 13 public static void main(String[] args) { 14 HashMap<String, Integer> maps = new HashMap<String, Integer>(); 15 maps.put("lhm", 35); 16 maps.put("flx", 33); 17 /** 18 * 变量的命名技巧:如果以后不知道一个变量该如何命名,就可以以方法名的形式来命名, 19 * 如果要定义变量接收返回值,如果此时不知道如何定义变量名时,就直接定义成returnValue 20 */ 21 Set<Map.Entry<String, Integer>> entrySet = maps.entrySet();// 这里的变量名直接以方法名的形式定义 22 // 使用增强的for循环迭代Map容器中的key和value 23 //这里的Entry是Map类的一个内部类,map类中存储的key和value都是封装在这个Entry内部类中的 24 //这个Entry内部类提供了getKey方法取出键,getValue方法取出值 25 for (Map.Entry<String, Integer> entry : entrySet) { 26 System.out.println(entry.getKey() + ":" + entry.getValue()); 27 } 28 } 29 }
在JSP页面中也经常要使用迭代标签<c:forEach>对Set或Map集合进行迭代:
1 <c:forEach items=”${map}” var=”entry”> 2 ${entry.key}:${entry.value} 3 </c:forEach>
六、自定义泛型方法
1 package cn.itcast.day2; 2 import java.io.Serializable; 3 /** 4 * 此类是用来演示如何定义和使用泛型方法的 5 * 6 * @author 孤傲苍狼 7 * 8 */ 9 public class GenericMethod { 10 public static void main(String[] args) { 11 add(3, 5); 12 Number x1 = add(3.5, 5);// Integer类型和Double类型的交集就是Number类,Number类是这两个类的父类,所以可以定义Number类型的变量来接收返回值 13 Object x2 = add(3, "abc");// Integer类型和String类型的交集就是Object类,因为Object类是所有类的父类,所以可以定义Object类型的变量来接收返回值 14 /** 15 * swap(new String[]{"abc","123","xdp"},1,2);的执行结果如下: 16 * abc 123 xdp 17 * abc xdp 123 18 * 从结果来看,索引为1的元素和索引为2的元素的确是交换了位置 19 */ 20 swap(new String[] { "abc", "123", "xdp" }, 1, 2);// 调用自定义泛型方法,传入的实际参数必须是引用类型的数组 21 // swap(new int[]{1,2,3,4,5},1,3);//只有引用类型才能作为泛型方法的实际参数,这里的int[]数组是属于基本类型,不能作为泛型方法的参数,所以这样会报错 22 printArray(new Integer[]{1,2,3});//可以传入Integer类型的数组,因为Integer类型的数组是属于引用类型的 23 //printArray(new int[]{10,2,5});不能传入非引用类型的数组作为泛型方法的实际参数 24 } 25 /** 26 * 泛型方法的定义语法: 这里定义的就是一个泛型方法 方法的返回值类型是T,即任意的类型 返回值的具体类型由传入的类型参数来定 27 * 28 * @param <T> 29 * 代表任意的类型 30 * @param x 31 * @param y 32 * @return 33 */ 34 private static <T> T add(T x, T y) { 35 return null; 36 } 37 /** 38 * 定义一个泛型方法来交换数组中指定索引位置的两个元素 这里传入的数组可以是任意类型的数组 39 * 传入泛型方法的实际参数类型必须是引用类型的数组,不能是基本类型的数组 40 * 41 * @param <T> 42 * @param a 43 * @param i 44 * @param j 45 */ 46 private static <T> void swap(T[] a, int i, int j) { 47 // 数组中元素位置未交换前的打印结果 48 printArray(a); 49 T temp = a[i]; 50 a[i] = a[j]; 51 a[j] = temp; 52 System.out.println(); 53 // 数组中元素位置交换后的打印结果 54 printArray(a); 55 } 56 /** 57 * 定义打印任意引用数组类型的方法 58 * 59 * @param <T> 60 * @param array 61 */ 62 private static <T> void printArray(T[] array) { 63 for (T t : array) { 64 System.out.print(t + "\t"); 65 } 66 } 67 /** 68 * 定义有extends限定符,并且具有多个上边界的泛型方法,各个边界使用&符号分隔 69 * @param <T> 70 */ 71 public <T extends Serializable & Cloneable> void method(){} 72 }
普通方法,构造方法和静态方法都可以使用泛型
七、泛型方法练习题
- 编写一个泛型方法,自动将Object类型对象转换为其他类型
- 定义一个泛型方法,可以将任意类型的数组中的所有元素填充为相应类型的某个对象
- 采用自定泛型方法的方式打印出任意参数化类型的集合中的所有内容。
- 定义一个泛型方法,把任意参数类型的集合中的数据安全地复制到相应类型的数组中
- 定义一个泛型方法,把任意参数类型的一个数组中的数据安全地复制到相应类型的另一个数组中去
1 /** 2 * 1.编写一个泛型方法,自动将Object类型对象转换为其他类型 3 * @param <T> 4 * @param obj 5 * @return 6 */ 7 private static <T> T autoConvert(Object obj){ 8 return (T)obj; 9 } 10 /** 11 * 2.定义一个泛型方法,可以将任意类型的数组中的所有元素填充为相应类型的某个对象 12 * @param <T> 13 * @param array 14 * @param obj 15 */ 16 private static <T> void fillArray(T[] array,T obj){ 17 for(int i=0;i<array.length;i++){ 18 array[i]=obj; 19 } 20 printArray(array); 21 } 22 /** 23 * 3.采用自定泛型方法的方式打印出任意参数化类型的集合中的所有内容 24 * @param <T> 25 * @param collection 26 */ 27 private static <T> void printCollection(Collection<T> collection){ 28 System.out.println(collection.size()); 29 for(Object obj:collection){ 30 System.out.println(obj); 31 } 32 } 33 /** 34 * 4.定义一个泛型方法,把任意参数类型的集合中的数据安全地复制到相应类型的数组中 35 * @param <T> 36 * @param srcCollection 37 * @param descArray 38 */ 39 private static <T> void CollectionCopyToarray(Collection<T> srcCollection,T[] descArray){ 40 Iterator<T> it = srcCollection.iterator(); 41 int recordElementPostion=0; 42 while(it.hasNext()){ 43 descArray[recordElementPostion]=it.next(); 44 recordElementPostion++; 45 } 46 printArray(descArray); 47 } 48 /** 49 * 5.定义一个泛型方法,把任意参数类型的一个数组中的数据安全地复制到相应类型的另一个数组中去 50 * @param <T> 51 * @param srcArray 52 * @param descArray 53 */ 54 private static <T> void srcArrayToDescArray(T[] srcArray,T[] descArray){ 55 for(int i=0;i<srcArray.length;i++){ 56 descArray[i]=srcArray[i]; 57 } 58 printArray(descArray); 59 } 60 private static <T> void printArray(T[] array) { 61 for (T t : array) { 62 System.out.print(t + "\t"); 63 } 64 }
八、自定义泛型类
如果类的实例对象中有多处都要用到同一个泛型参数,即这些地方引用的泛型类型要保持同一个实际类型时,这时就要采用泛型类型的方式定义,也就是类级别的泛型,语法格式如下:
1 package cn.itcast.day2; 2 import java.util.Set; 3 import cn.itcast.day1.ReflectField; 4 /** 5 * DAO:Data Access Object(数据访问对象) 6 * 数据访问:CRUD,即增删改查 7 * @author 孤傲苍狼 8 * 此类是用来演示如何定义泛型类 9 * 此泛型类中的<E>中的E代表实际操作的类型 10 * 指明了操作类型E之后,GenericDAO类中定义的CRUD方法就都是针对于指定的类型 11 */ 12 public class GenericDAO<E> { 13 private E field1; //定义泛型类型的成员变量 14 public <E> void add(E x){ 15 } 16 public <E> E findById(int id){ 17 return null; 18 } 19 public void delete(E obj){ 20 } 21 public void delete(int id){ 22 } 23 public void update(E obj){ 24 } 25 //public static void update(E obj){}这样定义会报错,静态方法不允许使用泛型参数 26 public static<E> void update2(E obj){}//这样定义就可以,此时的这个静态方法所针对的类型和GenericDAO<E>中指定的类型是两个不同的类型 27 public Set<E> findByConditions(String where){ 28 return null; 29 } 30 public static void main(String[] args) { 31 GenericDAO<ReflectField> dao = new GenericDAO<ReflectField>();//这里指定泛型类的操作类型是ReflectField 32 dao.add(new ReflectField(1,3)); 33 ReflectField rf = dao.findById(1); 34 GenericDAO<String> dao1=null; 35 new GenericDAO<String>(); 36 } 37 }
类级别的泛型是根据引用该类名时指定的类型信息来参数化类型变量的,例如,如下的两种方式都可以:
GenericDAO<String> dao=null;
new GenericDAO<String>();
注意:
1.在对泛型类型进行参数化时,类型参数的实例必须是引用类型,不能是基本类型
2.当一个变量被声明为泛型时,只能被实例变量和方法调用(还有内嵌类型),而不能被静态变量和静态方法调用,因为静态成员是被所有参数化的类所共享的,所以静态成员不应该有类级别的类型参数。
九、通过反射获得泛型的实际类型参数
1 package cn.itcast.day2; 2 import java.lang.reflect.Method; 3 import java.lang.reflect.ParameterizedType; 4 import java.lang.reflect.Type; 5 import java.util.Date; 6 import java.util.Vector; 7 /** 8 * 此类是用来演示如何通过反射获得泛型的实际类型参数 9 * Hibernate中的源代码就有这样的写法 10 * @author 孤傲苍狼 11 * 12 */ 13 public class UseReflectGetGenericParameter { 14 public static void main(String[] args) throws Exception { 15 /** 16 * 通过这种方式得到的字节码中是没有办法得到泛型类的实际类型参数的, 17 * 因为在编译这个泛型类时就已经把这个泛型类的实际参数给去掉了 18 * Vector<Date> v = new Vector<Date>(); 19 * v.getClass(); 20 */ 21 Method applyMethod = UseReflectGetGenericParameter.class.getMethod( 22 "applyVector", Vector.class); 23 //得到泛型类型的参数化类型数组,Type类是Class类的父类 24 Type[] types = applyMethod.getGenericParameterTypes(); 25 /** 26 * ParameterizedType这个类是一个参数化类型类,types数组中存储的都是参数化类型的参数, 27 * 这里取出第一个数组元素,并强制转换成ParameterizedType类型 28 */ 29 ParameterizedType pType = (ParameterizedType) types[0]; 30 System.out.println(pType.getRawType()/*得到原始类型,输出的结果为:class java.util.Vector*/); 31 System.out.println(pType.getActualTypeArguments()[0]/*获得泛型的实际类型参数,输出的结果为:class java.util.Date*/); 32 } 33 /** 34 * 利用反射可以得到这个方法的参数列表的类型 35 * 通过这个变量v是没有办法知道定义它的那个类型的 36 * 但是当把这个变量交给一个方法作为参数或者返回值去使用, 37 * Method类中提供了一系列方法可以获得方法的参数列表 38 * 并且是以泛型的那种形式来获得参数列表 39 * @param v 40 */ 41 public static void applyVector(Vector<Date> v) { 42 } 43 }
Java泛型中的标记符含义:
E - Element (在集合中使用,因为集合中存放的是元素)
T - Type(Java 类)
K - Key(键)
V - Value(值)
N - Number(数值类型)
? - 表示不确定的java类型
S、U、V - 2nd、3rd、4th types
Object跟这些标记符代表的java类型有啥区别呢?
Object是所有类的根类,任何类的对象都可以设置给该Object引用变量,使用的时候可能需要类型强制转换,但是用使用了泛型T、E等这些标识符后,在实际用之前类型就已经确定了,不需要再进行类型强制转换。