----------- android培训、java培训、期待与您交流! ------------
为何引入泛型
有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创造容器类。
JDK1.5以前的集合类有两个弊端:需要对类型进行转换,有可能取出来的结果类型不确定。
即:
无论是狗对象还是猫对象,丢进集合类中都会被转型为Object对象。
可能会出现“狗在猫列表中”这种问题。
如:
//JDK1.5之前的写法
ArrayList collection1= new ArrayList();
collection1.add(1);
collection1.add(1L);
collection1.add("abc");
//实际传入的是Long类型,但强制转换为int,报告ClassCastException
int i =(Integer) collection1.get(1);
JDK1.5的集合类希望在定义集合时,要求你明确出要向集合中装入的数据类型,无法加入指定类型以外的数据,如:
//JDK1.5以后应用泛型
ArrayList<String>collection2 = new ArrayList<String>();
/*因为进行了类型限定,只能为Sting,所以下面两句报告语法错误
*collection2.add(1);
collection2.add(1L);*/
collection2.add("abc");
//前面已经声明装入集合的类型,不必要再进行类型转换
String element=collection2.get(0);
用泛型对构造函数的反射应用进行改写:
//new String(new StringBuffer("abc"));
Constructor<String> constructor=String.class.getConstructor(StringBuffer.class);
//给构造函数里面传入实际对象
String str=constructor.newInstance(new StringBuffer("abc"));
System.out.println(str.charAt(2));//c
总结:没有使用泛型时,只要是对象,不管是什么类型的都可以存储进一个集合中。使用泛型集合,可以将一个集合中的元素限定为一个特定类型,集合中只能存储同一类型的对象,这样在编译时期就能发现错误,更安全。当从集合中获取一个对象时,编译器也可以知道这个对象的类型,不需要对对象进行强制类型转换,这样更方便。
在JDK1.5中依然可以按原来的方式将各种不同类型的数据装入一个集合,但编译器会报告unchecked警告。
另:需要注意的是,对集合类的改进或者说解决“狗在猫列表中”的问题可能是java中泛型出现的一个契机,但是这并不代表泛型的意义仅限于此。泛型作为一种通用语言特性(并非必须是其在java中的特定实现)的目的在于可表达性,而不仅仅是为了创建类型安全的容器。
正如泛型的名字所暗示的:它是一种方法,通过它可以编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制,因此单个的代码可以应用到更多的类型上。
泛型的内部原理及更深应用
泛型是提供给javac编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,编译器编译带类型说明的集合时会去除掉“类型”信息(去类型化,Thinking in Java中称之为擦除),使程序运行效率不受影响,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样。
//验证编译器编译完会去掉类型信息,原因由于编译完以后两个变量的类型是一致的,而getClass()拿到的是同一份字节码
ArrayList<String> collection2=new ArrayList<String>();
ArrayList<Integer> collection3=new ArrayList<Integer>(); System.out.println(collection3.getClass()==collection2.getClass());
//返回true
由于编译生成的字节码会去掉泛型的类型信息,只要能跳过编译器,就可以往某个泛型集合中加入其他类型的数据,例如,用反射得到的集合,再调动其add方法即可
/*由上面可知,只要跳过编译器,就可以往集合中出加入其它类型的数据,现在实现collection3.add("abc")的功能*/
ArrayList<Integer> collection3=new ArrayList<Integer>();
//得到类的add方法
MethodmethodAdd=collection3.getClass().getMethod("add", Object.class);
methodAdd.invoke(collection3, "abc");//明确要操作的对象以及要传入方法的参数
System.out.println(collection3.get(0));//abc
结论:泛型是给编译器看的
了解泛型
l ArrayList<E>类定义和ArrayList<Integer>类引用中涉及如下术语:
整个称为ArrayList<E>泛型类型
ArrayList<E>中的E称为类型变量或类型参数
整个ArrayList<Integer>称为参数化的类型(parametered Type)
ArrayList<Integer>中的Integer称为类型参数的实例或实际类型参数.
ArrayList<Integer>中的<>念为typeOf
ArrayList称为原始类型
l 参数化类型与原始类型的兼容性【jdk1.4与1.5的兼容】
参数化类型可以引用一个原始类型的对象,编译只报告警告[不是错误],例如:
Collection<String> c=new Vector();//通过
原始类型可以引用一个参数化类型的对象,编译值报告警告[不是错误],例如:
Collection c=new Vector<String>();//通过,原来方法接收一个集合参数,新的类型也要能传进去
l 参数化类型不考虑类型参数的继承关系:
Vector<String> v=new Vector<Object>();//错误,不写<object>就不会报错,写了就是明知故犯
Vector<Object> v=new Vector<String>();//也错误
如果希望集合在编译时期就能挡住意外的值,并且调用get方法返回的值不用进行类型转换,就用泛型去写。
明确概念:String s=new String();左边s是变量,右边newString()是实际类型
l 编译器不允许创建类型变量的数据,即在创建数组实例时,数组的元素不能使用参数化类型,例如,下面语句有错误
Vector<Integer>[] vectorList=newVector<Integer>[10];
l 思考题:下面的代码会报错误吗?
Vector v1=new Vector<String>();//参数化类型指向原始类型,而v1是原始类型,编译通过
Vector<Object> v=v1;//这里原始类型就指向了参数化类型,编译通过
这两句编译都能通过,因为编译器是严格按照语法检查的工具,不考虑运行时的效果【运行时已将泛型类型去掉】。它是逐行翻译代码的,上面两句都符合兼容性的语法规范。
泛型的?通配符及扩展应用
问题:定义一个方法,该方法用于打印出任意参数化类型的集合中的所有数据,如何定义?
错误方式:
ArrayList<Integer>collection3=new ArrayList<Integer>();
/*此操作等效于Collection<Object> collection=collection3,
* 编译器会报错, 因为此处违反了参数化类型不考虑类型参数的继承关系
* 当把一个对象传给你,就相当于把值赋给你【接收者】
*Collection<Object>和ArrayList<Integer>是两个没有转换关系的参数化类型
*/
// printCollection1(collection3);
//定义一个打印集合的方法
privatestaticvoid printCollection1(Collection<Object>collection) {}
正确方式:
printCollection2(collection3);
//在JDK1.5中的泛型里提供了一种通配符的方式?表示?可以指向任意类型【?表示任意类型】
privatestaticvoid printCollection2(Collection<?>collection) {
//collection.add(1);编译错误,通配符可以作为引用变量去引用一个类型,但是引用以后,不能拿它去调用与类型有关的方法
System.out.println(collection.size());//没错,size()方法与类型参数无关
for(Objectobj:collection){
System.out.println(obj);
}
//下面语句正确,?定义的变量可以指向任何实际类型的东西,只是一个=赋值
collection=new HashSet<Date>();
}
总结:使用?通配符可以引用其它各种参数化的类型,?通配符定义的变量主要用作引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。如Collection(E)的方法add(E e)
限定通配符的上边界:
正确:Vector<? extends Number> x=newVector<Integer>();
错误:Vector<? extends Number> x=newVector<String>();
限定通配符的下边界:
正确:Vector<? super Integer> x=newVector<Number>();
错误:Vector<? super Integer> x=new Vector<Byte>();
/*下面示例说明带<?>和实际类型的用法
*java.lang.Class中
* staticClass<?> forName(String className) 演示
*/
Class<?> y=Class.forName("java.lang.String");
Class<String> x = null;
y=x;
/**
* 下面写法会编译错误,因为不能那Class<String>=Class<?>;
* 结论:不能把?赋给一个具体的类型,但可以把具体类型赋给?
*/
// Class<String>x=Class.forName("java.lang.String");
总结:?类型参数只能传入比自己范围小的实际类型参数,而不能:小值=<?>
提示:限定通配符总是包括自己
复杂的泛型集合类:
/*
* 泛型的深入应用:HashMap存取集合元素
*/
HashMap<String,Integer> map=new HashMap<String,Integer>();
map.put("lisi", 23);
map.put("zhangsan", 20);
//key-vale的组合体叫Entry,Map.Entry<K,V>就是用来存放键值对
Set<Map.Entry<String,Integer>>entrySet=map.entrySet();
for(Map.Entry<String,Integer> entry:entrySet){
System.out.println(entry.getKey()+":"+entry.getValue());
}
在jsp页面中也经常要对Map或Set集合进行迭代:
<c:forEach items="${map}" var="entry">
${entry.key}:${entry.value}
</c:forEach>
如下的函数结构很相似:
int add(int x,inty){ returnx+y; }
float add(float x,floaty){ return x+y; }
double add(double x,double y){ return x+y; }
C++用模板函数解决,只写一个通用的方法,它可以适应各种类型,示意代码如下:
templet<class T>
T add(T x,T y){ return (T)(x+y); } T表示定义时类型不详,可以为任意类型
Java中的泛型类型(或者泛型)类似于 C++ 中的模板。但是这种相似性仅限于表面,Java 语言中的泛型基本上完全是在编译器中实现,用于编译器执行类型检查和类型推断,然后生成普通的非泛型的字节码,这种实现技术称为擦除(erasure)(编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除)。这是因为扩展虚拟机指令集来支持泛型被认为是无法接受的,这会为 Java 厂商升级其 JVM 造成难以逾越的障碍。所以,java的泛型采用了可以完全在编译器中实现的擦除方法。
java的泛型是从C++中借鉴过来的,但没有C++的强大,如下代码无法通过编译
/*
* 定义一个类似C++模板方法的的泛型:add()
* 在返回值之前用<>说明类型
*/
privatestatic <T> Tadd(T a,T b){
//return(T)(a+b);报告T类型可能不存在a+b的运算
returnnull;
}
/*泛型方法返回的真实类型无法判断,就用类型推断:取传入类型的交集【最大公约数】*/
add(3,9);//自动将两个类型装成Integer
add(3,4.5);//Integer和float共同的交集是Number,可用Numbern=add(3,4.5);测试
add("abc",'p');//推断出返回类型为Object,Object obj=add("abc",'p');正常编译
用于放置泛型类型参数的<>应出现在方法的其它所有修饰符之后、方法的返回值之前,也就是紧邻返回值之前。按照惯例,类型参数通常用单个大写字母表示
交换数组中两个元素的位置,用泛型方法定义如下:
//数组中的两个元素交换位置
privatestatic <T> void swap(T[] a,int i,int j){
Ttmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
//泛型实际类型只能是对象类型(引用类型),不能为基本类型
swap(new String[]{"hello","thanks","bye-bye"},1,2);
/*下面的编译报错,泛型实际类型只能是对象类型(引用类型),不能为基本类型
* 它不会像 add(2,8);自动装箱成Integer,因为数组已经是一个对象*/
// swap(new int[]{5,7,9,3},2,3);
自定义泛型方法的练习与类型推断总结
编写一个泛型方法,自动将Object类型的对象转换成其他类型
privatestatic <T> TautoConvert(Object obj) {
return (T)obj;
}
Object obj="abc";
String s=autoConvert(obj);
定义一个方法,可以将任意类型数组中的所有元素填充为相应类型的某个对象
//定义一个方法,可以将任意类型数组中的所有元素填充为相应类型的某个对象
privatestatic <T> void fillArray(T[]a,T obj){
for(int i=0;i<a.length;i++){
a[i]=obj;
}
}
采用自定义泛型方法的方式打印出任意参数化类型的集合中所有内容
在这种情况下,通配符方案要比泛型方法更有效,当一个类型变量用来表达两个参数之间或者参数参数和返回值之间的关系时,即同一个类型变量在方法签名的两处被使用,或者类型变量在方法体代码也被使用而不是仅在签名的时候使用,才需要使用泛型方法。
声明为具体类型T,可以操作具体类型的特有方法
privatestatic <T> void printColl(Collection<T>coll){
Iterator<T> it=coll.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
定义一个方法,把任意参数类型集合中的数据安全地复制到相应类型数组中
privatestatic <T> void copy1(Collection<T>src,T[] dest){
}
定义一个方法,把任意参数类型的一个数组中的数据安全地复制到相应类型的另一个数组中
privatestatic <T> void copy2(T[]src,T[] dest){ }
copy1(new Vector<String>(),new String[10]);
copy2(new Date[10],new String[10]);//类型推断取最大公约数,为Object
//根据类型推断的传播性,为Vector声明为Date,那么T的类型就为Date,而第二个数组为String类型,所以编译报错
//copy1(newVector<Date>(),new String[10]);
通过反射获得泛型的实际类型参数
/*通过反射获得泛型的实际类型参数,如获得
Vector<Date> v=newVector<Date>()的参数化类型Date,先定义一个方法applyVector*/
publicstaticvoid applyVector(Vector<Date>v){}
Method method=GenericTest.class.getMethod("applyVector", Vector.class);
//获取泛型方法的参数列表类型集合
Type[]types=method.getGenericParameterTypes();
//获得具体某个参数的参数化类型
ParameterizedTypetype=(ParameterizedType)types[0];
//得到原始的类型
System.out.println(type.getRawType());
//得到实际化的类型参数,因为知道方法只有一个参数,所以取第一个元素
System.out.println(type.getActualTypeArguments()[0]);