学习概述:泛型是JDK1.5增加的新特性,可以说是Java当中最难的一部分,本次课程包括泛型的实现原理,泛型的应用和自定义泛型
学习目标:尽量掌握泛型的内部原理,对于泛型的使用有一个初步认识,在以后的开发工作中,在逐步增加对泛型的认识深度。
1.<1>泛型概述:
泛型是JDK1.5新增加的特性,JDK1.5增加泛型支持很大程度上都是为了让集合能够记住元素的数据类型,在没有泛型之前,一旦把一个对象放进一个Java集合中,集合就会忘记对象的类型,把所有的对象当做Object类型处理。当程序中集合中拿出对象以后,就需要进行强制类型转换,这种强制转换不但会造成代码臃肿,还有已发ClassCastException异常。
增加了泛型的支持后的集合,完全可以记住集合中的元素的类型,并可以在编译时检查集合中元素类型,如果试图向集合中添加不满足类型的对象,编译器就会报错。增加了泛型支持后,代码更加简洁,程序更加健壮。
<2>泛型可以说是Java中最难以理解的部分,在学习反省之前我们必须对一些基本的概念彻底掌握。
ArrayLIst<E>和ArrayList<integer>类中涉及的用语如下:
【1】整个ArrayList<E>称为泛型类型,这个多出来的<E>表明就是这个ArrayList被参数化了。
【2】E称为类型变量或者是类型参数
【3】整个ArrayList<Integer>称为参数化类型,Integer被称为实际参数类型或者实际类型参数
【4】ArrayList被称为原始类型
<3>关于泛型的几个要点:
【1】泛型的定义实际上是这样的:可以在定义一个类或者是接口,方法的时候声明形参类型,可见泛型可以应用在任何类中,虽然泛型在集合类中应用的最多,但是实际上泛型不光应用于集合,所以不要想当然的理解为泛型只能在集合类中应用。
例如在反射中,泛型也被广泛应用,实例代码如下所示:
Constructor<T> constructor = String.class.getConstructor(StringBuffer.class);
这就是泛型在反射中的典型应用,如果没有对Constructor类加入泛型声明,那么编译器会发出警告。
【2】编译器编译完之后会去掉泛型信息。这种行为被称作泛型擦除。因此instanceof运算符不能使用泛型类。
下面这个实验结果为true,表明确实在编译阶段泛型信息被擦除。
import java.util.ArrayList;
public class CastDemo {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
ArrayList<String> c1 = new ArrayList<String>();
ArrayList<Integer> c2=new ArrayList<Integer>();
//System.out.println(c1.getClass()==c2.getClass());
}
}
泛型思考:参数泛型化与原始类型的兼容性?
<1>参数化类型可以引用一个原始类型的对象,编译器报告警告,例如:Collection<String> c = new Vector();
<2>原始类型可以引用一个参数化类型对象,编译器会发出警告,例如:Collection c = new Vector<String>();
参数化类型不考虑类型参数的继承关系
<1> Vector<String> v = new Vector<Object>(); 错误
<2> Vector<Object> v = new Vector<String>(); 错误
为什么:假如<1>正确的话,那么我从V中取出的是String对象,那么这个对象却是指向Object类型,这样不是矛盾吗?
思考题:下面代码会报错吗?
Vector v2 = new Vector<String>();
Vector<Object> v3=v2;
答案是不会,但是编译器会发出警告,v3可以放入任何类型的对象。
思考题:如何向一个Integer泛型中的ArrayList中添加String元素?答案是运用反射技巧!
2.深入泛型
除了在集合中使用泛型外。我们也可以允许在定义类,接口时指定类型形参,这个类型形参将在声明变量,创建对象时确定。
(1) 深入泛型之通配符(类和接口)
使用?通配符可以引用其他各种参数化的类型,?通配符定义的变量主要做引用,可以调用与参数化无关的方法,不能调用与参数化有关的方法。
当我们使用一个泛型类时,应该为这个泛型类传入一个类型实参,如果没有传入类型实参,就会引起泛型警告。假设现在需要定义一个方法,该方法有一个集合形参,及和形参是不确定的,那我们怎么样定义?
如以下所示:
public void(List<?> c){
for(int i=0;i<c.size();i+++){
System.out.println(c.get(i));
}
}
但是这种带通配符的List仅表示他是各种泛型List的父类,并不能把元素加入其中,因为我们还是不知道上面程序中c集合的类型,所以不能往里添加对象。
<1>设定类型通配符的上限
当我们直接使用?这种形式时,即表明List集合可以是任何泛型类型的父类,但是还有一种特殊的情形,我们不想这个List<?>是任何类型的父类,只想表示他是某一泛型的父类。,实际上我们需要一种泛型表示方法,为了满足这种需要,Java泛型提供了被限制的泛型通配符。被限制的泛型通配符如下表示:
List<? extends T>
(2) 泛型方法
Java中的泛型方法很像C++中的方法模板,但是不如C++中的功能强大,而且Java这种相似性仅仅存在于表面,Java语言中的泛型仅仅是在编译器层面实现的,用于编译器执行类型检查和判断,然后生成非泛型的字节码,这种技术称为擦除。如果想实现完全类似于C++中如此强大的功能,就要重现修改JVM,这对于SUN公司来说是非常难以逾越的障碍。
例如:public void print(Vector<String>)和public void print(Vector<Integer>)这两种方法会被认为是一种方法,不可认为是重载,编译器会报错。
定义一个泛型方法:
修饰符<T,S>返回值类型 方法名(参数列表){
方法体....
}
举例:
public static<T> void add(T x,T y){
}
public static <T> void swap(T[] a,int x,int y){
T tmp = a[x];
a[x]=a[y];
a[y]=tmp;
}
关于泛型方法的几个要点:
1.用于纺织泛型的类型参数的尖括号影出现在方法返回值之前和修饰符之后。按照惯例,泛型字母要用大写。
2.只用引用类型才能用作泛型方法的参数,例如swap(new int[ ] )编译器会报错。
3.在应用和定义泛型方法时都可以使用extends关键字,并且可以用来指定多个边界。
4.普通方法,静态方法,构造方法都可以使用泛型。编译器也不允许创建泛型类型数组。
5.也可以在类型变量中使用异常,称为参数化异常,可以用throws 抛出,但是不能用在try catch中
6.在泛型中可以同时有多个类型参数,在定义他们的时候用逗号分开。
(3) 泛型方法与类型通配符的区别
多数时候我们都可以使用泛型方法来代替类型通配符,例如对于JDK中的Collection接口中的两个方法定义:
public interface Collection<E>{
boolean containAll(Collection<?> c);
boolean addAll(Collection<?> c);
}
上面两个方法形参都采用了了类型通配符的形式,也可以采用泛型方法来代替它,如下所示:
public interface Collection<E>{
boolean <T> containsAll(Collection<T> c);
bollean <T eatends E> addAll(Collection<T> c);
}
类型通配符与显示声明类型形参还有一个显著的区别:类型通配符即可在方法签名中定义形参的类型,也可以用于定义变量类型。但泛型方法中类型形参必须在对应方法中显示声明。
5.泛型与数组
JDK1.5有一个很重要的设计原则:如果一段代码在编译时,系统没有产生“未经检查的转换的警告,则程序不会引发”ClassCastException“异常。正式基于这个原因,所以数组元素的类型不能包含类型变量或者是类型形参,除非是无上限的类型通配符。但可以这样声明数组,即使声明元素类型包含类型变量或者类型形参的数组。也就是说:只能声明List<String>[ ]形式的数组,但是不能创建ArrayList <String>[10]这样的数组对象。总之Java不支持创建泛型数组。
例如这种代码是不被允许的:List<String>[] a = new List<String>[10];
但是Java允许创建无上限的通配符泛型数组,例如new ArrayList<?>[10],这样是可以通过编译的因为这种情况下Java会进行强制类型转换。
与此类似的是:创建元素类型是类型变量的数组对象也将导致编译错误,如下代码所示:
<T> T[] makeArray(Collection<T> coll){
return new T[coll.size()];
}
6.自定义泛型类的应用
如果类的对象中多处都要用到一个泛型函数,这些地方引用的泛型要保持同一个实际类型,这时候要采用泛型类型的方式进行定义,也就是类级别的泛型,语法格式如下:
public class GenericDao<T>{
private T field1;
public void save(T obj);
public T getById(int id);
}
类级别的泛型是根据引用该类别名时指定的类型信息来参数化类型变量,例如如下两种格式都可以:
GenericDao<String> g =null;
new GenericDao<String>();
注意要点:
<1>在对泛型类型进行参数化时,类型的参数的实例必须是引用类型,而不是基本类型
<2> 当一个变量被声明为泛型时,只能被实例变量和方法和方法调用,而不能是静态变量和静态方法调用。因为静态成员是被所有实例共享的
问题:类中只有一个方法需要使用泛型,而是使用类别的泛型还是方法级别的泛型?
当然是类级别的泛型
全新知识点:如何通过反射获得集合的实际类型?
核心思想:Method有一个方法叫做getGenericParameterTypes(),那么要通过一个方法才能得到,示例如下:
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Date;
import java.util.Vector;
public class GetType {
/**
* 获得泛型类型
*/
// 定义一个与集合有关的方法
public static void apply(Vector<Date> v){
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Method method;
try {
method = GetType.class.getMethod("apply", Vector.class);
Type[] types = method.getGenericParameterTypes();
ParameterizedType pt = (ParameterizedType) types[0];
//获得apply方法的原始类型
System.out.println(pt.getRawType());
System.out.println(pt.getActualTypeArguments()[0]);
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchMethodException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
学习总结:1.学习了如何定义泛型接口,泛型类,以及如何从泛型类中派生出子类
2.深入了解了泛型的实质
3.学习了类型通配符的用法,包括设定了类型通配符的上限,下限等
4.学习了Java不支持泛型数组,并深入分析其原因
5.泛型在JSP开发中有非常广阔的用处,JSP中经常要对结合迭代。
课后练习:1.定义一个泛型方法,自动将Object类型对象转换成其他类型
2.定义一个泛型方法,将指定类型填充进入数组
//将任意类型转换为其他类型
public static <T> T autoConvert(Object obj){
return (T)obj;
}
//向数组中填充进入指定类型
public static<T> void fillArray(T[] a,T obj){
for(int i=0;i<a.length;i++){
a[i]=obj;
}
}