一、引言
泛型(generic)是指带参数化类型的能力,可以定义带泛型类型的类或方法,随后编译器会用具体的类型来替换它。例如,可以顶一个泛型栈类,它存储的是泛型元素,可以从这个泛型类生成一个包含字符串的栈对象和一个包含数字的栈对象。这里,字符串和数字都是替换泛型的具体类型。
使用泛型的主要优点是能够在编译时而不是运行时检测出错误。泛型类或方法允许用户指定可以和这些类或方法一起工作的对象类型。如果试图使用一个不相容的对象,编译器就会检测出这个错误。
接下来我们将介绍如何定义和使用泛型类、泛型接口和泛型方法
二、动机和优点
从JDK1.5开始,JAVA允许定义泛型类、泛型接口和泛型方法。可以使用泛型对Java API中的一些接口和类进行修改。例如,在JDK1.5之前,java.lang.Comparable接口被定义为
public interface Comparable{
public int compareTo(Object o)
}
JDK1.5之后
public interface Comparable<T>{
public int compareTo(T o)
}
这里的表示形式泛型类型(formal generic type),随后可以用一个实际具体类型(actual concrete type)来替代它。替换泛型类型称为泛型实例化(generic instantiation)。按照惯例,像E或T这样的单个大写字母用于表示一个形式泛型类型。
为了了解使用泛型的好处,我们观察下面两段代码
Comparable c = new Date();
System.out.println(c.compareTo("red"));
上面这段代码将c声明为一个引用变量,它的类型是Comparable,然后调用compareTo方法来比较Date对象和一个字符串。这样的代码可以编译,但是它会产生一个运行时错误,因为字符串不能与Date对象进行比较。
下面来观察另一段代码
Comparable<Date> c = new Date();
System.out.println(c.compareTo("red"));
上面这段代码将c声明为一个引用变量,它的类型是Comparable,然后调用compareTo方法来比较Date对象和一个字符串。这样的代码会产生编译错误,因为传递给compareTo方法的参数必须是Date类型的。由于这个错误可以在编译时而不是运行时被检测到,因为泛型类型使程序更加可靠。
比如,ArrayList类,从JDK1.5开始,该类是一个泛型类。
下面的语句能够创建一个字符串的线性表
ArrayList<String> list = new ArrayList<String>();
现在就只能向该线性表中添加字符串。例如,
list.add("Red");
如果向其中添加非字符串,就会产生编译错误。例如,下面的语句就是不合法的,因为list只能包含字符串
list.add(new Integer(1));
泛型类型必须是引用类型。不能用像int,double,或char这样的基本类型来替换泛型类型。例如,下面的语句是错误的
ArrayList<int> intList = new ArrayList<int>();
为了为int值创建一个ArrayList对象,必须使用:
ArrayList<Integer> intList = new ArrayList<Integer>();
可以在intList中加入一个int值。例如:
intlist.add(5);
java会自动地将5包装为new Integer(5)。这个过程称为自动打包(autoboxing)
无需类型转换就可以从一个元素类型已指定的列表中获取一个值,因为编译器已经知道了这个元素类型。例如,下面的语句创建了一个包含字符串的列表,然后将字符串加入这个列表,最后从这个列表中获取该字符串。
ArrayList<String> list = new ArrayList<String>();
list.add("Red");
list.add("White");
String s = list.get(0);//无需转换
在JDK1.5之前,由于没有使用泛型,所以必须把返回值的类型转换为String,如下所示:
String s = (String)(list.get(0));//需要转换
如果元素是包装类型,例如,Integer,Double或Character,那么可以直接将这个元素赋给一个基本类型的变量。这个过程称为自动解包(autounboxing)
ArrayList<Double> list = new ArrayList<Double>();
list.add(5.5);//5.5被自动转换成new Double(5.5)
list.add(3.0);//3.0被自动转换成new Double(3.0)
Double doubleObject = list.get(0);//无需转换
double d = list.get(1);//自动转换成double类型
在第2句和第3句,5.5和3.0自动转换为Double对象,并添加到list中。在第4句,list中的一个元素被赋给一个Double变量。在此无需类型转换,因为list被声明为Double对象。在第5句中,list中的第二个元素被赋给一个double变量。list.get(1)中的对象自动转换为一个基本类型的值。
三、定义泛型类和接口
修改栈类,将元素类型扩展到泛型类型
GenericStack.java
public class GenericsStack<E>{
private java.util.ArrayList<E> list = new java.util.ArrayList<E>();
public int getSize(){
return list.size();
}
public E peek(){
return list.get(getSize()-1);
}
public void push(E o){
list.add(o);
}
public E pop(){
E o = list.get(getSize()-1);
list.remove(getSize()-1);
return o;
}
public boolean isEmpty(){
return list.isEmpty();
}
}
下面给出一个例子,它先创建一个存储字符串的栈,然后向这个栈中添加三个字符串:
GenericStack<String> stack1 = new GenericStack<String>();
stack1.push("London");
stack1.push("Paris");
stack1.push("Berlin");
下面给出另一个例子,它先创建一个存储整数的栈,然后向这个栈添加三个整数:
GenericStack<Integer> stack2 = new GenericStack<Integer>();
stack2.push(1);//自动打包1 成 new Integer(1)
stack2.push(2);
stack2.push(3);
也可以不使用泛型,只要将元素类型设置为Object,它就可以适应任何对象类型。但是,使用泛型能够提高软件的可靠性和可读性,因为某些错误能在编译时而不是运行时被发现。例如,由于stack1中添加整数就会产生编译错误。
warning:为了创建一个字符串堆栈,可以使用new GenericStack()。但是不要误以为GenericStack的构造方法也为public GenericStack();这是错误的!!!!!
正确的定义方法应该为:public GenericStack()
注意:有时候,泛型类可能会有多个参数。在这种情况下,应将所有参数一起放在尖括号中,并用逗号隔开,比如
public clas GenericMethodDemo{
public static void main(String[] args){
Integer[] integers = {1,2,3,4,5};
String[] strings = {"London","Paris","New York","Austin"};
GenericMethodDemo.<Integer>print(integers);
GenericMethodDemo.<String>print(strings);
}
public static <E> void print(E[] list){
for(int i = 0;i<list.length;i++){
System.out.print(list[i]+" ");
System.out.println();
}
}
为了调用泛型方法,需要将实际类型放在尖括号内作为方法名的前缀。例如,
GenericMethodDemo.print(integers);
GenericMethodDemo.print(strings);
可以将泛型指定为另外一种类型的子类型。这样的泛型类型称为受限的(bounded)。例如下面所示的代码用以测试两个几何对象是否具有相同的面积。受限的泛型类型将E指定为GeometricObject的泛型子类型。此时必须传递两个GeomericObject的实例来调用equalArea
BoundedTypeDemo.java
public class BoundedTypeDemo{
public static void main(String[] args){
Rectangle rectangle = new Rectangle(2,2);
Circle circle = new Circle(2);
System.out.println("Same area?"+BoundedTypeDemo.<GeotricObject>equalArea(rectangle,circle));
public static <E extends GeometricObject> boolean equalArea(E object1,E object2){
return object1.getArea()==object2.getArea();
}
}
注意:非受限泛型类型和是一样的。
注意:为了定义一个类为泛型类型,需要将泛型类型放在类名之后,例如,GenericStack
为了定义一个方法为泛型类型,要将泛型类型放在方法返回类型之前,例如,void max(E o1,E o2)
五、原始类型和向后兼容
可以使用泛型类而无需指定具体类型,如下所示:
GenericStack stack = new GenericStack();
它大体等价于下面的语句
GenericStack<Object> stack = new GenericStack<Object>();
像GenericStack和ArrayList这样不使用类型参数的泛型类称为原始类型(raw type)。在Java的早期版本中,允许使用原始类型向后兼容(backward compatibility)。例如,从JDK1.5开始,在java.lang.Comparable中使用了泛型类型,但是,许多代码仍然使用原始类型Comparable
Max.java
public class Max{
//返回两个对象之间的最大值
public static Comparable max(Comparable o1,Comparable o2){
if(o1.compareTo(o2)>0)
return o1;
else
return o2;
}
}
Comparable o1和Comparable o2都是原始类型声明。原始类型是不安全的。例如,我们可能会使用下面的语句调用max方法:
Max.max("Welcome",23);//23被自动打包成new Integer(23)
这可能会引起一个运行时错误,因为不能将字符串与整数对象进行比较。
一个更好的编写max方法的方式是使用泛型类型,如下面所示:
Max1.java
public class Max1{
public static <E extends Comparable<E>>E max(E o1,E o2){
if(o1.compareTo((o2)>0)
return o1;
else
return o2;
}
}
如果使用下面的命令调用max方法:
Max1.max("Welcom",23);//23 is autoboxed into new Integer(23)
就会显示一个编译错误,因为Max1中的max方法的两个参数必须是相同的类型(例如,两个字符串或两个整数对象)。此外,类型E必须是Comparable的子类型。
下面的代码是另外一个例子,可以在第1行声明一个原始类型stack,在第2行将new GenericStack赋给它,然后在第3行和第4行将一个字符串和一个整数对象压入栈中。
GenericStack stack;
stack = new GenericStack<String>();
stack.push("Welcome to Java");
stack.push(new Integer(2));
第4行是不安全的,因为该栈是用于存储字符串的,但是一个Integer对象被添加到该栈中。第3行本应是可行的,但是编译器会在第3行和第4行都显示警告,因为它没有遵循程序的语义含义。所有编译器都知道该栈是原始类型的,并且在执行某些操作时会不安全。因此,它会显示警告以提醒你提防潜在的问题。
六、通配泛型
例:定义一个泛型max方法,该方法可以找出数字栈中的最大数,main方法创建了一个整数对象栈,然后向该栈添加三个整数,最后调用max方法找出该栈中的最大数字
WildCardDemo1.java
public class WildCardDemo1{
public static void main(String[] args){
GenericStack<Integer> intStack = new GenericStack<Integer>();
intStack.push(1);
intStack.push(2);
intStack.push(-2);
System.out.print("The max number is " + max(intStack));
}
public static double max(GenericStack<Number> stack){
double max = stack.pop().doubleValue();
while(!stack.isEmpty()){
double value = stack.pop().doubleValue();
if(value>max)
max=value;
}
return max;
}
}
第8行会出现编译错误,因为intStack不是GenericStack**的实例。所以,不能调用max(intStack)
尽管Integer是Number的子类型,但是GenericStack**并不是GenericStack**的子类型。为了避免这个问题,可以使用通配泛型类型。通配泛型类型有三种形式——?、? extends T或者?super T,其中T是某个泛型类型。
第一种形式?称为非受限通配(unbounded wildcard),它和? extends Object是一样的。第二种形式?extends T称为受限通配(bounded wilcard),表示T或T的一个未知子类型。第三种形式? super T称为下限通配l(lower-bounded wildcard),表示T或T的一个未知父类型。
使用下面的语句替换上面代码中的第12行就可以修复上面的错误:
public static double max(GenericStack<? extends Number>stack)
public class WildCardDemo2{
public static void main(String[] args){
GenericStack<Integer> intStack = new GenericStack<Integer>();
intStack.push(1);
intStack.push(2);
intStack.push(-2);
print(intStack);
}
public static void print(GenericStack<?> stack){
while(!stack.isEmpty()){
System.out.print(stack.pop()+" ");
}
}
}
什么时候需要
public class WildCardDemo3{
public static void main(String[] args){
GenericStack<String> stack1 = new GenericStack<String>();
GenericStack<Object> stack2 = new GenericStack<Object>();
stack2.push("java");
stack.push(2);
stack1.push("Sun");
add(stack1,stack2);
WildCardDemo2.print(stack2);
}
public static <T> void add(GenericStack<T> stack1,GenericStack<? super T> stack2){
while(!stack.isEmpty())
stack2.push(stack1.pop());
}
}
七、消除泛型和对泛型的限制
泛型是使用一种称为类型消除(type erasure)的方法来实现的。编译器使用泛型类型信息来编译代码,但是随后会消除它。因此,泛型信息在运行时是不可用的。这种方法可以使泛型代码向后兼容使用原始类型的遗留代码。
泛型存在于编译时。一旦编译器确认的泛型类型是安全使用的,就会将它转换为原始类型。例如,编译器会检查在图a中的代码里泛型是否被正确使用,然后将它翻译成如图b所示的在运行时使用的等价代码。图b中的代码使用的是原始类型。
图a:
ArrayList<String> list = new ArrayList<String>();
list.add("OKlahoma");
String state = list.get(0);
图b:
ArrayList list = new ArrayList();
list.add("OKlahoma");
String state = list.get(0);
当编译泛型类、接口和方法时,编译器用Object类型代替泛型类型。例如,编译器会将图a中的方法转换为图b中的方法。
图a:
public static <E> void print(E[] list){
for(int i=0;i<list.length;i++){
System.out.print(list[i]+" ");
System.out.println();
}
图b:
public static void print(Object[] list){
for(int i = 0;i<list.length;i++)
System.out.print(list[i]+" ");
System.out.println();
如果一个泛型类型是不受限的,那么编译器就会用一个受限类型来替换它。例如,编译器会将图a中的方法转换为图b中的方法。
图a:
public static <E extends GeometricObject> boolean equalArea(E object1,E object2){
E object1;
E object2;
return object1.getArea()==object2.getArea();
}
图b:
public static boolean equalArea(GeometricObject object1,GeometricObject object2){
return object1.getArea()==object.getArea();
}
非常需要注意的是,不管实际的具体类型是什么,泛型类是被它的所有实例所共享的。假定按如下方式创建list1和list2:
ArrayList<String> list1 = new ArrayList<String>();
ArrayList<Integer> list2 = new ArrayList<Integer>();
尽管在编译时ArrayList和ArrayList并没有在JVM中存储为单独一个类,所以,在运行时使用它是毫无意义的。
由于泛型类型在运行时被消除,因此,对于如何使用泛型类型是有一些限制的。下面是其中的一些限制:
(1)不能使用new E()
不能使用泛型类型参数创建实例。例如,下面的语句是错误的:
E object = new E();
出错的原因是运行时执行的是new E(),但是运行时泛型类型E是不可用的
(2)不能使用new E[]
不能使用泛型类型参数创建数组。例如,下面的语句是错误的:
E[] elements = new E[capacity];
可以通过创建一个Object类型的数组,然后将它的类型转换为E[]来规避这个限制,如下所示:
E[] elements = (E[])new Object[capacity];
但是,类型转换到(E[])会导致一个免检的编译警告。该警告会出现是因为编译器无法确保在运行时类型转换能成功。例如,如果E是String,而new Object[]是Integer对象的数组,那么(String[])(new Object[])将会导致ClassCastException异常。这种类型的编译警告是对Java泛型的限制,也是无法避免的。
不允许使用泛型类创建泛型数组。例如,下面的代码是错误的:
ArrayList<String>[] list = new ArrayList<String>[10];
可以使用下面的代码来规避这种限制:
ArrayList<String>[]list = (ArrayList<String>[])new ArrayList[10];
你将会得到一个编译警告
(3)在静态环境下不允许类的参数是泛型类型
由于泛型类的所有实例都有相同的运行时类,所以泛型类的静态变量和方法是被它的所有实例所共享的。因此,在静态方法、数据域或者初始化语句中,为了类而引用泛型类型参数是非法的。例如,下面的代码是非法的:
public class Test<E>{
public static void m(E o1){
}//静态方法
public static E o1;//数据域
static {
E o2;
}//初始化语句
}
(4)异常类不能使泛型的
泛型类不能扩展java.lang.Throwable,因此,下面的类声明是非法的:
为什么?如果允许这样做,就应为MyException添加一个catch子句,如下所示:
public class MyException<T> extends Exception{
}
JVM必须检查这个从try子句中抛出异常已确定它是否与catch子句总指定的类型匹配。但这是不可能的,因为在运行时类型信息是不出现的。
try{
…
}
catch(MyException ex){
…
}