在JDK1.5 以前,当你把数据存入到集合的时候,需要主观去判断类型是否合法,假如你明明知道你定义的是String类型,而保存的时候你存了一个Integer类型的数据,编译是可以成功的,但是运行的时候就可能导致类型转换异常ClassCastException,这是严重的安全隐患,而在1.5之后,引入了泛型并且使用了泛型之后,这个隐患在编译时期就被暴露出来,便于开发者去避免这种隐患,也避免了进行强制转换。可以把泛型理解为是java的一种安全机制,当然除了这个功能的话,泛型让程序变得更加通用灵活,同时也带来了对象的另一种”多态” 。尤其是在集合的操作中更为常见。
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。当在尖括号中使用统配符、泛型限定的时候有点类似JavaScript的“晚绑定”(指的是编译器或解释程序在运行前,不需要明确知道对象的类型)
在定义带类型参数的类时,在紧跟类命之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用,号分隔。定义完类型参数后,可以在定义位置之后的类的几乎任意地方(静态块,静态属性,静态方法除外)使用类型参数,就像使用普通的类型一样。注意:父类定义的类型参数不能被子类继承。
public class GenericClass<T, S extends T,...> {
....
}
从上文,我们已经对泛型有了个大概的了解了吧,但是为何使用泛型和如何使用泛型还是不是很明确吧,首先通过一个不使用泛型的例子和使用泛型的例子来说明,需求如下:假设你有一个工具类,专门用于操作其他实体类(内部细节实现忽略)
class Payout{
...
}
class Income{
...
}
//未使用泛型工具类
class Utils{
private Object obj;
public void setObject(Object obj){
this.obj=obj;
}
public Object get(){
return obj;
}
}
使用Utils类操作实体类,首先我们new 一个Utils,然后使用Payout初始化,调用getObject方法获取对象,再强制转换为Income类型,
public static void main(String args[]){
Utils t=new Utils();
t.setObject(new Payout());
Income in=(Income)t.getObject();//明显产生强制转换异常,但是编译通过了,因为没有用泛型
}
虽然这样子语法是没有错的,所以编译能够顺利通过,但是运行的时候在我们强制转换的时候引发了异常,这是很危险的,相反如果我们使用泛型来就能完美避免这种隐患,把工具类改为
//使用泛型定义工作类,此时工具类的类型为UtilsGeneric
class UtilsGeneric{
private T t;
public void setObject(T t){
this.t=t;
}
public void getObject(){
return this.t;
}
}
/*泛型用法,将类型转换出现的问题暴露在编译期,注意泛型类定义的泛型在整个类中有效,如果被方法使用,那么泛型的对象明确要操作的具体类型后,所有要操作类型已经固定了,但是每一个方法都要操作同一类型的数据一定程度上降低了泛型的灵活性
*/
public void static main(String[] args){
UtilsGeneric st=new UtilsGeneric();
st.setObject(new Payout());
//Income stu=st.getObject();编译失败
Payout stu=st.getObject();
}
使用了泛型类之后,在定义工具类的时候已经明确指明了,将要操作的类型为Payout。
在定义带类型参数的方法时,在紧跟可见范围修饰(例如public)之后的<>内,指定一个或多个类型参数的名字,同时也可以对类型参数的取值范围进行限定,多个类型参数之间用,号分隔。 定义完类型参数后,可以在定义位置之后的方法的任意地方使用类型参数
public <T, S extends T,...> T GenericFunction(T t, S s){
...
}
泛型方法的应用:
//定义泛型方法
class GenericMethod{
//即使把两个都写成T也没有关系,因为两者没有联系
public void show(T t){
System.out.println(t);
}
public void prin(T t){
System.out.println(t);
}
}
使用泛型方法
public static void main(String[] args){
GenericMethod m=new GenericMethod();
m.show("xianshiyong zifuchuan ");
m.show(new Integer(8));
d.prin(new Integer(9));
d.prin("caozuo zifuchuan");
}
泛型类中存在泛型方法:泛型类中的T是在建立对象的时候才明确类型的,若存在静态方法,静态方法不可以访问类上定义的泛型,如果静态方法操作的应用数据类型不确定,可以将泛型定义在方法上。
//泛型类中使用泛型方法和静态泛型方法
class GenericClassMethod{
//普通方法
public void show(T t){
System.out.println(t);
}
//泛型方法
public void prin(E e){
System.out.println(e);
}
//编译失败,报“无法从静态上下文中引用非静态类T”错误,因为静态先于对象存在。
/*public static void method(T t){
System.out.println(t);
}*/
//静态泛型方法
public static void method(W t){
System.out.println(t);
}
}
使用泛型类和泛型方法
//泛型方法
public static void main(){
GenericClassMethod m=new GenericClassMethod();
m.show("xianshiyong zifuchuan ");
//m.show(new Integer(8));错误,因为已指定了String类型
d.prin("caozuo zifuchuan");
d.prin(new Integer(9));
GenericClassMethod.method("静态泛型方法");
}
泛型接口与泛型类的语法没有多大的区别
//定义泛型接口
Interface IConnet{
void show(T t);
}
//实现泛型接口,已知T类型的,可以在实现的时候指定类型
class ConnectImp implements IConnet<String>{
public void show(String t){
...
}
}
//实现泛型接口,当你实现时你也还未确定数据类型时,依然可以引用泛型类型T
class ConnectImp2 implements IConnet<T>{
public void show(T t){
...
}
}
使用泛型接口
public static void main(String args[]){
ConnectImp imp=new ConnectImp();
imp.show("sddd");//只能传入类型为String的
ConnectImp2 imp=new ConnectImp2();
imp.show(9);//传入的数据类型取决于定义时候的<>
}
我们还可以把泛型参数(即尖括号里的类型)理解成方法参数,当我们的对象类型不明确时可以用一个通配符”?”来表示,通配符代表任意类型,相当于是一个占位符,执行的时候回被具体的类型所替代。看一个例子,加入我们想实现一个能够遍历所有类型的ArrayList的通用方法,由于我们实现前并不知道到底有多少类型我们需要打印,而使用Object又可能导致ClassCastException,此时通配符的优势显露无遗,
//打印任意类型的ArrayList对象
public static void printAll(ArrayList> al){
Iterator> it=al.iterator();//因为ArrayList的类型未定,所以迭代器的类型与ArrayList一直也是未定
while(it.hasNext()){
System.out.println(it.next());
//System.out.println(it.next().length());错误的,因为不是所有的类型都拥有这个方法,很明显假如?要被Integer替换Integer就不具有length方法
}
}
//当然不使用通配符也能实现
public static void printAll(ArrayList al){
Iterator it=al.iterator();
while(it.hasNext()){
T t=it.next();//与通配符最大的区别,因为T代表具体的某个类型,而通配符不是代表具体的类型,所以不能操作。
s.o.p(it.next());
}
}
使用通配符
public static void main(String [] args){
ArrayList al=new ArrayList();
al.add("sdd");
al.add("dfd");
ArrayList al2=new ArrayList();
al2.add(9);
al2.add(8);
printAll(al);
printAll(al2);
}
由于使用了通配符,无论输出的是String类型还是Integer类型的ArrayList,我们在实现通用遍历方法时的时候都不需要去关注,是不是有点像SQL里的占位符?
泛型限定语法和泛型类、泛型接口没有什么不同,泛型限定只是给泛型参数限定了一些范围——上限和下限。
从上面可以看到使用通配符就能完美解决,通用遍历简单类型的集合,但是我们要保存的是对象呢?要遍历的对象之间又存在继承关系呢?很明显通配符当然可以,但是并不是所有类型的数据我们都想要的,我们想只要有关系的类型,请看一个例子
把Person和Student对象保存到ArrayList并使用通用方法遍历
class Person{
private String name;
Person(String name){
this.name=name;
}
public String getName(){
return name;
}
}
class Student extends Person{
public Student(String name){
super(name);
}
}
未使用泛型限定实现的
public static void prinColl(ArrayList pl){
Iterator it=pl.iterator();
while(it.hasNext()){
T t=it.next();
s.o.p(it.next().getName());
}
}
测试遍历
ArrayList pl=new ArrayList();
pl.add(new Person("df"));
pl.add(new Person("er"));
pl.add(new Person("am"));
prinColl(pl);//可以成功遍历
//此时再存入Student时,再调用printAll时就报错
ArrayList sl=new ArrayList();
sl.add(new Student("son"));
sl.add(new Student("son2"));
sl.add(new Student("son3"));
//prinColl(pl);//编译失败,出错的原因,其实质是ArrayList al=new ArrayList()这是不允许的,因为声明集合的是时候要往里面存的是Person,如果有教师类也继承Person的话,是存不进来的,因为new 的时候只存Student,这就产生了隐患是不安全的。所以编译失败,解决方法是左右两边的类型必须一致
于是乎泛型限定的优势就显露无疑了,我们不需要改动调用代码,只需要稍微改动下遍历方法即可
//指定类型为可以接收Person类型或Person的子类,即向上限定,上限
public static void prinColl(ArrayList<?extends Person> pl){
Iterator<?extends Person> it=pl.iterator();
while(it.hasNext()){
T t=it.next();
s.o.p(it.next().getName());
}
}