------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
1. 概述
1.1 问题的引出
我们在前面的内容中主要介绍了ArrayList、LinkedList、HashSet和TreeSet集合类及其使用方法、特点及底层数据结构等内容。下面我们主要以ArrayList为例,通过下面的代码来引出本篇博客的主题——泛型。这里需要声明一点,泛型应用并不仅限于集合框架中,这里只是以集合类为例进行说明。代码1:
import java.util.*;将中间两行代码注释掉以后的运行结果为:
class GenericDemo
{
public static void main(String[] args)
{
ArrayList al = new ArrayList();
//先存储若干字符串对象
al.add("Sun");
al.add("Mountain");
al.add("Restaurant");
/*
//再存储两个Integer对象
al.add(new Integer(56));
al.add(1010);//自动装箱
*/
for(ListIterator li = al.listIterator(); li.hasNext(); )
{
String str = (String)li.next();
//打印字符串本身,以及字符串的长度
System.out.println(str+" : "+str.length());
}
}
}
Sun : 3
Mountain : 8
Restaurant : 10
但是将注释符号去掉以后,虽然编译成功,但是运行时,却抛出了异常,提示如下:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.StringatGenericDemo.main(GenericDemo.java:23)
异常提示告诉我们:Integer类型对象无法转换为String类型。这是显然的,因为两种类型之间如果要进行转换,必须具有继承或实现关系,而Integer类与String类之间并不具备这样的关系。
那么上述例子所反映的问题是:能否向某一个集合中存储多种类型的对象?通常情况下这种做法是不合理的。这是因为,我们将一些对象存储到集合中时,在底层会将每个元素均向上转型为Object类型对象,因此我们在取出元素时需要进行向下转型,并对这些元素进行一些针对性地操作。而这里所说的针对性的操作,就比如像代码1中那样获取字符串长度等。而如果向集合中存储了多种类型的对象,我们就无法将元素准确的转型为其原来的类型。
因此为了避免出现ClassCastException——类型转换异常——最安全的做法就是向一个集合中只存储某一种类型的对象。而在1.5版本以前,通常程序员都是主观判断集合的类型问题,那么为了提高开发效率,在1.5版本以后增加了泛型机制,让编译器帮助我们检查此类问题,以此在提高安全性的同时减少了程序员的工作负担。我们之前也提到过,JDK版本的升级一般有三种情况:简化书写、提高效率与提高安全性,那么这一改进就是为了提高安全性。
1.2 问题的解决
那么泛型机制是如何解决集合的类型问题的呢?我们可以参考数组的定义方式。阅读下面的代码,代码2:
class GenericDemo2{编译时抛出了异常:
public static void main(String[] args)
{
int[] arr = new int[3];
arr[0] = 37;
arr[1] = 34.5;
}
}
GenericDemo2.java:8: 错误: 不兼容的类型: 从double转换到int可能会有损失
arr[1] = 34.5;
^
1 个错误
那么这个异常的产生是显然的,因为我们在定义arr数组的时候,就已经明确其类型为int型数组,换句话说,我们不能向该数组存储除int以外的其他类型的变量,在编译期间就排除了类型问题。而泛型机制的基本原理与数组类似,就是在定义集合时就明确存储元素的类型,也是在编译时期,从语法角度,杜绝了类型问题。
1.3 泛型的定义格式
我们以代码1中ArrayList集合对象为例,如果我们希望向集合中存储String类型的对象,其定义格式为:ArrayList<String> al = new ArrayList<String>();
集合类名后的尖括号内,定义的就是元素的类型。上述代码的意思是:定义一个ArrayList集合,而该集合只能存储String类型的对象。那么我们对代码1进行如上修改,
代码3:
import java.util.*;此时,编译时期就抛出了异常:
class GenericDemo3
{
public static void main(String[] args)
{
//泛型
ArrayList<String> al = new ArrayList<String>();
//先存储若干字符串对象
al.add("Sun");
al.add("Mountain");
al.add("Restaurant");
//再存储两个Integer对象
al.add(new Integer(56));
for(ListIterator li = al.listIterator(); li.hasNext(); )
{
String str = (String)li.next();
System.out.println(str+" : "+str.length());
}
}
}
GenericDemo3.java:17: 错误: 对于add(Integer), 找不到合适的方法
al.add(new Integer(56));
^
方法Collection.add(String)不适用
(参数不匹配; Integer无法转换为String)
方法List.add(String)不适用
(参数不匹配; Integer无法转换为String)
方法AbstractCollection.add(String)不适用
(参数不匹配; Integer无法转换为String)
方法AbstractList.add(String)不适用
(参数不匹配; Integer无法转换为String)
方法ArrayList.add(String)不适用
(参数不匹配; Integer无法转换为String)
注: 某些消息已经过简化; 请使用 -Xdiags:verbose重新编译以获得完整输出
1 个错误
此时就将集合的类型问题,从运行时期转移到了编译时期,那么编译失败,就需要修改代码,就可以保证在运行期间不会再出现此类问题,从而避免了安全问题。既然代码3中我们限定了ArrayList集合只能存储字符串对象,那么在通过for循环获取元素时是不是不需要,进行向下转型动作了呢?就像下面的代码所示,
代码4:
import java.util.*;编译器件依然会报出错误:
class GenericDemo4
{
public static void main(String[] args)
{
//泛型
ArrayList<String> al = new ArrayList<String>();
//先存储若干字符串对象
al.add("Sun");
al.add("Mountain");
al.add("Restaurant");
for(ListIterator li = al.listIterator(); li.hasNext(); )
{
//去掉向下转型的动作
String str = li.next();
System.out.println(str+" : "+str.length());
}
}
}
GenericDemo4.java:18: 错误: 不兼容的类型
String str = li.next();
^
需要: String
找到: Object
1 个错误
实际上,上述这个问题是出在了Iterator迭代器上。迭代器作为操作集合中元素的工具,也应对其可操作的对象类型进行限定——只能操作其所属集合的可操作引用数据类型。那么同理为迭代器对象添加泛型,修改后的最终代码为,
代码5:
import java.util.*;运行结果与代码1相同:
class GenericDemo5
{
public static void main(String[] args)
{
//迭代器也定义泛型
ArrayList<String> al = new ArrayList<String>();
//存储若干字符串对象
al.add("Sun");
al.add("Mountain");
al.add("Restaurant");
for(ListIterator<String> li = al.listIterator(); li.hasNext(); )
{
String str = li.next();
System.out.println(str+" : "+str.length());
}
}
}
Sun : 3
Mountain : 8
Restaurant : 10
如果大家自行尝试运行上述代码就会发现,不仅没再出现编译时期的错误提示,并且也没有出现如下警告:
注: GenericDemo5.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked重新编译。
这是因为,上述代码已经不存在类型安全问题,因此编译器不再作出警告。
1.4 总结
那么泛型机制的优点可总结为以下两点:(1) 将运行时期出现的ClassCastException,转移到了编译时期,方便程序员解决问题,减少运行时可能出现的安全问题。
(2) 避免了进行强制类型转换,简化代码的书写。
2. 理解泛型
2.1 泛型术语简介
在介绍泛型的应用以前,为帮助大家理解,我们首先说一说泛型技术中常用的一些术语,这里以ArrayList为例:(1) 将泛型定义在类上时,比如ArrayList<E>,就称之为泛型类型,其中的E称为类型变量或类型参数;
(2) 若指定了具体的泛型类型,比如ArrayList<Integer>,则称为参数化的类型(Parametered Type),其中的Integer称为实际类型参数或类型参数的实例;
(3) ArrayList<Integer>用英语读作:“ArrayList typeof Integer”;
(4) 未定义任何泛型的ArrayList称为原始类型。
2.2 泛型的应用场景
既然泛型机制具有可以提高代码的安全性以及简化代码书写的优点,那么应该在什么情况下使用泛型呢?通常情况下,集合框架中的类,尤其是集合类,都会使用泛型机制。比如,Collection接口的API文档中,Collection接口名后接一对尖括号,其内包含一个大写字母E,这也是泛型机制的体现。可能有朋友会问,既然尖括号内需要定义引用数据类型,那么E又是什么类型呢?实际上这里的E是一种泛指,在定义这一接口的时候,它并没有限定该接口所能操作的引用数据类型,而是需要在程序员创建Collection实现类对象时再去告诉编译器具体将要操作的引用数据类型是什么,它可以是String、Integer、Double或者自定义类型。大家可以这样类比地理解泛型,当我们调用某个方法时,需要向其传递某个参数,这个参数可以是对象也可以是基本数据类型变量,那么泛型也好比是传递参数,只不过这个参数是引用数据类型名称而已。再比如,大家去查阅ArrayList类的API文档,类名后同样有一对尖括号,内含一个字母E,而在方法摘要中add方法的参数列表的变量类型也是E,这也就说明了,当我们去创建一个ArrayList对象时,尖括号中声明了什么类型,该集合对象的部分方法(参数列表中变量类型为E的方法)所操作的变量就是什么类型。所以,我们将这个E称为类型变量,或者类型参数 ,由它来接收传递的具体引用数据类型。
说到这,有的朋友可能会感到有些困惑,API文档中类名后有一对尖括号用于声明该类可操作的引用数据类型,而在前述代码5中,创建ArrayList对象时也在类名后添加一对尖括号,这两种泛型的使用有什么区别和联系呢?实际上API文档是通过javadoc程序,以html文件的形式呈现类的各种信息(包括成员方法、字段、子父类等等),因此API文档就相当于是我们定义的类。我们以集合类为例,假设我们现在需要自己开发一种集合类A,那么在定义A类的时候,我们事先是不能知道,使用这个类对象的程序员会向这个集合存储什么类型的对象,因此就在类名后用E去泛指各种类型。而当程序员创建A类对象时,通过类名后的尖括号声明A类集合所存储元素的具体类型以后,才能在运行时最终确定。那么这就是定义类时的泛型声明和创建对象时的泛型声明之间的关系。
2.3 泛型机制原理简述
既然定义泛型可以将运行时期可能发生的错误,转移到编译时期,由编译器提醒我们类型错误,那么实际上定义的泛型就是提供给编译器使用的。编译器从语法的角度,挡住程序员向集合中存储非法类型的元素,以此来提高代码的安全性。但是,编译器在完成编译以后,就会自动去掉集合的“类型信息”,也就是泛型类型,使程序的运行效率不受影响。比如以下两行代码
ArrayList<String> al1 = new ArrayList<String>();
ArrayList<Integer> al2 = new ArrayList<Integer>();
虽然,两个ArrayList对象定义了不同的泛型类型,但是在完成编译以后,由于编译器将两者的类型信息去掉了,因此它们在内存中的字节码对象(Class对象)是完全相同的,如下代码所示,
代码6:
import java.lang.reflect.Constructor;import java.util.ArrayList;执行结果为:
public class GenericDemo6 {
public static void main(String[] args) throws Exception {
ArrayList<String> al1 = new ArrayList<String>();
ArrayList<Integer> al2 = new ArrayList<Integer>();
System.out.println(al1.getClass().equals(al2.getClass()));
}
}
true
执行结果表明,al1和al2指向的两个对象,又同时对应了内存中同一份字节码,因为一个类的“.class”文件只能被加载一次。由此可知,同一种集合类对象尽管定义了不同的泛型类型,但是由于在编译期间编译器去掉了类型信息,因此加载到内存中的字节码对象均是原始字节码对象(即没有定义任何泛型的字节码对象)。那么这种在编译期间去掉类型信息的动作,称为去类型化。我们可以利用去类型化的特点,利用反射技术,向定义有泛型的集合中存储非泛型类型的对象,这一部分演示请参考“3.5节泛型在反射中的应用”。
上面啰嗦了这么多,简单总结一下:
(1) 定义类时,尖括号用于接收类型,对外声明该类可能将会操作某个类型的变量,但定义该类时并不确定;
(2) 创建对象时,尖括号用于传递类型,用于在运行时最终确定该对象将操作哪种类型的对象;
(3) 由于编译器去类型化的特点,在源代码中定义了泛型的对象,在运行期间加载到内存中的字节码对象就是原始类型,并不会因为定义了泛型而有所不同。
最后需要强调的是,尖括号中只能传递引用数据类型,而不能传递基本数据类型。
2.4 泛型类型的兼容性
(1) 参数化类型可以引用一个原始类型的对象
比如,代码“Collection<String> coll = new Vector();”,是可以通过编译并执行的,只不过会报出编译时警告。假设,当前的编译器版本和JRE版本均为1.5,然后在源代码中调用了一个JDK1.4版本的一个方法,该方法会返回一个集合。由于JDK1.4版本时还没有引入泛型机制,因此该方法返回的集合中也肯定没有定义泛型。那么为了兼顾调用JDK1.4版本方法的需求,以及泛型机制优势的发挥,参数类型时可以引用一个原始类型的对象的。如果没有这样的兼容性,那么为了泛型机制的应用,JDK1.4版本以前的方法就都无法使用了。(2) 原始类型引用可以指向一个参数化类型对象
比如,代码“Collection coll = new Vector<String>();”,同样可以通过编译并执行,但也会报出编译时警告。假设,需要调用JDK1.4版本以前的一个方法,该方法的参数类型中需要传递一个集合对象,比如这样的一个方法“public void fun(Collection coll)”,显然集合的引用中并没有定义泛型。当我们在1.5版本的编译环境下,就可以在调用该方法的同时传递一个参数化类型集合对象。(3) 参数化类型不考虑类型参数间的继承关系
阅读下面两行代码,Vector<String> vec = new Vector<Object>();
Vector<Object> vec = new Vector<String>();
以上两行代码都是不能通过编译的。从语法角度来说,参数化类型中定义了哪种类型的参数,那么该参数化类型对象就只能操作哪种类型。拿第一行代码为例,集合引用中明确规定只能存储String类型对象,但是实际指向的集合对象中存储可能有各种类型对象,因此无论从语法角度,还是安全角度都是错误的。当然,依据泛型类型的兼容性,如果赋值符号右侧的对象上没有定义任何泛型时可以的。
(4) 数组不能定义参数化类型
在创建数组实例时,数组的元素不能使用参数化的类型,比如,阅读下面的代码,Vector<Integer>[] vectorList = new Vector<Integer>[10];
以上代码是不能通过编译并执行的。关于这个问题,张孝祥老师并没有给出解答,因此我在网上找到两篇博客,地址如下:
http://www.blogjava.net/sean/archive/2005/08/09/9630.html
http://www.blogjava.net/deepnighttwo/articles/298426.html
这两篇博客的作者根据自己的分析给出了解释,大家可以参考一下。主要的意思,就是可以使用一个Object类型数组的引用——比如Object[] objs——指向上述Vector<Integer>类型数组。然后,通过objs引用向其中添加其他类型的元素。最终使用Vector<Integer>类型的引用vector变量指向从objs取出的元素,接着使用一个Integer类型的引用变量指向,从vector取出的元素,但此时由于vector实际指向的对象并非是Vector<Integer>,因此会抛出ClassCastException。因此为了避免类似的类型装换异常,而禁止了参数化类型数组。
关于这个知识点大家只要记住就可以了,有兴趣的朋友可以看看上面两个博客。
小知识点1:
阅读以下代码,思考能够通过编译并执行:
Vector vec = new Vector<String>();
Vector<Object> vec2 = vec;
如果进行测试就会发现,以上两行代码既可以通过编译,又能成功执行。编译器的运作原理,是一行一行的对源代码进行编译,并且不考虑代码的实际执行效果,只要每一行的代码定义符合语法规则即可。第一行代码根据泛型类型的兼容性——原始类型引用是可以指向一个参数化类型的对象的,因此没有任何问题。可能有朋友认为第二行代码的实际作用是将实际参数类型为Object的Vector引用指向实际参数类型为String的Vector对象,由于泛型类型之间不存在继承关系,因此会编译失败。但是单看第二行代码的语法是没有任何问题的,因为其编译阶段的含义其实是参数化类型引用指向原始类型对象。
3. 泛型的应用
在前面的内容中,我们主要是直接使用类库定义好的泛型类或泛型接口,下面我们主要介绍如何根据实际需要自己定义泛型类或泛型接口。3.1 简单应用
下面的代码来是对泛型机制的一个简单应用。需求:将存储到TreeSet集合中的字符串按照长度,从短到长进行排序。
代码7:
import java.util.*;运行结果为:
//定义一个比较器,令存储到TreeSet集合中的字符串按长度从短到长排序
class LenComparator implements Comparator
{
public int compare(Object o1, Object o2)
{
//强制向下转型,便于调用String对象的特有方法
String str1 = (String)o1;
String str2 = (String)o2;
int value = new Integer(str1.length()).compareTo(new Integer(str2.length()));
if(value == 0)
return str1.compareTo(str2);
return value;
}
}
class GenericDemo7
{
public static void main(String[] args)
{
TreeSet<String> ts = new TreeSet<String>(new LenComparator());
ts.add("Car");
ts.add("People");
ts.add("Today");
ts.add("February");
ts.add("Enthusiastic");
for(Iterator<String > it = ts.iterator(); it.hasNext(); )
{
String str = it.next();
System.out.println(str);
}
}
}
Car
Today
People
February
Enthusiastic
结果表明,代码6可以实现将字符串按照长度排序的功能。虽然我们在创建TreeSet集合对象的时候声明了,该集合将专门用于存储String对象,但是在运行代码6时依然提示泛型警告。问题出现在了定义Comparator实现类时并没有定义泛型。查阅Comparator接口的API文档,接口名后的一对尖括号表明,该接口同样需要应用泛型机制,定义自定义比较器(LenComparator)时需要指定该比较器专门用于比较什么类型的变量,由此我们对代码6作出如下修改,
代码8:这里仅体现比较器LenComparator的代码
/* 通过泛型机制,对外声明,该自定义比较器专门用于比较String对象运行结果同样是:
那么compare方法参数列表中的参数类型,也需要相应的修改为String
*/
class LenComparator implements Comparator<String>
{
public int compare(String str1, String str2)
{
/*
由于参数列表中的参数类型已经声明为了String
不必再进行如下强转动作
*/
//String str1 = (String)o1;
//String str2 = (String)o2;
int value = new Integer(str1.length()).compareTo(new Integer(str2.length()));
if(value == 0)
return str1.compareTo(str2);
return value;
}
}
Car
Today
People
February
Enthusiastic
并且,没有任何泛型警告。该例程就同时体现了泛型机制,既能保证程序安全,又可以免去强转麻烦的优点。此外,在该例程中,如果希望按照字符串的长度从长到短进行排序,只需将上面代码中str1和str2的位置颠倒即可(包括if语句中的位置)。
这里我们需要补充说明三点:
首先,大家需要注意的是,我们在介绍HashSet集合时说到,如果需要向集合中存储自定义类型对象时,应该复写该类的hashCode和equals方法,我们需要强调的是,由于这里的equals方法复写的是Object类的方法,而Object并没有应用泛型,因此参数列表中的参数类型一定是Object,不能是其他类型,这一点一定要与compareTo和compare方法区别开来。
其次,那么我们从Comparator接口联想到Comparable接口是否也能应用泛型机制呢?查阅Comparable接口的API文档发现,该接口其实也使用了泛型机制,也就是说将来我们在自定义Comparable接口的实现类时,也应该对外声明该类专门用于比较什么类型的对象。
最后,到目前为止,如果我们要自定义一个类,应该尽可能的作三个复写动作:复写hashCode、equals以及实现Comparable接口,复写compareTo这三个方法。这样不仅可以将自定义类对象存储与List集合也可以存储于HashSet和TreeSet集合中,这样做更加便于后期功能的扩展。当然,我们也应尽可能的复写toString方法,便于通过打印结果观察程序的运行情况。
3.2 泛型类
无论是Comparable还是Comparator接口,它们都是Java类库中已经定义好的应用泛型机制的接口,因此我们也可以在实际开发中,针对自己的需求开发泛型类或泛型接口。那么我们就通过下面的例子,来介绍如何定义泛型类,以及泛型类给我们带来的便利之处。比如我们要定义一个工具类,专门用于操作一些对象,但是这些对象的类型是比较多样的,那么在泛型机制出现以前,通常会通过Object类作为媒介来实现上述功能,比如下面的代码,
代码9:
//定义两个类class Student{}上述代码可以编译成功并运行,但是如果不小心向Tool中存储的是Student对象,虽然可以通过编译,然而运行时将会抛出ClassCastException——类型转换异常。而通常情况下,我们希望这类问题能够出现在编译阶段而不是运行时期,因为实际开发时我们要面对的代码规模不会像代码8这样短小,一旦程序在运行时期出现问题,可能会牵扯很多方面,不便于修改。那么泛型机制的出现,将此类问题从运行时期转移到了编译时期,从语法角度根本上避免了这类主观因素导致的错误,同时免去了强转动作的麻烦,减轻了程序员负担。由此对代码8作出如下修改,
class Worker{}
//用于操作对象的工具类,因仅为演示,只定义set和get两个方法
class Tool
{
//在泛型机制出现以前,通过Object类作为媒介
private Object obj;
public void setObject(Object obj)
{
this.obj = obj;
}
public Object getObject()
{
returnobj;
}
}
class GenericDemo8
{
public static void main(String[] args)
{
Tool tool = new Tool();
tool.setObject(new Worker());
//获取对象时,需要进行强转动作
//这里就需要程序员在主观上,对强转类型进行判断
Worker worker = (Worker)tool.getObject();
}
}
代码10:
class Student{}class Worker{}//定义泛型,由调用者指定Tool对象将要操作的对象的类型//这里我们使用T来代表将来调用者指定的类型class Tool<T>{ private T t; //方法的实现与不使用泛型的情形是一样的 public void setObject(T t) { this.t = t; } public T getObject() { return t; }}class GenericDemo9 { public static void main(String[] args) { //指定需要操作的对象类型 Tool<Student> tool = new Tool<Student>(); tool.setObject(new Student()); //由于使用泛型,这里不再需要强转 Student stu = tool.getObject(); //也可以用于操作Worker对象 Tool<Worker> tool = new Tool<Worker>(); tool.setObject(new Worker()); Worker worker = tool.getObject(); //以下代码会报错 /* Tool<Student> tool = new Tool<Student>(); tool.setObject(new Worker()); Student stu = tool.getObject();*/ }}上述代码编译成功并可以正常运行。同样,如果我们向指定操作Student类型对象的Tool存储Worker对象,编译时期就会报出错误,无法通过编译,这样就将问题从运行阶段转移到了编译阶段,有利于更加高效的进行开发。
我们再举一个泛型类的例子。DAO(Data Access Object)是用来进行数据访问的类。所谓的数据访问其实就是对数据库进行的增删改查的操作——CRUD。那么被操作的数据对象可能是用户信息、产品信息、或者图书信息等等。当被操作的数据对象是某一类事物时,就可以为DAO对象指定一个类型参数,用来规定该DAO对象专门用于操作这一类事物。此时就可以利用泛型技术来实现这一需求。那么应用泛型技术的优点就在于,一方面只需定义一个DAO类,即可将其应用于对所有事物对象数据的操作;另一方面,我们在获取数据对象时,不必进行强制类型转换操作,非常方便。
我们通过下面的代码来演示,需要为DAO类定义哪些常用方法。
代码11:
import java.util.Set;public class GenericDao<T> { //增加对象 public void add(T obj){} //通过编号查询对象 public T findByID(int id){ return null; } //根据条件查询对象 public <T> Set<T> findByConditions(String con){ return null; } //删除对象 public void delete(T obj){} //通过编号删除对象 public void delete(int id){} //修改指定对象 public void update(T obj){} }以上代码中为DAO类定义了常规的数据操作方法:增加对象、删除对象、修改对象、查询对象等。那么在创建GenericDao对象的时候,需要指定具体的类型参数,来明确该Dao对象要操作哪种类型的数据。
其中实现删除、修改、查询等操作之前,首先都要在数据库中找到目标对象,那么对于对象的匹配既可以通过对象本身,也可以通过编号进行匹配,这可以按照需要进行不同的设定。
需要说明的,查询操作除了简单的查询单个对象以外,还可以通过指定查询条件,返回多个满足条件的对象,也即返回存有多个对象的集合。比如同一天生日的所有个人信息、汽车相关所有图书等等。该方法的参数就是字符串类型的查询条件,这里仅作了简单演示。
小知识点2:
利用DAO还可以定义用户登录系统。比如定义一个名为“findByUserName(String name)”的方法,则可以在数据库中查询是否存在用户指定的账号。其实在Web页面用户是需要同时输入账号和密码的,但是注意到上述方法的参数仅包含用户账号。这样做的好处是,首先通过账号名查询是否存在这样的账号记录,如果存在记录则再进一步比对密码是否相同。如果账号本身就不存在,也就没有匹配密码的必要了,这样可以提高查询效率,而且可以明确的告诉用户是账号还是密码错误。
对上述内容我们简单做个总结:当我们事先并不能知道某个自定义类将要操作的引用数据类型,并且该类的大部分方法都将操作该类型对象时,我们可以在类上进行泛型声明,以提高代码的灵活性和扩展性。那么泛型机制的使用,将可能发生的类型错误,控制在编译阶段,提高开发效率。
3.3 泛型方法
3.3.1 泛型方法的定义与调用
泛型不仅可以定义在类上,还可以定义到方法上,而这两种定义方式之间又有什么联系和区别呢?我们通过下面的例子引出泛型方法的应用。代码12:
//定义一个泛型类class Demo<T>运行结果为:
{
//打印指定类型对象的信息
public void show(T t)
{
System.out.println(t);
}
}
class GenericDemo10
{
public static void main(String[] args)
{
//操作String对象
Demo<String> d1 = new Demo<String>();
d1.show("Hello World!");
//操作Integer对象
Demo<Integer> d2 = new Demo<Integer>();
d2.show(10);//自动装箱
//以下为错误代码
//声明操作String对象,但试图操作Integer对象
//Demo<String> d3 = new Demo<String>();
//d3.show(50);
//声明操作Integer对象,但是试图操作String对象
//Demo<Integer> d4 = new Demo<Integer>();
//d4.show("Hello Java!");
}
}
Hello World!
10
上述代码可以通过编译,并正常运行。但是如果试图像错误代码那样操作,又会在编译时期报出类型错误,当然这种错误是显而易见的,但却暴露了泛型类的一个局限性——一旦创建了Demo对象,那么该对象所能操作的引用数据类型也就随之固定了,假设这个类型为T,那么Demo类所有参数类型为T的方法,将都不能操作除T以外的任何类型的对象。
那么更为灵活,更具有扩展性的做法是:只需要创建一个Demo对象,而且该Demo对象的某个方法可以操作任意引用数据类型的对象。而实现这一需求的方式就是将泛型定义在方法上。
泛型方法的定义格式如下,以show方法为例:
public <T> void show(T t)
关于泛型方法的定义格式需要强调三点:
(1) 泛型一定要定义在访问权限修饰符后(包括static),返回值类型前;
(2) 不同方法之间,泛型的类型变量名(如上所示的T)是可以相同的,因为这一变量仅在该方法内有效,方法之间不会相互影响。
(3) 同时定义泛型类和泛型方法是允许的,但要保证各自的类型变量名不同,因为泛型类的类型变量将作用于整个类。
我们通过下面的代码演示泛型方法的定义与使用方法,
代码13:
//不再将泛型定义在类上class Demo运行结果为:
{
//定义泛型方法
public<T> void show(T t)
{
System.out.println(t);
}
}
class GenericDemo11
{
public static void main(String[] args)
{
//只创建一个Demo对象
//无需在创建对象时,声明将要操作的引用数据类型
Demo d = new Demo();
/*
show方法不仅可以操作String对象,也可以操作Integer对象
并且,不必事先进行类型声明
*/
d.show("Hello World!");
d.show(54);//自动装箱
}
}
Hello World!
54
上述代码的正常编译与运行表明,泛型方法是可以操作各种类型的对象的,并且不必事先进行声明,也就是说,当我们不确定某个方法将会操作哪种类型的对象时,定义泛型方法将会非常方便。
当然泛型方法也不是万能的,上述代码中我们仅仅对String和Integer对象进行了简单的打印操作,而toString方法是所有对象都具备的方法,如果我们想在show方法中调用所传参数对象的特有方法,比如String类的length()方法时,将会报出如下错误,
GenericDemo10.java:7: 错误: 找不到符号
System.out.println(t+" : "+t.length());
^
符号: 方法 length()
位置: 类型为T的变量 t
其中, T是类型变量:
T扩展已在方法<T>show(T)中声明的Object
1 个错误
错误提示的意思是当我们使用泛型方法时,该方法将默认把所接受的参数对象向上转型为Object类型,那么Object类型当然没有子类的特有方法,因此报出错误也是正常的。而向上转型为Object类型对象的操作,正是泛型方法可以接收任何类型参数的原因所在。
因此,我们在泛型类和泛型方法之间做出选择也是要依据实际需求,如果我们事先知道某个类的所有(或者大部分)方法都将操作一种类型的对象,那么定义泛型类将更为方便;反之,如果我们事先无法知道某各类的某些方法将操作哪种类型的对象,并且仅仅对参数对象进行一些简单地操作,那么泛型方法将会更加灵活一些。
3.3.2 静态泛型方法
在上面的内容中我们涉及到的方法均是非静态泛型方法,而当泛型机制应用在静态方法上时会相应的发生一些变化。观察下面的代码12,代码14:
//定义泛型类class Demo<T>运行结果为:
{
//非静态方法可以使用泛型类定义的类型变量T
public void show(T t)
{
System.out.println(t);
}
//静态方法不可以使用泛型类定义的类型变量T
/*
public static void method(T t)
{
System.out.println(t);
}
*/
}
class GenericDemo12
{
public static void main(String[] args)
{
Demo<String> d = new Demo<String>();
d.show("Hello World!");
//d.method("Hello Java!");//错误代码
}
}
Hello World!
上述代码只有在注释掉定义和调用静态方法method那一部分代码以后才可以通过编译,并执行。当我们试图按照代码12中的方式定义静态方法时,会报出以下错误提示:
GenericDemo11.java:9: 错误: 无法从静态上下文中引用非静态类型变量 T
public static void method(T t)
^
1 个错误
上述错误提示想必大家非常熟悉,那么对于它的理解是:究竟method方法要操作什么类型的对象,只有在运行时期,创建Demo对象时才能确定,也就是说T是随着对象的创建而“创建”的,而这并不是静态变量的特点,因此静态方法method是无法访问到T类型变量t所指向的对象。
因此我们说:静态方法不可以访问泛型类的类型变量,如果静态方法操作的引用数据类型不确定,可以定义为静态泛型方法。定义方式如下,
代码15:
class Demo<T>{运行结果为:
//非静态方法,使用泛型类定义的类型变量
public void show(T t)
{
System.out.println(t);
}
//静态泛型方法
public static <E> void method(E e)
{
System.out.println(e);
}
}
class GenericDemo13
{
public static void main(String[] args)
{
Demo<String> d = new Demo<String>();
d.show("Hello World!");
Demo.method(24);//通过类名调用静态泛型方法
}
}
Hello World!
24
通过以上方式,就实现了静态方法与泛型机制的结合。
3.3.3 泛型方法的类型推断
以上介绍的泛型方法的参数列表中都只定义了一个参数,并且返回值类型也并没有使用类型参数。那么如果参数列表中的多个参数,以及返回值类型均使用类型参数来定义,并且在运行阶段,以上多个位置处类型参数的实际类型友都互不相同(参考代码15)时,类型参数的实际类型应该如何确定呢?此时就需要通过类型推断进行确定。这里我们首先给出张孝祥老师总结的有关泛型方法类型推断的五条规律:
(1) 当类型参数只是在参数列表中其中一个参数,或者返回值中应用了泛型方法的类型参数,那么该类型参数的实际类型就是调用方法时程序员指定的类型,并且由于仅有一处应用了类型参数而不需要进行类型推断。
(2) 当在泛型方法参数列表中的多个参数,以及返回值等多个位置处应用了类型参数时,如果调用方法时,以上多处位置的类型参数的实际类型均为同一种类型时,也不需要进行类型推断,类型参数实际类型就是这一类型。
(3) 当在泛型方法参数列表中的多个参数,以及返回值等多个位置处应用了类型参数时,如果调用方法时,以上多处位置的类型参数的实际类型不唯一,并且没有使用返回值,此时该类型参数的实际类型取多个实际类型的共同父类作为泛型方法类型参数的实际类型。
(4) 当在泛型方法参数列表中的多个参数,以及返回值等多个位置处应用了类型参数时,如果调用方法时,以上多处位置的类型参数的实际类型不唯一,并且使用了返回值,此时该类型参数的实际类型则以程序员指定的返回值类型为准。
(5) 参数类型的类型推断具有传递性。关于传递性我们将在后面的博客中进行详细介绍。
阅读下面的代码,
代码16:
import java.io.Serializable;以上代码中,定义了一个静态泛型方法,参数列表的参数类型,以及返回值类型均定义为了同一个泛型类型。将main方法的第四行代码注释后,即可通过编译经执行。由于没有定义输出语句,因此内没有显示任何执行结果。
public class GenericDemo14 {
public static void main(String[] args) {
Number result1 = fun(12, 3.14);
Comparable result2 = fun("Hello World! ", 123);
Serializable result3 = fun("David ", 21);
//String result4 = fun("Kate", 19);
fun("Peter", 31);
}
//定义一个泛型方法
private static <T> T fun(T a, T b){
return null;
}
}
代码说明:
(1) main方法的前三行代码,均使用了与实际参数类型完全不同的类型的引用来接收fun方法的返回值(实际为null)。比如,第一行代码在实际执行时,向fun方法传递的12和3.14分别通过自动装箱机制,转换为了Integer和Double对象,并且该方法的的返回值使用了Number类型的引用来接收,这就表示在运行阶段,泛型方法的类型参数的实际类型被指定为了三种不同的类型。那么类型参数的实际类型究竟应该是三种类型中的哪一种呢?首先根据类型推断的第三条原则,当参数列表中多个参数的实际类型不同时,类型参数的实际类型取多个类型的共同父类,那么可能的实际类型即为Object、Number、Comparable、Serializable等,因为它们都是Integer和Double的共同父类。那么再根据第四条原则,在调用fun方法时,返回值通过一个Number类型的引用接收,因此最终,类型参数的实际类型即为Number,而Number恰好是两者的诸多共同父类之一,因此能够通过编译。
(2) main方法中的第四行代码,如果去掉注释符,则无法通过编译,提示“Type mismatch : cannot convert from Object&Serializable&Comparable<?> to String”,意思是无法将Object、Serializable或者Comparable<?>类型转换为String类型。这是因为在第四行代码中,分别向fun方法传递了一个String和一个Integer对象,两者的共同父类有Object、Serializable以及Comparable等,因此根据第三条原则类型参数的实际类型可能为以上三种类型之一。但是,在调用fun方法时,却又使用了一个String类型的引用去接收返回值,根据第四条原则,fun方法类型参数的实际类型就只能是String类型,但这又不是Object、Serializable以及Comparable类型之一,因此出现了如上编译错误的提示。
(3) 第五行代码看似与第四行是一样的,但是为什么能够通过编译呢?这是因为这里并没有使用返回值,因此这里的类型推断仅使用第三条原则即可。
(4) 我们并没有为fun方法定义实际的方法体,比如,可以对形式参数a和b进行加法运算,然后将运算结果作为该方法的返回值。但是,并非所有类型都支持“+”运算,因此无法通过编译。
3.3.4 注意事项
需要向大家强调的是,泛型的类型参数只能是引用数据类型,而不能是基本数据类型,因此像“Collection<int> coll = new ArrayList<int>();”这样的代码是不能通过编译的。而即使像代码11-14中那样,向泛型方法中传递基本数据类型的参数,也会通过自动装箱机制,转换为与基本数据类型相对应的包装类对象的。再比如,阅读下面的代码,
代码17:
public class GenericDemo15 { public static void main(String[] args) {以上代码main方法中的第二行代码时不能通过编译的,因为泛型的类型参数只能是引用数据类型,因此不能传入整型类型的数组。从另一方面来说,整型数组已经是引用数据类型对象,因此也不存在对其进行自动装箱的操作了。
int[] arr = new int[]{12, 41, 55};
//swap(arr, 0, 2);
}
//用于交换数组中两个角标位上元素的泛型方法
private static <T> void swap(T[] arr, int i, int j){
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
3.4 泛型接口
顾名思义,就是应用了泛型机制的接口,就像前文提到的Comparable和Comparator接口那样,下面我们来介绍泛型接口的定义方式,以及两种使用方法。(1) 子类实现接口时,指定所操作的引用数据类型
代码18://定义一个泛型接口,定义方式与泛型类相同interface Inter<T>运行结果为:
{
public abstract void show(T t);
}
/*
定义泛型接口的实现类,指定该接口操作String类型对象
那么该实现类将同样操作String类对象
由于接口已经声明完毕,实现类就不必重复声明
*/
class InterImplement implements Inter<String>
{
public void show(String t)
{
System.out.println("show:"+t);
}
}
class GenericDemo16
{
public static void main(String[] args)
{
InterImplement ii = new InterImplement();
ii.show("Hello World!");
}
}
Hello World!
上述泛型接口的应用方式的特点是:定义实现类时,指定接口可操作的引用数据类型,那么实现类也就同样可以操作这一引用数据类型。
(2) 子类实现接口时,不指定接口可操作的引用数据类型
代码19://定义一个泛型接口interface Inter<T>运行结果为:
{
public abstract void show(T t);
}
/*
定义泛型接口的实现类,但并不指定该接口可操作的引用数据类型
因此若要在实现类上使用泛型机制,需将实现类定义为泛型类
*/
class InterImplement<T> implements Inter<T>
{
public void show(T t)
{
System.out.println("show:"+t);
}
}
class GenericDemo17
{
public static void main(String[] args)
{
//创建泛型类对象时,需指定该对象将要操作的引用数据类型
InterImplement<Integer> ii = new InterImplement<Integer>();
ii.show(56);
}
}
show:56
以上就是泛型接口的两种应用方式,不过在实际开发中很少需要我们自己定义泛型接口,更多的是使用别人定义好的接口。
3.5 泛型在反射中的应用
(1) 简单应用
比如我们通过String类的Class对象,通过反射技术获得了表示其构造方法的Constructor对象。在不定义泛型的情况下,当我们调用Constructor对象的newInstance方法,只能返回一个Object对象,我们必须进行强制类型转换才能得到String对象。而正像前文所述,定义泛型的好处之一就是避免了强制类型转换的麻烦。以下代码演示了,如何利用泛型,使用Constructor对象创建String对象,代码20:
import java.lang.reflect.Constructor;执行结果为:
public class GenericDemo18{
public static void main(String[] args) throws Exception {
//为Constructor对象定义泛型,泛型类型为String
Constructor<String> constructor =
String.class.getConstructor(StringBuilder.class);
//直接调用newInstance方法创建String对象,无需进行强制类型转换
String str = constructor.newInstance(new StringBuilder("Hello World!"));
System.out.println(str);
}
}
Hello World!
从以上代码可以看出,在Constructor类型引用上定义了泛型以后,就免去了强制类型转换的麻烦。
(2) 高级应用
需求:利用反射,绕过泛型集合的元素类型限制,向其中存储非泛型类型对象。比如创建一个ArrayList对象,并将其泛型类型定义为String。然后利用反射,向其中存储Integer对象。代码21:
import java.lang.reflect.Method;import java.util.ArrayList;执行结果为:
import java.util.Collection;
public class GenericDemo19 {
public static void main(String[] args) throws Exception {
Collection<String> collection = new ArrayList<String>();
//以下代码由于无法通过编译而无法执行
//collection.add(new Integer(9099));
//通过ArrayList字节码对象,获取代表add方法的Method对象
Method addMethod = collection.getClass().getMethod("add", Object.class);
//通过反射调用集合对象的add方法,绕开编译时期的类型检查
addMethod.invoke(collection, new Integer(100));
addMethod.invoke(collection, new StringBuilder("Hello World!"));
addMethod.invoke(collection, new Double(3.14));
for(Object obj : collection){
System.out.println(obj);
}
}
}
100
Hello World!
3.14
代码说明:
根据前述2.2泛型机制原理简述中的描述,定义泛型的目的,仅仅是在编译时期,从语法角度防止程序员向集合中存储非泛型类型对象。而编译完成后,在运行期间,加载到内存中的ArrayList的字节码实际就是没有进行任何泛型定义的原始字节码。
那么在以上代码中,我们根据这一原始字节码对象,获取表示添加元素的add方法对象,而此Method对象根据去类型化的特点,已经不再受到泛型类型的限制了,那么通过invoke方法,将add方法对象施加到collection集合中,即可向其中添加任意类型的对象,从而实现了绕过编译器检查泛型类型的限制。
4 泛型限定
4.1 通配符
在说明这一部分内容之前,我们先看一个例子,通过这个例子来引出泛型限定。需求:假设在某段代码中创建了大量的集合对象(下例中为演示方便,以两个集合为例),如果想要遍历这些集合中的元素并打印元素信息时(即toString方法),最为高效的方式就是专门定义一个方法用于遍历集合并打印。
代码22:
import java.util.*;运行结果为:
class GenericDemo20
{
public static void main(String[] args)
{
ArrayList<String> al1 = new ArrayList<String>();
al1.add("String1");
al1.add("String2");
al1.add("String3");
ArrayList<Integer> al2 = new ArrayList<Integer>();
al2.add(45);
al2.add(12);
al2.add(106);
printColl(al1);//向集合传递专门存储String对象的al1
/*
下面的代码
由于printColl方法形式参数泛型类型
与实际参数泛型类型不一致,而编译失败
*/
//printColl(al2);
}
//定义一个专门用于遍历集合并打印元素信息的方法
public static void printColl(ArrayList<String> al)//这里暂时定义泛型类型为String
{
for(Iterator<String> it = al.iterator(); it.hasNext(); )
{
System.out.println(it.next());
}
}
}
String1
String2
String3
注释部分代码将无法通过编译,这是显然的。因为printColl方法的形式参数的泛型类型为String,而注释部分代码的实际参数泛型类型为Integer,这就相当于如下代码,
ArrayList<String> al = new ArrayList<Integer>();
两泛型类型不一致当然编译失败。当然,我们也不能说为每个类都定义一个相对应的printColl方法,因为这样做扩展性非常差。那么这里我们需要介绍一个新的符号——?,称为通配符。下面我们通过通配符来修改上述代码,进而了解通配符的作用及意义,
代码23:只体现printColl方法部分的代码
public static void printColl(ArrayList<?> al)//将具体的泛型类型替换为通配符{做出如上修改以后,打开代码16中的注释,再次运行结果为:
for(Iterator<?> it = al.iterator(); it.hasNext(); )
{
System.out.println(it.next());
}
}
String1
String2
String3
45
12
106
结果表明,定义有通配符的泛型类型引用,可以接收定义有任意类型参数的对象,避免了为遍历打印不同泛型类型的集合而定义大量重载方法的麻烦。然而,虽然通配符的作用看起来非常方便,但这仅仅是从语法角度而言的,其实际作用是非常有限的:
(1) 由于不代表任何实际类型,在代码17的printColl方法中,我们不能对al集合进行任何与其参数类型相关的操作,比如调用add方法。由于在编译阶段,编译器并不知道在运行时期传入到printColl方法中的集合对象的泛型类型具体是什么,那么当调用al的add方法时,编译器只能从语法角度检查传入到add方法中的参数类型是否与泛型类型al的类型参数相匹配。但显然,通配符与任何类型都是不匹配的,这在语法上行不通的。因此对于定义有通配符的泛型类型集合,我们只能调用其与类型参数无关的方法,比如size、toString等等。
(2) 同样由于不知道泛型类型al的具体类型参数是什么,因此在对al进行遍历时,无法使用除Object类型以外的任意类型引用指向集合中的元素,更不能调用这些元素中的特有方法,除了从Object类继承来的方法。
可能有朋友认为调用printColl方法时,明明向其中传递了定义有具体泛型类型的集合对象,就好比“ArrayList<?> al = new ArrayList<String>();”,那么为什么还是不能调用al的add方法向其中传递String对象呢?大家还是要分清编译时期和运行时期的区别。对于编译器来说,它并不了解代码实际的运行效果。也就是说,当我们通过“al.add(“Hello World!”);”语句试图向al中传递一个字符串时,从代码的字面含义来说,被调用add方法的依旧是al引用,而非“ArrayList<String>()”对象,因此编译器当然就会检查al的泛型类型是否与add方法参数类型相匹配。当然,如果以上代码真的执行起来,实际被调用add方法的就是“new ArrayList<String>();”对象,那么从代码执行角度来说,当然可以向其中存储String对象了,但这已经不是编译器的管辖范围了。
正是由于以上两点限制,在定义printColl方法时将通配符改为类型参数可能更好,就像如下定义方式,
public static <T> void printColl(ArrayList<T> al, T ele)
这样一来,还可以对集合进行类型相关操作,比如调用add方法将另一个参数ele添加到集合中,因为将来在执行代码时,类型参数总是能够通过类型推断确定为某个类型,因此不会出现使用通配符时可能发生类型不匹配的问题。当然如果方法的功能是与集合元素类型无关的操作,那么使用通配符也是可以的。
4.2 泛型限定——规定上限
我们同样也是先来看一个例子。需求:比如自定义一个类Person,为其分别定义一个成员变量name和成员方法getName()(为演示方便,定义内容比较简单)。然后创建ArrayList集合对象,定义泛型类型为Person,存储若干Person对象,最后遍历集合并打印元素姓名。
代码24:
import java.util.*;运行结果为:
class Person
{
private String name;
Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
class GenericDemo21
{
public static void main(String[] args)
{
ArrayList<Person> al1 = new ArrayList<Person>();
al1.add(new Person("David"));
al1.add(new Person("Kate"));
al1.add(new Person("Wilson"));
printColl(al1);
}
//定义用于遍历集合元素,并打印元素姓名的方法
public static void printColl(ArrayList<Person> al)//定义泛型类型为Person
{
for(Iterator<Person> it = al.iterator(); it.hasNext(); )
{
Person p = it.next();
System.out.println(p.getName());
}
}
}
David
Kate
Wilson
上述例子比较简单,不多解释了,我们接着往下思考。
需求2:我们再定义一个新类Student,该类继承自Person类,同样希望创建ArrayList集合并存储若干Student对象,遍历集合并打印元素姓名。
代码25:
import java.util.*;上述代码中被注释掉的代码是不能通过编译的。可能有朋友会认为Student继承了Person,那么应该可以被认为是“同一类”事物,而通过编译,那么这就相当于如下代码,
class Person
{
private String name;
Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
//Student继承Person
class Student extends Person
{
Student(String name)
{
super(name);
}
}
class GenericDemo22
{
public static void main(String[] args)
{
ArrayList<Person> al1 = new ArrayList<Person>();
al1.add(new Person("David"));
al1.add(new Person("Kate"));
al1.add(new Person("Wilson"));
printColl(al1);
ArrayList<Student> al2 = new ArrayList<Student>();
al1.add(new Student("Jully"));
al1.add(new Student("Beth"));
al1.add(new Student("Tom"));
//printColl(al2);
}
public static void printColl(ArrayList<Person> al)
{
for(Iterator<Person> it = al.iterator(); it.hasNext(); )
{
Person p = it.next();
System.out.println(p.getName());
}
}
}
ArrayList<Person> al = new ArrayList<Student>();
而在获取元素时为了保证类型安全,可以以父类Person的形式,多态地获取元素。虽然这种思想是正确的,但这并不符合泛型机制的语法规定。
那么我们的需求就是:将某个类的若干不同子类对象(或接口的实现类)分别存储到不同集合,而只需一种方法即可对这些集合进行遍历,而不必定义大量的重载方法,并且希望在遍历的同时能够调用特有方法,而不仅仅是调用toString等Object类的方法。为了满足这一需求,Java中的解决方式就是利用泛型限定。我们先按照泛型限定的方式修改上述代码20,观察能否满足我们的需求,然后对其进行解释。
代码26:
import java.util.*;运行结果为:
//为演示方便省略了Person类以及Student类的代码
class GenericDemo23
{
public static void main(String[] args)
{
ArrayList<Person> al1 = new ArrayList<Person>();
al1.add(new Person("David"));
al1.add(new Person("Kate"));
al1.add(new Person("Wilson"));
printColl(al1);
System.out.println("========================");
ArrayList<Student> al2 = new ArrayList<Student>();
al2.add(new Student("Jully"));
al2.add(new Student("Beth"));
al2.add(new Student("Tom"));
printColl(al2);
}
/*
应用泛型限定定义形式参数泛型类型
它的意思是,printColl方法可以接收的ArrayList集合泛型类型为Person及其所有 子类
其中通配符的作用就是代表Person类的任意子类,但同样不具体指代某个类型
*/
public static void printColl(ArrayList<? extends Person> al)
{
//同样,迭代器也要应用泛型限定
for(Iterator<? extends Person> it = al.iterator(); it.hasNext(); )
{
//因为集合中的元素为Person及其子类
//就可以以Person类的形式多态地获取到集合中元素的
Person p = it.next();
System.out.println(p.getName());
}
}
}
David
Kate
Wilson
========================
Jully
Beth
Tom
从运行结果来看,泛型限定实现了我们的需求,不仅保证了类型安全,还提高了扩展性。
为了下面解释方便,我们将应用泛型机制的集合简称为泛型集合。在上述代码21中,<? extends Person>这个表达式的意思是:printColl方法可以接收定义了不同泛型类型的ArrayList泛型集合,但在运行时,虚拟机并不清楚这些泛型类型具体是什么类,但前提条件是这些类必须是Person类的子类(或者某个接口的实现类)。由于有了这样的限定条件,就不需要知道传入printColl方法的泛型集合(ArrayList)定义了哪个泛型类型,而一视同仁的通过多态的方式获取到这些元素,并调用这些类从Person继承而来的共性方法。那么泛型限定这一方式保证类型安全就体现在,泛型类型均可以向上转型为其父类,并以父类对象的形态,调用父类的特有方法;而提高扩展性体现在,即使后期再定义更多的Person类子类,均可以通过printColl方法遍历并调用泛型类型父类特有方法,进而实现复杂应用。
以上所述是泛型限定两种方式之一——规定上限,也就是说不同泛型类型都有共同的父类,比如代码21中的Person类。那么下面我们来了解泛型限定的另一种方式——规定下限。
4.3 规定下限
我们以TreeSet集合为例进行说明,首先该类本身定义了泛型,类型变量名为E,而其API文档构造方法摘要TreeSet(Comparator<? super E> comparator)中应用了泛型限定的下限规定,也就是说我们在创建TreeSet集合对象时,可传入构造方法的Comparator对象,其泛型类型可以是E,或者是E的父类。我们就按照这个规定,做一个简单应用,需求:我们首先定义一个Person类,该类为父类,并以Person类为基础定义了一个比较器类,用于比较Person类的姓名,希望将来存储到TreeSet集合中的Person类对象能够按照姓名的字母顺序排序。后期,我们再定义Person类子类Student,并将若干Student对象存储到泛型类型为Student的TreeSet集合对象中。此时,按照泛型限定规定下限的方式,为TreeSet集合对象初始化上述泛型类型为Person类的比较器对象,并同样希望按照姓名的字符顺序为Student对象排序。
代码27:
import java.util.*;运行结果为:
class Person
{
private String name;
Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
class Student extends Person
{
Student(String name)
{
super(name);
}
}
//该比较器用于比较Person类的姓名
class Comp implements Comparator<Person>
{
//假设所有Person对象的姓名一定不相同,因此不比较年龄
public int compare(Person p1, Person p2)
{
return p1.getName().compareTo(p2.getName());
}
}
class GenericDemo24
{
public static void main(String[] args)
{
/*
TreeSet集合构造方法,可以接收的比较器泛型类型为
该集合泛型类型的任意父类
*/
TreeSet<Student>ts = new TreeSet<Student>(new Comp());
ts.add(new Student("Peter"));
ts.add(new Student("Anna"));
ts.add(new Student("Tim"));
printColl(ts);
}
//printColl方法规定下限,可接受泛型类型为Person及其子类的TreeSet集合对象
public static void printColl(TreeSet<? extends Person>ts)
{
for(Iterator<? extends Person> it = ts.iterator(); it.hasNext(); )
{
System.out.println(it.next().getName());
}
}
}
Anna
Peter
Tim
结果表明,通过比较Student父类Person的比较器,成功将Student对象按照姓名字母顺序对其进行排序。
我们就以代码22为例进行说明,规定下限的意义。就拿自定义比较器类Comp来说,由于Comp实现了Comparator<Person>,因此Comp比较器类的类型参数同为Person。那么当通过代码
new TreeSet<Student>(new Comp());
创建一个TreeSet对象,并为其初始化一个比较器对象时,其隐含的语义其实就是
new TreeSet<Student>(new Comp<Person super Student>);
那么这是完全符合泛型规定下线的语法规则的。而之所以定义自定义比较类Comp的类型参数为Person类,是为了只需定义一次自定义比较器类,即可为今后新定义的所有Person类子类所用。说白了,还是为了提高代码的扩展性,因为定义比较器类Comp<Person>时,并不知道Person还会有什么样的子类,但只要继承自Person,都可以通过Comp<Person>进行比较。也就是说,在调用compare方法时,以Person类的形态,多态的接收Student对象(以及其他子类对象),对他们的姓名进行比较。反过来,如果比较器的泛型类型限定为某一个类型,那么就要为Person类的每个子类都单独定义一个比较器,扩展性极差。
那么对于TreeSet的构造方法“TreeSet<E>(Comparator<? super E> comparator)”的理解,可以是能够向构造方法中传入类型参数为任意的比较器对象,但是该类型参数必须是E的父类;也可以理解为,当传入构造方法中的比较器对象类型参数固定时(比如就是Person),TreeSet的类型参数就只能是Person的子类了。
那么以上内容就对泛型机制规定下限的含义进行了解释,并举了一个实际的例子帮助大家理解
最后,我们对泛型限定做一个简单总结:
规定上限:? extends E:可以接受E类型或者E的子类;
规定下限:? super E:可以接受类型或者E的父类。
那么总的来说,泛型限定就是用于泛型扩展用的,二者的具体作用参见前述说明。
5 泛型练习
需求1:编写一个泛型方法,自动将Object类型的对象转换成其他类型。代码28:
public class GenericTest { public static void main(String[] args) {以上代码能够通过编译并执行。
Object obj = "Hello World!";
String str = autoConvert(obj);
}
private static <T> T autoConvert(Object obj){
return (T)obj;
}
}
代码说明:
autoConvert方法的原理和使用方式都非常简单,可能有朋友认为这个方法有些画蛇添足,因为只要在main方法的第二行代码上通过强制类型转换,即可完成与autoConvert方法一样的操作。但是,这个演示这个方法的目的在于,泛型方法类型参数的实际类型,可以是由程序员指定的引用类型决定的,而并不一定是通过方法的参数列表类型来确定的。
需求2:定义一个方法,可以将任意类型数组中的所有元素填充为相应类型的某个对象。
代码29:
import java.util.Arrays;执行结果为:
public class GenericTest2 {
public static void main(String[] args) {
Integer[] ints = new Integer[3];
Integer num = new Integer(127);
fillArray(ints, num);
System.out.println(Arrays.toString(ints));
}
private static <T> void fillArray(T[] arr, T obj){
for(int i=0; i<arr.length; i++){
arr[i] = obj;
}
}
}
[127, 127, 127]
以上需求较为简单,这里不再详细说明。
需求3: 定义一个方法,把任意参数类型集合中的数据安全地复制到相同类型的数组中。
思路:如果定义一般的方法,那么由于无法限制集合以及数组中存储的元素类型,而有可能将不同类型的对象存储到同一个数组中。如果通过泛型对集合以及数组中存储元素的类型进行限制,就可以避免此类问题的发生。
代码30:
import java.util.ArrayList;import java.util.Arrays;执行结果为:
import java.util.Collection;
import java.util.Iterator;
public class GenericTest3 {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<String>();
coll.add("ABC");
coll.add("def");
coll.add("GhI");
String[] strings = new String[5];
copyToArray(coll, strings);
System.out.println(Arrays.toString(strings));
}
private static <T> void copyToArray(Collection<T> orig, T[] dest){
Iterator<T> it = orig.iterator();
//在遍历集合和数组时,要保证二者都不会角标越界
for(int i=0; i< dest.length && it.hasNext(); i++){
dest [i] = it.next();
}
}
}
[ABC, def, GhI, null, null]
需求4:定义一个方法,把任意类型数组中的数据安全地复制到相同类型的另一个数组中。
代码31:
public class GenericTest4 { public static void main(String[] args) {执行结果为:
String[] orig = new String[]{"abc", "def", "ghi"};
String[] dest = new String[3];
arrayToArray(orig, dest);
System.out.println(Arrays.toString(dest));
}
private static <T> void arrayToArray(T[] orig, T[] dest){
for(int i=0, j=0; i<orig.length && j<dest.length; i++, j++){
dest[j] = orig[i];
}
}
}
[abc, def, ghi]
之所以要举例说明练习三、练习四,是因为这其中又涉及到了类型推断。阅读下面的代码,
代码32:
import java.util.ArrayList;以上代码中,main方法的第三行代码是不能通过编译的。而第二行代码之所能够通过编译,正是得益于泛型方法的类型推断,也就是说,在执行第二行代码时,arrayToArray方法通过类型推断,推断出类型参数T为Integer和String的共同父类,那么在方法内部的实际执行效果就是:
public class GenericTest5 {
public static void main(String[] args) {
CollectionToArray(new ArrayList<String>(), new String[3]);//通过编译
arrayToArray(new Integer[5], new String[5]);//通过编译
//CollectionToArray(new ArrayList<Integer>(), new String[3]);//无法通过编译
}
}
Object[] orig = new Integer[5];
Object[] dest = new String[5];
因此,arrayToArray方法就可以对不同类型的数组进行元素复制操作。我们这里以两者共同父类Object为例进行说明。
但是对于CollectionToArray方法则不存在类型推断。因为类型推断的存在是为了解决泛型方法类型参数不确定的一种折衷办法,但是如果泛型的类型参数已经被指定为了一个确定的类型则不需要进行类型推断,就像CollectionToArray方法中接收参数化类型时,ArrayList的类型参数已经明确为Integer,此时CollectionToArray的泛型类型也就随之确定为Integer,这称为类型参数的传播性。
6 补充
6.1 多重泛型限定
多重泛型限定的意思是,可以为泛型类型定义多个上限或下限,比如“<V extends Serializable & Cloneable> void method(){}”,表示类型参数V既是Serializable的实现类,也要是Cloneable的实现了。当然,若要定义多重限定,那么限定的组合只能是多个接口,或者一个类与多个接口,不可能是多个类,因为Java不支持直接地多继承。多重泛型限定的与一般的泛型限定没有什么区别,只是对类型参数限制更为强烈而已。6.2 泛型机制也可应用于异常
类型参数代表异常时,称为参数化的异常,可以将类型参数应用于方法的异常声明列表中(也就是throws列表中),但是不能用于catch代码块。如下代码演示了参数化的异常,代码33:
private static <T extends Exception> void fun() throws T{ try{静态泛型方法fun中定义了泛型类型T,并且规定T类型必须是Exception的子类,但是与前面的例子不同的是,该类型参数并没有应用在方法的参数列表中,而是应用在了异常声明列表中。
}catch(Exception e){
throw (T)e;
}
}
但是,catch代码块中捕获的异常对象的类型却不能定义为T类型,比如定义成“catch(T e)”就无法通过编译,必须要明确规定捕获什么类型的异常对象。这是因为,在try代码块中实际抛出的异常类型可能与fun方法中声明抛出的异常类型(也就是T)是不相同的,或者没有继承关系,这样一来如果真的抛出异常,也无法对其进行捕获。那么最为安全的做法就是在catch代码块中声明捕获的异常类型为所有异常的父类Exception,这样在运行时无论抛出什么样的异常均可以多态的接收异常对象。
其次,在catch代码块中,将捕获的异常对象强制类型转换为T类型,其实体现了异常处理的分层思想。既然方法中声明了可能会抛出T类型的异常,那么别人在调用fun方法时,就会按照T类型异常的特点进行异常处理。因此在catch代码块中最终要抛出的异常类型必须将其转换为T类型,否则方法调用者将无法处理异常。
6.3 多泛型类型定义
就像Map接口的API文档中定义的那样,定义泛型的时候,可以定义多个类型参数,不同类型参数之间需要用逗号分隔,比如“Map<K, V>”。当然,泛型方法中也可以定义多个泛型类型参数,与泛型类型的是一样的。6.4 通过反射获得泛型的实际类型参数
正如前文所述,由于泛型机制在编译阶段的类型擦除,因此在运行阶段加载到内存中的字节码对象中并没有类型信息,与原始类型完全相同。但是在实际开发中,我们有时候需要获取类型参数的实际类型。比如,某个方法的参数类型为一个定义有泛型的集合,但是只有在运行阶段才能最终确定该集合类型参数的实际类型,因此当获取到集合中元素后,由于不知道其实际类型,也就不能进行强制类型转换,更不能调用特有方法。当然泛型限定在一定程度上解决了这个问题,但是有没有准确获取到这一实际类型的办法呢?这一问题同样可以通过反射来解决。假设Demo类中定义了一个fun方法,参数列表为一个定义有泛型的集合:
class Demo{ public void fun(ArrayList<String> coll){};我们的目标就是,获取到参数化类型ArrayList <String>类型参数的实际类型——java.lang.String。
}
代码34:
import java.lang.reflect.Method;import java.lang.reflect.ParameterizedType;import java.lang.reflect.Type;import java.util.ArrayList;class Demo{ public void fun(ArrayList<String> al){}}public class GenericDemo25 { public static void main(String[] args) throws Exception { Demo demo = new Demo(); Method funMethod = demo.getClass().getMethod("fun", ArrayList.class); Type[] types = funMethod.getGenericParameterTypes(); ParameterizedType pt = (ParameterizedType)types[0]; System.out.println(pt.getActualTypeArguments()[0]); System.out.println(pt.getRawType()); }}执行结果为:
class java.lang.String
class java.util.ArrayList
通过以上代码,获取到了参数化类型ArrayList<String>类型参数的实际类型。
代码说明:
(1) getGenericParameterTypes方法的作用是返回fun方法参数列表中所有参数的Type对象,并且如果有定义泛型,这些Type对象中就会带有类型参数信息。
(2) 以上所说的类型参数信息,只有将Type对象强制类型转换为ParameterizedType——Type的子接口——才能获取。顾名思义,ParameterizedType的意思就是参数化类型,比如ArrayList<String>,那么获取其类型参数实际类型的方式就是调用getAcutalArguments,作用就是获取实际类型参数。由于类型参数可能有多个,比如Map接口,因此getActualArguments方法的返回值是一个数组。该数组中包好有,fun方法指定参数的所有类型参数的实际类型Type对象。
(3) 注意到代码34中导入了一个名为Type的接口,它是Java编程语言中所有类型的公共高级接口,是在JDK1.5版本中引入的。而这一公共高级接口的子接口或者实现类包括原始类型(8中基本数据类型)、参数化类型、数组类型、类型变量和基本类型,换句话说,它是一切Java类型的父接口。Type接口是将“类型”这类抽象事物向上抽取而来,比如用“class”关键字定义的是基本类型,而用“interface”关键字定义的是接口类型,用“enum”关键自定义的是枚举类型,他们之间具有相似性,但也互相有区别,就将“类型”这类抽象事物向上抽取就得到了Type接口。
从以上代码的原理可以看出,通过以上方法仅能获取到参数化类型的实际类型参数,但是对泛型类型是无效的。大家可以将fun方法修改为“public <T> void fun(ArrayList<T> al){}”,那么获取到的泛型类型ArrayList<T>的实际类型就是T,反映了以上方法的局限性。