泛型
泛型是由JDK5引入的,在两个重要方面改变了Java。首先,泛型为语言增加了新的语法元素。其次,泛型改变了核心API中的许多类和方法。通过使用泛型,可以创建以类型安全的方式使用各种类型数据的类、接口以及方法。许多算法虽然操作的数据类型不同,但算法逻辑是相同的。例如,不管堆栈存储的数据类型是Integer、String、Object还是Thread,支持堆栈的机制是相同的。使用泛型,可以只定义算法一次,使其独立于特定的数据类型,然后将算法应用于各种数据类型而不需要做任何额外的工作。泛型为语言添加的强大功能从根本上改变了编写Java代码的方式。
在泛型所影响的Java特性中,受影响程度最大的一个可能是集合框架(Collection Framework)。集合是一组对象。集合框架定义了一些类,例如列表和映射,这些类用来管理集合。集合类总是可以使用任意类型的对象。因为增加了泛型特性,所以现在可以采用类型绝对安全的方式使用集合类。因此,除了本身是一个强大的语言元素外,泛型还能够从根本上改进已有特性。
1.1 什么是泛型
就本质而言,术语“泛型”的意思是参数化类型。参数化类型很重要,因为使用该特性创建的类、接口以及方法,可以作为参数指定所操作数据的类型。例如,使用泛型可以创建自动操作不同类型数据的类。操作参数化类型的类、接口或方法被称为泛型,例如泛型类或泛型方法。
通过操作Object类型的引用,Java总是可以创建一般化的类、接口以及方法。因为Object是所有其他类的超类,所以Object引用变量可以引用所有类型的对象。因此,在Java提供泛型特性之前编写的代码,一般化的类、接口以及方法使用Object引用来操作各种类型的对象。问题是它们不能以类型安全的方式进行工作。
泛型提供了以前缺失的类型安全性,并且还可以简化处理过程,因为不再需要显示地使用强制类型转换,即不再需要在Object和实际操作的数据类型之间进行转换。使用泛型,所有类型转换都是自动和隐式进行的。因此泛型扩展了重用代码的能力,并且可以安全、容易地重用代码。
注意:
尽管泛型和C++中的模板很类似,但它们不是一回事,这两种处理泛型类型的方式之间有一些本质区别。不要草率地认为Java中泛型的工作机理与C++中的模板相同。
1.2 一个简单的泛型示例
下面首先看一个泛型类的简单示例。下面的程序定义了两个类。第一个是泛型类Gen;第二个是泛型类GenDemo,该类使用Gen类。
//A simple generic class.
//Here,T is a type parameter that
//will be replaced by a real type
//when an object of type Gen is created.
public class Gen<T> {
T ob;//declare an object of type T
//Pass the constructor a reference to
Gen(T o) {
ob = o;
}
//Return ob.
T getOb() {
return ob;
}
//Show type of T.
void showType() {
System.out.println("Type of T is " +
ob.getClass().getName());
}
}
//Demostrate the generic class.
public class GenDemo {
public static void main(String[] args) {
//Create a Gen reference for Integers.
Gen<Integer> iOb;
//Create a Gen<Integer> object and assign its
//reference to iOb. Notice the use of autoboxing
//to encapsulate the value 88 within an Integer object.
iOb=new Gen<Integer>(88);
//Show the type of data used by iOb.
iOb.showType();
//Get the value in iOb.Notice that
//no cast is needed.
int v = iOb.getOb();
System.out.println("value: "+v);
//Create a Gen object for Strings.
Gen<String> strOb = new Gen<String>("Generics Tests");
//Show the type of data used by strOb.
strOb.showType();
//Get the value of strOb.Again,Notice
//that no cast is needed.
String str = strOb.getOb();
System.out.println("value: "+str);
}
}
输出结果:
Type of T is java.lang.Integer
value: 88
Type of T is java.lang.String
value: Generics Tests
程序分析:
首先,注意下面这行代码声明泛型类Gen的方式:
class Gen<T>{
其中,T是类型参数的名称。这个名称是实际类型的占位符,当创建对象时,将实际类型传递给Gen。因此在Gen中,只要需要类型参数,就使用T。注意T被包含在<>中。可以推广该语法。只要声明类型参数,就需要在尖括号中指定。因为Gen使用类型参数,所以Gen是泛型类,也称为参数化类型。
接下来使用T声明对象ob,如下所示:
T ob;//declare an object of type T
前面解释过,T是将在创建Gen对象时指定的实际类型的占位符。因此,ob是传递给T的那种实际类型的对象。例如,如果将String传递给T,ob将是String类型。
现在分析Gen的构造函数:
Gen(T o){
ob = o;
}
注意参数o的类型是T,这意味着o的实际类型取决于创建Gen对象时传递给T的类型。此外,因为参数o和成员变量ob的类型都是T,所以在创建Gen对象时,它们将具有相同的类型。
还可以使用类型参数T指定方法的返回类型,就行getOb()方法那样,如下所示:
T getob(){
return ob;
}
因为ob也是T类型,所以ob的类型和getob()方法指定的返回类型是兼容的。
showType()方法通过对Class对象调用getName()方法来显示T的类型,而这个Class对象是通过ob调用getClass()方法返回的。getClass()方法是由Object类定义的,因此该方法是所有类的成员。该方法返回一个Class对象,这个Class对象与调用对象所属的类对应。Class定义了getName()方法,该方法返回类名字的字符串表示方式。
GenDemo类演示了泛型化的Gen类。它首先创建整型版本的Gen类,如下所示:
Gen<Integer> iOb;
首先,注意类型Integer是在Gen后面的尖括号中指定的。在此,Integer是传递给Gen的类型参数。这有效地创建了Gen的一个版本,在该版本中,对T的所有引用都被转换成对Integer的引用。因此对于这个声明,ob是Integer类型,并且getob()方法的返回类型是Integer。
Java编译器实际上没有创建不同版本的Gen类,或者说没有创建任何其他泛型类。尽管那样认为是有帮助的,但是实际情况并非如此。相反,编译器移除所有泛型类型信息,进行必需的类型转换,从而使代码的行为好像是创建了特定版本的Gen类一样。因此,在程序中实际上只有一个版本的Gen类。移除泛型类型信息的过程被称为擦除。
下一行代码将一个引用(指向Integer版本的Gen类的一个实例)赋给iOb:
iOb = new Gen<Integer>(88);
注意在调用Gen构造函数时,仍然指定了类型参数Integer。这是必需的,因为将为其赋值的对象(在此为iOb)的类型是Gen<Integer>。因此, new返回的引用也必须是Gen<Integer>类型。如果不是的话,就会产生编译时错误。例如,下面的赋值操作会导致编译时错误:
iOb = new Gen<Double>(88.0);//Error!
因为iOb是Gen<Integer>,所以不能引用Gen<Double>类型的对象。这种类型检查是泛型的主要优点之一,因为可以确保类型安全。
注意:
自JDK7开始,可以缩短创建泛型类的实例的语法。
正如程序中的注释所表明的,下面的赋值语句:
iOb = new Gen<Integer>(88);
使用自动装箱封装数值88,将这个int型数值转换成Integer对象。这可以工作,因为Gen<Integer>创建了一个使用Integer参数的构造函数。因为期望Integer类型的对象,所以Java会自动将数值88装箱到Integer对象中。当然,也可以像下面这样显示地编写这条赋值语句:
iOb = new Gen<Integer>(new Integer(88));
但是,使用这个版本的代码没有任何好处。
然后程序显示iOb中ob的类型,也就是Integer类型。接下来,程序使用下面这行代码获取ob的值:
int v = iOb.getob();
因为getob()方法的返回类型是T,当声明iOb时T已被替换为Integer类型,所以getob()方法的返回类型也是Integer,当将返回值赋给v(是int类型)时会自动拆箱为int类型。因此,不需要将getob()方法的返回类型强制转换成Integer。当然,并不是必须使用自动拆箱特性。前面这行代码也可以像下面这样编写:
int v = iOb.getob().intValue();
但是,自动拆箱特性使代码更紧凑。
接下来,GenDemo声明了Gen<String>类型的一个对象:
Gen<String> strOb = new Gen<String>("Generics Test");
因为类型参数是String,所以使用String替换Gen中的T。(从概念上讲)这会创建Gen的String版本,就像程序中的剩余代码所演示的那样。
1.2.1 泛型只使用引用类型
当声明泛型类的实例时,传递过来的类型参数必须是引用类型。不能使用基本类型。如int或char。例如,对于Gen,可以将任何类型传递给T,但是不能将基本类型传递给类型参数T。所以,下面的声明是非法的:
Gen<int> intOb = new Gen<int>(53);//Error,cannot use primitive type
当然,不能使用基本类型并不是一个严重的限制,因为可以使用类型封装器封装基本类型(就像前面的例子那样)。此外,Java的自动装箱和拆箱机制使得类型封装器的使用是透明的。
1.2.2 基于不同类型参数的泛型类型是不同的
对特定版本的泛型类型的引用和同一泛型类型的其他版本不是类型兼容的,这是关于泛型类型方面更需要理解的关键一点。例如,对于刚才显示的程序,下面这行代码是错误的,并且不能通过编译:
iOb = strOb;//Wrong!
尽管iOb和strOb都是Gen<T>类型,但它们是对不同类型的引用,因为它们的类型参数不同。这是泛型添加类型安全性以及防止错误的方式的一部分。
1.2.3 泛型提升类型安全性的原理
既然在泛型类Gen中,通过简单地将Object作为数据类型并使用正确的类型转换,即使不使用泛型也可以得到相同的功能,那么将Gen泛型化有什么好处呢?答案是对于所有涉及Gen的操作,泛型都可以自动确保类型安全。在这个过程中,消除了手动输入类型转换以及类型检查的需要。
为了理解泛型带来的好处,首先考虑下面的程序,这个程序创建了一个非泛型的Gen的等价类:
//NonGen is functionally equivalent to Gen
//but does not use generics.
public class NonGen {
Object ob;//ob is now of type Object
//Pass the constructor a reference to
//an object of type Object
NonGen(Object o) {
ob = o;
}
//Return type Object.
Object getob() {
return ob;
}
//Show type of ob.
void showType() {
System.out.println("Type of ob is " + ob.getClass().getName());
}
}
//Demonstrate the non-generic class.
public class NonGenDemo {
public static void main(String[] args) {
NonGen iOb;
//Create NonGen Object and store
//an Integer in it. Autoboxing still occurs.
iOb = new NonGen(88);
//Show the type of data used by iOb.
iOb.showType();
//Get the value of iOb.
//This time,a cast is necessary.
int v = (Integer) iOb.getob();
System.out.println("value: " + v);
//Create another NonGen object and
//store a String in it.
NonGen strOb = new NonGen("Non-Generics Test");
//Show the type of data used by strOb.
strOb.showType();
//Get the value of strOb.
//Again,notice that a cast is necessary.
String str = (String) strOb.getob();
System.out.println("value: " + str);
//This compiles,but is conceptually wrong!
iOb = strOb;
v = (Integer) iOb.getob();//run-time error!
}
}
在这个版本中有几个有趣的地方。首先,注意NonGen类使用Object替换了所有的T。这使得NonGen能够存储任意类型的对象,就像泛型版本那样。但是,这样做也使得Java编译器不知道NonGen中实际存储的数据类型的任何相关信息,这是一件坏事情,原因有二,首先,对于存储的数据,必须显示地进行类型转换才能提取。其次,许多类型不匹配错误直到运行时才能发现。下面深入分析每个问题。
请注意下面这行代码:
int v = (Integer)iOb.getob();
因为getob()方法的返回类型是Object,所以为了能够对返回值进行自动拆箱并保存在v中,必须将返回值强制转换为Integer类型。如果移除强制转换,程序就不能编译。使用泛型版本的话,这个类型转换是隐式进行的。在非泛型版本中,必须显示地进行类型转换。这不但不方便,而且还是潜在的错误隐患。
现在分析下面的代码,这些代码位于程序的末尾:
//This compiles,but is conceptually wrong!
iOb = strOb;
v = (Integer)iOb.getob();//run-time error!
在此,将strOb赋给iOb。但是strOb引用的是包含字符串而非包含整数的对象。这条赋值语句在语法上是合法的,因为所有NonGen引用都是相同的,所有NonGen引用变量都可以引用任意类型的NonGen对象。但是,这条语句在语义是错误的,正如下一行所显示的那样。这一行将getob()方法的返回值强制转换成Integer类型,然后试图将这个值赋给v。现在的麻烦是:iOb引用的是包含字符串而非包含整数的对象。遗憾的是,由于没有使用泛型,Java编译器无法知道这一点。相反,当试图强制转换为Integer时会发生运行时异常。在代码中发生运行时异常是非常糟糕的。
如果使用泛型,就不会发生上面的问题。在程序的泛型版本中,如果试图使用这条语句,编译器会捕获该语句并报告错误,从而防止会导致运行时异常的严重bug。创建类型安全的代码,从而在编译时能够捕获类型不匹配错误,这是泛型的一个关键优势。尽管使用Object引用创建“泛型”代码总是可能的,但这类代码不是类型安全的,并且对它们的误用会导致运行时异常。泛型可以防止这种问题的发生。本质上,通过泛型可以将运行时错误转换成编译时错误,这是泛型的主要优势。
1.3 带两个类型参数的泛型类
在泛型中可以声明多个类型参数。为了指定两个或更多个类型参数,只需要使用逗号分隔参数列表即可。例如,下面的TwoGen是Gen泛型类的另一个版本,具有两个类型参数:
//A simple generic class with two type
//parameters: T and V.
public class TwoGen<T, V> {
T ob1;
V ob2;
//Pass the constructor a reference to
//an object of type T and an object of type V.
TwoGen(T o1, V o2) {
ob1 = o1;
ob2 = o2;
}
//Show types of T and V
void showTypes() {
System.out.println("Type of T is " + ob1.getClass().getName());
System.out.println("Type of V is " + ob2.getClass().getName());
}
T getob1() {
return ob1;
}
V getob2() {
return ob2;
}
}
//Demonstrate TwoGen.
public class SimpGen {
public static void main(String[] args) {
TwoGen<Integer,String> tgObj=new TwoGen<Integer,String>(88,"Generics");
//Show the types.
tgObj.showTypes();
//Obtain and show values.
int v = tgObj.getob1();
System.out.println("value: "+v);
String str =tgObj.getob2();
System.out.println("value: "+str);
/**
* 输出结果
* Type of T is java.lang.Integer
* Type of V is java.lang.String
* value: 88
* value: Generics
*/
}
}
注意TwoGen的声明方式:
class TwoGen<T,V>{
在此指定了两个类型参数:T和V,使用逗号将它们隔开。创建对象时必须为TwoGen传递两个类型参数,如下所示:
TwoGen<Integer,String> tgObj = new TwoGen<Integer,String>(88,"Generics");
在此,Integer替换T,String替换V。
在这个例子中,尽管两个类型参数是不同的,但是可以将两个类型参数设置为相同的类型。例如,下面这行代码是合法的:
TwoGen<String,String> x = new TwoGen<String,String>("A","B");
在此T和V都是String类型。当然,如果类型参数总是相同的,就不必使用两个类型参数了。
1.4 泛型类的一般形式
在前面例子中展示的泛型语法可以一般化。下面是声明泛型类的语法:
class class-name<type-param-list>{//...
下面是声明指向泛型类的引用的语法:
class-name<type-arg-list> var-name = new class-name<type-arg-list>(cons-arg-list);
1.5 有界类型
在前面的例子中,可以使用任意类替换类型参数。对于大多情况这很好,但是限制能够传递给类型参数的类型有时是有用的。例如,假设希望创建一个泛型类,类中包含一个返回数组中数字平均值的方法。此外,希望能使用这个类计算一组任意类型数字的平均值,包括整数、单精度浮点数以及双精度浮点数。因此,希望使用类型参数以泛型化的方式指定数字类型。为了创建这样一个类,我们可能尝试写类似下面的代码:
//Stats attempts (unsuccessfully) to
//create a generic class that can compute
//the average of an array of numbers of
//any given type.
//The class contains an error!
public class Stats<T> {
T[] nums;//nums is an array of type T
//Pass the constructor a reference to
//an array of type T.
Stats(T[] o){
nums = o;
}
//Return type double in all cases.
double average(){
double sum=0.0;
for (int i=0;i<nums.length;i++)
sum += nums[i].doubleValue();//Error!
return sum / nums.length;
}
}
在Stats类中,average()方法通过调用doubleValue(),试图获得nums数组中每个数字的double版本。因为所有数值类,比如Integer以及Double,都是Number的子类,而Number定义了doubleValue()方法,所以所有数值类型的封装器都可以使用该方法。问题是编译器不知道您正试图创建只使用数值类型的Stats对象。因此,当试图编译Stats时,会报告错误,指出doubleValue()是未知的。为了解决这个问题,需要以某种方式告诉编译器,您打算只向T传递数值类型。此外,需要以某种方式确保实际上只传递了数值类型。
为了处理这种情况,Java提供了有界类型(bounded type)。在指定类型参数时,可以创建声明超类的上界,所有类型参数都必须派生自超类。这是当指定类型参数时使用extends子句完成的,如下所示:
<T extends superclass>
这样就指定T只能被superclass或其子类替代。因此superclass定义了包括superclass在内的上限制。
可以通过将Number指定为上界,修复前面显示的Stats类,如下所示:
//In this version of Stats,the type argument for
//T must be either Number,or a class derived
//from Number.
public class Stats<T extends Number> {
T[] nums;//array of Number or subclass
//Pass the constructor a reference to
//an array of type Number of subclass.
Stats(T[] o){
nums = o;
}
//Return type double in all cases.
double average(){
double sum=0.0;
for (int i=0;i<nums.length;i++)
sum += nums[i].doubleValue();//Error!
return sum / nums.length;
}
}
//Demonstrate Stats.
public class BoundsDemo {
public static void main(String[] args) {
Integer inums[] = {1, 2, 3, 4, 5};
Stats<Integer> iOb = new Stats<Integer>(inums);
double v = iOb.average();
System.out.println("iOb average is " + v);
Double dnums[] = {1.1, 2.2, 3.3, 4.4, 5.5};
Stats<Double> dob = new Stats<Double>(dnums);
double w = dob.average();
System.out.println("dob average is " + w);
//This wonnot compile because String is not a
//subclass of Number.
// String strs[]={"1","2","3","4","5"};
//Stats<String> strOb = new Stats<String>(strs);
//double x = strOb.average();
//System.out.println("strob average is "+x);
/**
* 输出结果:
* iOb average is 3.0
* dob average is 3.3
*/
}
}
注意现在使用下面这行代码声明Stats的方式:
class Stats<T extends Number>{
现在使用Number对类型T进行了限定,Java编译器知道所有T类型的对象都可以调用doubleValue()方法,因为该方法是Number声明的。就其本身来说,这是一个主要优势。除此之外,还有另外一个好处:限制T的范围也会阻止创建非数值类型的Stats对象,例如,如果尝试移除对程序底部几行代码的注释,然后重新编译,就会收到编译时错误,因为String不是Number的子类。
除了使用类作为边界之外,也可以使用接口。实际上,可以指定多个接口作为边界。此外,边界可以包含一个类和一个或多个接口。对于这种情况,必须首先指定类类型。如果边界包含接口类型,那么只有实现了那种接口的类型参数是合法的。当指定具有一个类和一个或多个接口的边界时,使用&运算符连接它们。例如:
class Gen<T extends MyClass & MyInterface>{//...
在此,通过类MyClass和接口MyInterface对T进行了限制。因此,所有传递给T的类型参数都必须是MyClass的子类,并且必须实现MyInterface接口。
1.6 使用通配符参数
类型安全虽然有用,但是有时可能会影响完全可以接受的结构。例如,对于上一个节末尾显示的Stats类,假设希望添加方法sameAvg(),该方法用于判定两个Stats对象包含的数组的平均值是否相同,而不考虑每个对象包含的数值数据的具体类型。例如,如果一个对象包含double值1.0、2.0和3.0,另一个对象包含整数值2、1和3,那么平均值是相同的。实现sameAvg()方法的一种方式是传递Stats参数,然后根据调用对象比较参数的平均值,只有当平均值相同时才返回true。例如,您希望能够向下面这样调用sameAvg()方法:
Integer inums[] = {1,2,3,4,5};
Double dnums[] = {1.1,2.2,3.3,4.4,5.5};
Stats<Integer> iob = new Stats<Integer>(inums);
Stats<Double> dob = new Stats<Double>(dnums);
if(iob.sameAvg(dob))
System.out.println("Averages are the same.");
else
System.out.println("Averages differ.");
起初,创建sameAvg()方法看起来是一个简单的问题。因为Stats是泛型化的,并且它的average()方法可以使用任意类型的Stats对象,看起来创建sameAvg()方法会很直观。遗憾的是,一旦试图声明Stats类型的参数麻烦就开始了。Stats是参数化类型,当声明这种类型的参数时,将Stats的类型参数指定为什么好呢?
乍一看,您可能会认为解决方案与下面类似,其中的T用作类型参数:
//This won't work!
//Determine if two average are the same.
boolean sameAvg(Stats<T> ob){
if(average() == ob.average())
return true;
return false;
}
这种尝试存在问题:只有当其他Stats对象的类型和调用对象的类型相同时才能工作。例如,如果调用对象时Stats<Integer>类型,那么参数ob也必须是Stats<Integer>类型。不能用于比较Stats<Double>类型对象的平均值和Stats<Short>类型对象的平均值。所以这种方式的使用范围恩债,无法得到通用的(即泛型化的)解决方案。
为了创建泛型化的sameAvg()方法,必须使用Java泛型的另一个特性:通配符(wildcard)参数。通配符参数是由"?"指定的,表示未知类型。下面是使用通配符编写的sameAvg()方法的一种方式:
//Use a wildcard
public class Stats<T extends Number> {
T[] nums;//array of Number or subclass
//Pass the constructor a reference to
//an array of type Number of subclass.
Stats(T[] o){
nums = o;
}
//Return type double in all cases.
double average(){
double sum=0.0;
for (int i=0;i<nums.length;i++)
sum += nums[i].doubleValue();//Error!
return sum / nums.length;
}
//Determine if two averages are the same.
//Notice the value of the wildcard.
boolean sameAvg(Stats<?> ob){
if(average() == ob.average())
return true;
return false;
}
}
//Demonstrate wildcard.
public class WildcardDemo {
public static void main(String[] args) {
Integer inums[] = {1, 2, 3, 4, 5};
Stats<Integer> iob = new Stats<Integer>(inums);
double v = iob.average();
System.out.println("iob average is " + v);
Double dnums[] = {1.1, 2.2, 3.3, 4.4, 5.5};
Stats<Double> dob = new Stats<Double>(dnums);
double w = dob.average();
System.out.println("dob average is " + w);
Float fnums[] = {1.0F, 2.0F, 3.0F, 4.0F, 5.0F};
Stats<Float> fob = new Stats<Float>(fnums);
double x = fob.average();
System.out.println("fob average is " + x);
//See which arrays have same average.
System.out.print("Averages of iob and dob ");
if (iob.sameAvg(dob))
System.out.println("are the same.");
else
System.out.println("differ.");
System.out.print("Averages of iob and fob ");
if (iob.sameAvg(fob))
System.out.println("are the same.");
else
System.out.println("differ.");
/**
* 输出结果:
* iob average is 3.0
* dob average is 3.3
* fob average is 3.0
* Averages of iob and dob differ.
* Averages of iob and fob are the same.
*/
}
}
最后一点:通配符不会影响创建什么类型的Stats对象,理解这一点很重要。这是由Stats声明中的extends子句控制的。通配符只是简单地匹配所有有效地Stats对象。
有界通配符
可以使用与界定类型参数大体相同的方式界定通配符参数。对于创建用于操作类层次的泛型来说,有界通配符很重要。为了理解其中的原因,下面看一个例子。分析下面的类层次,其中的类封装了坐标:
//Two-dimensional coordinates.
public class TwoD {
int x,y;
TwoD(int a,int b){
x=a;
y=b;
}
}
//Three-dimensional coordinates.
public class ThreeD extends TwoD{
int z;
ThreeD(int a, int b,int c) {
super(a, b);
z=c;
}
}
//Four-dimensional
public class FourD extends ThreeD{
int t;
FourD(int a, int b, int c,int d) {
super(a, b, c);
t=d;
}
}
在这个类层次的顶部是TwoD,该类封装了二位坐标(XY坐标)。ThreeD派生自TwoD,该类添加了第3维,创建了XYZ坐标。FourD派生自ThreeD,该类添加了第4维度(时间),生成4维坐标。
下面显示的是泛型类Coords,该类存储了一个坐标数组:
//This class holds an array of coordinate objects.
class Coords<T extends TwoD>{
T[] coords;
Coords(T[] o){
coords = o;
}
}
注意Coords指定了一个又TwoD界定的类型参数。这意味着在Coords对象中存储的所有数组将包含TwoD类或其子类的对象。
现在,假设希望编写一个方法,显示Coords对象的coords数组中每个元素的X和Y坐标。因为所有Coords对象的类型都至少有两个坐标(X和Y),所以使用通配符很容易实现,如下所示:
static void showXY(Coords<?> c){
System.out.println("X Y Coordinates:");
for(int i=0;i<c.coords.length;i++)
System.out.println(c.coords[i].x+" "+c.coords[i].y);
System.out.println();
}
因为Coords是有界的泛型类,并且将TwoD指定为上界,所以能够用于创建Coords对象的所有对象都将是TwoD类及其子类的数组。因此,showXY()方法可以显示所有Coords对象的内容。
但是,如果希望创建显示ThreeD或FourD对象的X、Y和Z坐标的方法,该怎么办呢?麻烦是,并非所有Coords对象都有3个坐标,因为Coords<TwoD>对象只有X和Y坐标。所以,如何编写能够显示Coords<ThreeD>和Coords<FourD>对象的X、Y和Z坐标的方法,而又不会阻止该方法使用Coords<TwoD>对象呢?答案是使用有界的通配符参数。
有界的通配符为参数指定上界或下界,从而可以限制方法能够操作的对象类型。最常用的有界通配符是上界,是使用extends子句创建的,具体方式和用于创建有界类型的方式大体相同。
如果对象实际拥有3个坐标的话,使用有界通配符,可以很容易创建出显示Coords对象中的X、Y和Z坐标的方法。例如下面的showXYZ()方法,如果Coords对象中存储的元素的实际类型是ThreeD(或派生自ThreedD),那么showXYZ()方法将显示这些元素的X、Y和Z坐标:
static void showXY(Coords<? extends ThreeD> c){
System.out.println("X Y z Coordinates:");
for(int i=0;i<c.coords.length;i++)
System.out.println(c.coords[i].x+" "+c.coords[i].y
+" "+c.coords[i].z);
System.out.println();
}
注意,在参数c的声明中为通配符添加了extends子句。这表明"?“可以匹配任意类型,只要这些类型为ThreeD或其派生类即可。因此,extends子句建立了”?"能够匹配的上界。因为这个界定,可以使用对Coords<ThreeD>或Coords<FourD>类型对象的引用调用showXYZ()方法,但不能使用Coords类型的引用进行调用。如果试图使用Coords<TwoD>引用调用showXYZ()方法,就会导致编译时错误,从而确保了类型安全。
下面是演示使用有界通配符参数的整个程序:
//Two-dimensional coordinates.
public class TwoD {
int x,y;
TwoD(int a,int b){
x=a;
y=b;
}
}
//Three-dimensional coordinates.
public class ThreeD extends TwoD{
int z;
ThreeD(int a, int b,int c) {
super(a, b);
z=c;
}
}
//Four-dimensional
public class FourD extends ThreeD{
int t;
FourD(int a, int b, int c,int d) {
super(a, b, c);
t=d;
}
}
//This class holds an array of coordinate objects.
public class Coords <T extends TwoD>{
T[] coords;
Coords(T[] o){ coords = o; }
}
//Demonstrate a bounded wildcard
public class BoundedWildcard {
static void showXY(Coords<?> c){
System.out.println("X Y Coordinates:");
for (int i=0;i<c.coords.length;i++)
System.out.println(c.coords[i].x+" "+c.coords[i].y);
System.out.println();
}
static void showXYZ(Coords<? extends ThreeD> c){
System.out.println("X Y Z Coordinates:");
for (int i=0;i<c.coords.length;i++)
System.out.println(c.coords[i].x+" "+c.coords[i].y
+" "+c.coords[i].z);
System.out.println();
}
static void showAll(Coords<? extends FourD> c){
System.out.println("X Y Z T Coordinates:");
for (int i=0;i<c.coords.length;i++)
System.out.println(c.coords[i].x+" "+c.coords[i].y
+" "+c.coords[i].z+" "+c.coords[i].t);
System.out.println();
}
public static void main(String[] args) {
TwoD td[] = {
new TwoD(0,0),
new TwoD(7,9),
new TwoD(18,4),
new TwoD(-1,-23)
};
Coords<TwoD> tdlocs = new Coords<TwoD>(td);
System.out.println("Contents of tdlocs.");
showXY(tdlocs);//OK,is a TwoD
// showXYZ(tdlocs);//Error,not a ThreeD
//showAll(tdlocs);//Error not a FourD
//Now,create some FourD objects
FourD fd[]={
new FourD(1,2,3,4),
new FourD(6,8,14,8),
new FourD(22,9,4,9),
new FourD(3,-2,-23,17)
};
Coords<FourD> fdlocs=new Coords<FourD>(fd);
System.out.println("Contents of fdlocs.");
//These are all OK.
showXY(fdlocs);
showXYZ(fdlocs);
showAll(fdlocs);
/**
* 输出:
* Contents of tdlocs.
* X Y Coordinates:
* 0 0
* 7 9
* 18 4
* -1 -23
*
* Contents of fdlocs.
* X Y Coordinates:
* 1 2
* 6 8
* 22 9
* 3 -2
*
* X Y Z Coordinates:
* 1 2 3
* 6 8 14
* 22 9 4
* 3 -2 -23
*
* X Y Z T Coordinates:
* 1 2 3 4
* 6 8 14 8
* 22 9 4 9
* 3 -2 -23 17
*/
}
}
一般来说,要为通配符建立上界,可以使用如下所示的通配符表达式:
<? extends superclass>
其中,superclass是作为上界的类的名称。这是一条包含子句,因为形成上界(由superclass指定的边界)的类也位于边界之内。
还可以通过为通配符添加一条super子句,为通配符指定下界。下面是一般形式:
<? super subclass>
对于这种情况,只有subclass的超类是可接受的参数。这是一条排除剧组,因此与subclass指定的类不相匹配;
1.7 创建泛型方法
正如前面的例子所显示的,泛型类中的方法可以使用类的类型参数,所以他们是自动相对于类型参数泛型化的。不过,可以声明本身使用一个或多个类型参数的泛型方法。此外,可以在非泛型类中创建泛型方法。
下面的程序声明了非泛型类GenMethDemo,并在该类中声明了静态泛型方法isIn()。isIn()方法用于判定某个对象是否是数组的成员,可以用于任意类型的对象和数据,只要数组包含的对象和将要检查对象的类型兼容即可。
//Demonstrate a simple generic method.
public class GenMethDemo {
//Determine if an object is an array.
static <T extends Comparable<T>, V extends T> boolean isIn(T x, V[] y) {
for (int i = 0; i < y.length; i++)
if (x.equals(y[i])) return true;
return false;
}
public static void main(String[] args) {
//Use isIn() on Integers.
Integer nums[] = {1,2,3,4,5};
if(isIn(2,nums))
System.out.println("2 is in nums");
if(!isIn(7,nums))
System.out.println("7 is not in nums");
//Use isIn() on Strings.
String strs[] = {"one","two","three","four","five"};
if(isIn("two",strs))
System.out.println("two is in strs");
if(!isIn("seven",strs))
System.out.println("seven is not in stars");
//Oops! Won't compile!Types must be compatible
// if (isIn("two",nums))
// System.out.println("two is not in stars");
/**
* 输出
* 2 is in nums
* 7 is not in nums
* two is in strs
* seven is not in stars
*/
}
}
首先,注意下面这行代码声明isIn()方法的方式:
static <T extends Comparable<T>,V extends T> boolean isIn(T x,V[] y){
类型参数在方法的返回类型之前声明。其次,注意T扩展了Comparable<T>。Comparable是在java.lang中声明的一个接口。实现Comparable接口的类定义了可被排序的对象。因此,限制上界为Comparable确保了在isIn()中只能使用可被比较的对象。Comparable是泛型接口,其类型参数指定了要比较的对象的类型。接下来,注意T为类型V设置了上界。因此,V必须是类T或其子类。这种关系强制只能使用相互兼容的参数来调用isIn()方法。还应当注意isIn()方法是静态的,因而可以独立于任何对象进行调用。泛型方法既可以是静态的也可以是非静态的,对此没有限制。
现在,注意在main()中调用isIn()方法的方式,使用常规的调用语法,不需要指定类型参数。这是因为参数的类型是自动辨别的,并且相应的调整T和V的类型。例如,在第一次调用中:
if(isIn(2,nums))
第一个参数的类型是Integer(由于自动装箱),这会导致使用Integer替换T。第二个参数的基类型(base type)也是Integer,因而也用Integer替换V。在第二次调用中,使用的是String类型,因而使用String替换T和V代表的类型。
尽管对于大多数泛型方法调用,类型推断就足够了,但是需要时,也可以显示指定类型参数。例如,下面显示了当指定类型参数时对isIn()方法的第一次调用:
GenMethDemo .<Integer,Integer>isIn(2,nums)
当然,在本例中,指定类型参数不会带来什么好处。而且JDK8改进了有关方法的类型推断。所以,需要显示指定类型参数的场合不是太多。
现在,注意注释掉的代码,如下所示:
// if (isIn("two",nums))
// System.out.println("two is not in stars");
如果移除注释符号,会报编译错误。原因在于V声明中extends子句中的T,对类型参数V进行了界定。这意味着V必须是T类型或其子类类型。而在此处,第一个参数是String类型,因而将T装换为Stirng;但第二个参数是Integer类型,不是String的子类,这会导致类型不匹配的编译时错误。这种强制类型安全的能力是泛型方法最重要优势之一。
用于创建isIn()方法的语法可以通用化。下面是泛型方法的语法:
<type-param-list> ret-type meth-name (param-list){//.....
对于所有情况,type-param-list是由逗号分隔的类型参数列表。注意对于泛型方法,类型参数列表位于返回类型之前。
泛型构造函数
可以将构造函数泛型化,即使它们的类不是泛型类。例如,分析下面的简短程序:
//Use a generic constructor
public class GenCons {
private double val;
<T extends Number> GenCons(T arg){
val = arg.doubleValue();
}
void showval(){
System.out.println("val: "+val);
}
}
public class GenConsDemo {
public static void main(String[] args) {
GenCons test = new GenCons(100);
GenCons test2 = new GenCons(123.5F);
test.showval();
test2.showval();
/**
* 输出:
* val: 100.0
* val: 123.5
*/
}
}
因为GenCons 指定了一个泛型类型的参数,并且这个参数必须是Number的子类,所以可以使用任意数值类型调用GenCons ,包括Integer、Float以及Double。因此,虽然GenCons不是泛型类,但是它的构造函数可以泛型化。
1.8 泛型接口
除了可以定义泛型类和泛型方法之外,还可以定义泛型接口。泛型接口的定义和泛型类相似。下面是一个例子。该例创建了接口MinMax,该接口声明了min()和max()方法,它们返回某个对象的最小值和最大值。
//A generic interface example.
//A Min/Max interface.
public interface MinMax<T extends Comparable<T>> {
T min();
T max();
}
//Now,implement MinMax
public class MyClass<T extends Comparable<T>> implements MinMax<T>{
T[] vals;
MyClass(T[] o){
vals=o;
}
//Return the minimum value in vals.
@Override
public T min() {
T v = vals[0];
for (int i=1;i<vals.length;i++)
if(vals[i].compareTo(v)<0) v = vals[i];
return v;
}
@Override
public T max() {
T v = vals[0];
for (int i=1;i<vals.length;i++)
if(vals[i].compareTo(v)>0) v = vals[i];
return v;
}
}
public class GenIFDemo {
public static void main(String[] args) {
Integer inums[] = {3,6,2,8,6};
Character chs[] = {'b','r','p','w'};
MyClass<Integer> iob = new MyClass<Integer>(inums);
MyClass<Character> cob = new MyClass<Character>(chs);
System.out.println("Max value in inums: "+iob.max());
System.out.println("Min value in inums: "+iob.min());
System.out.println("Max value in chs: "+cob.max());
System.out.println("Min value in chs: "+cob.min());
/**
* 输出:
* Max value in inums: 8
* Min value in inums: 2
* Max value in chs: w
* Min value in chs: b
*/
}
}
尽管这个程序的大多数方面应当很容易理解,但是有几个关键点需要指出。首先,注意MinMax的声明方式,如下所示:
interface MinMax<T extends Comparable<T>>{
一般而言,声明泛型接口的方式与声明泛型类相同。对于这个例子,类型参数是T,它的上界是Comparable,如前所示,这是由java.lang定义的接口,指定了比较对象的方式。它的类型参数指定了将进行比较的对象的类型。
接下来,MyClass实现了MinMax。注意MyClass的声明,如下所示:
class MyClass<T extends Comparable<T>> implements MinMax<T>{
请特别注意MyClass声明类型参数T以及将T传递给MinMax的方式。因为MinMax需要实现了Comparable的类型,所以实现类(在该例中是MyClass)必须指定相同的界限。此外,一旦建立这个界限,就不需要再在implements子句中指定。实际上,那么做是错误的。例如,下面这行代码是不正确的,不能通过编译:
//This is wrong!
class MyClass<T extends Comparable<T>>
implements MinMax<T extends Comparable<T>>
一旦建立类型参数,就可以不加修改地将之传递给接口。
一般而言,如果类实现了泛型接口,那么类也必须是泛型化的,至少需要带有将被传递给接口的类型参数。例如,下面对MyClass的声明时错误的:
class MyClass implements MinMax<T>{//Wrong!
因为MyClass没有声明参数类型,所以无法为MinMax传递类型参数。对于这种情况,标识符T是未知的,编译器会报告错误。当然,如果类实现了某种类型的泛型接口,如下所示:
class MyClass implements MinMax<Integer>{//OK
那么实现类不需要是泛型化的。
泛型接口具有两个优势。首先,可以针对不同类型的数据进行实现。其次,可以为实现接口的数据类型限制条件(即界限)。在MinMax例子中,只能向T传递实现了Comparable接口的类型。
下面是泛型接口的通用语法:
interface interface-name<type-param-list>{
在此,type-param-list是由逗号分隔的类型参数列表。当实现泛型接口时,必须指定类型参数,如下所示:
class class-name<type-param-list>
implements interface-name<type-arg-list>
1.9 泛型类层次
泛型类可以是类层次的一部分,就像非泛型类那样。因此,泛型类可以作为超类或子类。泛型和非泛型层次之间的关键区别是:在泛型层次中,类层次中的所有子类都必须向上传递超类所需要的所有类型参数。这与必须沿着类层次向上传递构造函数的参数类似。
1.9.1 使用泛型超类
下面是一个简单的类层次示例,该类层次使用了泛型超类:
//A simple generic class hierarchy
public class Gen<T> {
T ob;
Gen(T o){
ob = o;
}
//Return ob.
T getOb(){
return ob;
}
}
//A subclass of Gen.
public class Gen2<T> extends Gen<T>{
Gen2(T o) {
super(o);
}
}
在这个类层次中,Gen2扩展了泛型类Gen。注意下面这行代码声明Gen2的方式:
class Gen2<T> extends Gen<T>{
Gen2指定的类型参数T也被传递给extends子句中的Gen,这意味着传递给Gen2的任何类型也会被传递给Gen。例如下面这个声明:
Gen2<Integer> num = new Gen2<Integer>(100);
会将Integer作为类型参数传递给Gen。因此,对于Gen2中Gen部分的ob来说,其类型将是Integer。
还应当注意,除了将T传递给Gen超类外,Gen2没有再使用类型参数T。因此,即使泛型超类的子类不必泛型化,也必须指定泛型超类所需要的类型参数。
当然,如果需要的话,子类可以自由添加自己的类型参数。例如,下面是前面类层次的另外一个版本,此处的Gen2添加它自己的类型参数:
//A subclass can add its own type parameters.
public class Gen<T> {
T ob;//declare an object of type T
//Pass the constructor a reference to
//an object of Type T
Gen(T o){
ob = o;
}
//Return ob.
T getOb(){
return ob;
}
}
//A subclass of Gen that defines a second
//type parameter,called V.
public class Gen2<T,V> extends Gen<T>{
V ob2;
Gen2(T o,V o2) {
super(o);
ob2 = o2;
}
V getOb2(){
return ob2;
}
}
//Create an object of type Gen2
public class HierDemo {
public static void main(String[] args) {
//Create a Gen2 object for String and Integer.
Gen2<String,Integer> x = new Gen2<String,Integer>
("Value is: ",99);
System.out.print(x.getOb());
System.out.println(x.getOb2());
/**
* 输出:
* Value is: 99
*/
}
}
注意该版本中Gen2的声明,下所示:
class Gen2<T,V> extends Gen<T>{
在此,T是传递给Gen的类型,V是特定于Gen2的类型。V用于声明对象ob2,并且作为getOb2()方法的返回类型。在main()中创建了一个Gen2对象,它的类型参数T是String、类型参数V是Integer。
1.9.2 泛型子类
非泛型类作为泛型子类的超类是完全可以的。例如,分析下面这个程序:
//A non-generic class can be the superclass
//of a generic subclass.
//A non-generic class.
public class NonGen {
int num;
NonGen(int i){
num = i;
}
int getNum(){
return num;
}
}
//A generic subclass.
public class Gen<T> extends NonGen{
T ob;//declare an object of type T
//Pass the constructor a reference to
//an object of Type T.
Gen(T o,int i)
{
super(i);
ob = o;
}
//Return ob.
T getOb(){
return ob;
}
}
//Create a Gen object.
public class HierDemo2 {
public static void main(String[] args) {
//Create a Gen object for String.
Gen<String> w = new Gen<String>("Hello",1);
System.out.print(w.getOb()+" ");
System.out.println(w.getNum());
/**
* 输出:
* Hello 1
*/
}
}
在该程序中,注意在下面的声明中Gen继承NonGen的方式:
class Gen<T> extends NonGen{
因为NonGen是非泛型类,所以没有指定类型参数。因此,尽管Gen声明了参数类型T,但NonGen却不需要(也不能使用)。因此,Gen以常规方式继承NonGen,没有应用特殊的条件。
1.9.3 泛型层次中的运行时类型比较
运行时类型信息运算符instanceof用于判定对象是否是某个类的实例。如果对象是指定类型的实例或者可以转换为指定的类型,就返回true。可以将instanceof运算符应用于泛型类对象。下面的类演示了泛型层次的类型兼容的一些内涵:
//Use the instanceof operator with a generic class hierarchy
public class Gen<T> {
T ob;
Gen(T o){
ob = o;
}
//Return ob.
T getOb(){
return ob;
}
}
//A subclass of Gen.
public class Gen2<T> extends Gen<T>{
Gen2(T o) {
super(o);
}
}
//Demonstrate run-time type ID implications of generic
//class hierarchy.
public class HierDemo3 {
public static void main(String[] args) {
//Create a Gen object for Integers.
Gen<Integer> iOb = new Gen<Integer>(88);
//Create a Gen2 object for Integer.
Gen2<Integer> iOb2 = new Gen2<Integer>(99);
//Create a Gen2 object for Strings.
Gen2<String> strOb2 = new Gen2<String>("Generics Test");
//See if iOb2 is some from Gen2.
if(iOb2 instanceof Gen2<?>)
System.out.println("iOb2 is instance of Gen2");
//See if iOb2 is some from of Gen
if(iOb2 instanceof Gen<?>)
System.out.println("iOb2 is instance of Gen");
//See if strOb2 is a Gen2.
if(strOb2 instanceof Gen2<?>)
System.out.println("strOb2 is instance of Gen2");
//See if strOb2 is a Gen
if(strOb2 instanceof Gen<?>)
System.out.println("strOb2 is instance of Gen");
//See if iOb is an instance of Gen2,which it is not.
if(iOb instanceof Gen2<?>)
System.out.println("iOb is instance of Gen2");
//See if iOb is an instance of Gen,which it is.
if(iOb instanceof Gen<?>)
System.out.println("iOb is instance of Gen");
//The following can't be complied because
//generic type info does not exist at run time.
// if(iOb2 instanceof Gen2<Integer>)
// System.out.println("iOb2 is instance of Gen<Integer>");
/**
* 输出:
*iOb2 is instance of Gen2
* iOb2 is instance of Gen
* strOb2 is instance of Gen2
* strOb2 is instance of Gen
* iOb is instance of Gen
*/
}
}
在该程序中,Gen2是Gen的子类,Gen是泛型类,类型参数为T。在main()中创建了3个对象。第1个对象是iOb,它是Gen<Integer>类型的对象。第2个对象是iOb2,它是Gen2<Integer>类型的对象。最后一个对象strOb2,它是Gen2<String>类型的对象。
然后,该程序针对iOb2的类型执行以下这些instanceof测试:
//See if iOb2 is some from Gen2.
if(iOb2 instanceof Gen2<?>)
System.out.println("iOb2 is instance of Gen2");
//See if iOb2 is some from of Gen
if(iOb2 instanceof Gen<?>)
System.out.println("iOb2 is instance of Gen");
正如输出所显示的,这些测试都是成功的。在第1个测试中,根据Gen2<?>对iOb2进行测试。这个测试成功了,因为很容易就可以确定iOb2是某种类型的Gen2对象。通过使用通配符,instanceof能够检查iO2是否是Gen2任意特定类型的对象。接下来根据超类类型Gen<?>测试iOb2。这个测试也为true,因为iOb2是某种形式的Gen类型,Gen是超类。在main()方法中,接下来的几行代码显示了对strOb2进行的相同测试(并且测试结果也相同)。
接下来对iOb进行测试,iOb是Gen<Integer>(超类)类型的对象,通过下面这些代码进行测试:
//See if iOb is an instance of Gen2,which it is not.
if(iOb instanceof Gen2<?>)
System.out.println("iOb is instance of Gen2");
//See if iOb is an instance of Gen,which it is.
if(iOb instanceof Gen<?>)
System.out.println("iOb is instance of Gen");
第1个测试失败了,因为iOb不是某种类型的Gen2对象。第2个测试成功了,因为iOb是某种类型的Gen对象。
现在,仔细分析下面这些被注释掉的代码行:
//The following can't be complied because
//generic type info does not exist at run time.
// if(iOb2 instanceof Gen2<Integer>)
// System.out.println("iOb2 is instance of Gen<Integer>");
正如注释所说明的,这些代码行不能被编译,因为它们试图将iOb2与特定类型的Gen2进行比较,在这个例子中是Gen<Integer>进行比较。请记住,在运行时不能使用泛型类型信息。所以,instanceof无法知道iOb2是否是Gen2<Integer>类型的实例。
1.9.4 强制转换
只有当两个泛型实例的类型相互兼容并且它们的类型参数也相同时,才能将其中的一个实例转换成为另一个实例。例如,对于前面的程序,下面这个转换是合法的:
(Gen<Integer>)iOb2//legal
因为iOb2是Gen<Integer>类型的实例。但是,下面这个转换:
(Gen<Long>)iOb//illegal
不是合法的,因为iOb2不是Gen<Long>类型的实例。
1.9.5 重写泛型类的方法
可以像重写其他任何方法那样重写泛型类的方法。例如,分析下面这个程序,该程序重写了getOb()方法:
//Overriding a generic in a generic class.
public class Gen<T> {
T ob;//declare an object of type T
//Pass the constructor a reference to
//an object of type T.
Gen(T o){
ob = o;
}
//Return ob.
T getOb(){
System.out.print("Gen's getOb():");
return ob;
}
}
//A subclass of Gen that overrides getOb().
public class Gen2<T> extends Gen<T>{
Gen2(T o) {
super(o);
}
//Override getOb().
T getOb(){
System.out.print("Gen2's getOb():");
return ob;
}
}
//Demonstrate generic method override.
public class OverrideDemo {
public static void main(String[] args) {
//Create a Gen object for Integers.
Gen<Integer> iOb = new Gen<Integer>(88);
//Create a Gen2 object for Integers.
Gen2<Integer> iOb2 = new Gen2<Integer>(99);
//Create a Gen2 object for Strings.
Gen2<String> strOb2 = new Gen2<String>("Generics Test");
System.out.println(iOb.getOb());
System.out.println(iOb2.getOb());
System.out.println(strOb2.getOb());
/**
* 输出:
* Gen's getOb():88
* Gen2's getOb():99
* Gen2's getOb():Generics Test
*/
}
}
正如输出所确认的,为Gen2类型的对象调用重写版本的getOb()方法,但是为Gen类型的对象调用超类中的版本。
1.10 泛型的类型推断
从JDK7开始,可以缩短用于创建泛型类实例的语法。首先,分析下面的泛型类:
class MyClass<T,V>{
T ob1;
V ob2;
MyClass(T o1,V o2){
ob1 = o1;
ob2 = o2;
}
}
在JDK7之前,为了创建MyClass的实例,需要使用与下面类似的语句:
MyClass<Integer,String> mcOb = new MyClass<Integer,String>(98,"A String");
在此,类型参数(Integer和String)被指定了两此:第1次是在声明mcOb时指定的,第2次是当使用new创建MyClass实例时指定的。自动JDK5引入泛型以来,这是JDK7以前所有版本所要求的形式。尽管这种形式本身没有任何错误,但是相对于需要来说有些繁琐。在new子句中,类型参数的类型可以立即根据mcOb的类型推断出;所以,实际上不需要第2次指定。为了应对这类情况,JDK7增加了避免第2次指定类型参数的语法元素。
现在,可以重写前面的声明,如下所示:
MyClass<Integer,String> mcOb = new MyClass<>(98,"A String");
注意,实例创建部分简单地使用<>,这是一个空的类型参数列表,这被称为菱形运算符。它告诉编译器,需要推断new表达式中构造函数所需要的类型参数。这种类型推断语法的主要优势是缩短了有时相当长的声明语句。
前面的声明可以一般化。当使用类型推断时,用于泛型引用和实例创建的声明语法具有如下所示的一般形式:
class-name<type-arg-list> var-name = new class-name<>(cons-arg-list);
在此,new子句中构造函数的类型参数列表是空的。
也可以为参数传递应用类型推断。例如,如果为MyClass添加下面的方法:
boolean isSame(MyClass<T,V> o){
if(ob1 == o.ob1 && ob2 == o.ob2) return true;
else return false;
}
那么下面的调用时合法的:
if(mc.isSame(new MyClass<>(1,"test")))
System.out.println("Same");
在这个例子中,可以推断传递给isSame()方法的类型参数。
1.11 擦除
通常,不必知道Java编译器将源代码转换为对象代码的细节。但是对于泛型而言,大致了解这个过程是很重要的,因为这揭示了泛型特性的工作原理——以及为什么它们的行为有时有点令人惊奇。为此,接下来简要讨论Java实现泛型的原理。
影响泛型以何种方式添加到Java中的一个重要约束是:需要与以前的Java版本兼容。简单地说,泛型代码必须能够与以前的非泛型代码相兼容。因此,对Java语言的语法或JVM所做的任何修改必须避免破坏以前的代码。为了满足这条约束,Java使用擦除实现泛型。
大体而言,擦除的工作原理如下:编译Java代码时,所有泛型信息被移除(擦除)。这意味着使用它们的界定类型替换类型参数,如果没有显示地指定界定类型,就使用Object,然后应用适当的类型转换(根据类型参数而定),以保持与类型参数所指定类型的兼容性。编译器也会强制实现这种类型兼容性。使用这种方式实现泛型,意味着在运行时没有类型参数。它们只是一种源代码机制。
桥接方法
编译器偶尔需要为类添加桥接方法(bridge method),用于处理如下情形:子类中重写方法的类型擦除不能产生与超类中方法相同的擦除。对于这种情况,会生成使用超类类型擦除的方法,并且这个方法调用具有由子类指定的类型擦除的方法。当然,桥接方法只会在字节码级别发生,我们不能看到,也不能使用。
尽管通常不需要关心桥接方法,但是查看产生桥接方法的情形还是有意义的。分析下面的程序:
//A situation that creates a bridge method.
public class Gen<T> {
T ob;//declare an object of type T
//Pass the constructor a reference to
//an object of type T.
Gen(T o){
ob = o;
}
//Return ob.
T getOb(){
System.out.print("Gen's getOb():");
return ob;
}
}
//A subclass of Gen.
public class Gen2 extends Gen<String>{
Gen2(String o) {
super(o);
}
//Override getOb().
//A String-specific override of getOb()
String getOb(){
System.out.print("You called String getOb():");
return ob;
}
}
//Demonstrate a situation that requires a bridge method.
public class BridgeDemo {
public static void main(String[] args) {
//Create a Gen2 object for Strings.
Gen2 strOb2 = new Gen2("Generics Test");
System.out.println(strOb2.getOb());
/**
* 输出:
* You called String getOb():Generics Test
*/
}
}
在这个程序中,子类Gen2扩展了Gen,但是使用特定于String的Gen版本,就像声明显示的那样:
public class Gen2 extends Gen<String>{
此外,在Gen2中,对getOb()方法进行了重写,指定String作为返回类型:
//A String-specific override of getOb()
String getOb(){
System.out.print("You called String getOb():");
return ob;
}
所有这些都是可以接受的。唯一的麻烦是由类型擦除引起的,本来期望以下形式的getOb()方法:Object getOb(){//…
为了解决这个问题,编译器生成一个桥接方法,这个桥接方法使用调用String版本的那个签名。因此,如果检查由javap为Gen2生成的类文件,就会看到以下方法:
class Gen2 extends Gen<java.lang.String>{
Gen2(java.lang.String);
java.lang.String getOb();
java.lang.Object getOb();//bridge method
}
可以看出,已经包含了桥接方法。
对于这个示例,还有最后一点需要说明。注意两个getOb()方法之间唯一的区别是它们的返回类型。在正常情况下,这会导致错误,但是因为这种情况不是在我们编写的源代码中发生的,所以不会引起问题,并且JVM会正确地进行处理。
1.12 模糊性错误
泛型的引入,增加了引起一种新类型错误——模糊性错误的可能,必须注意防范。当擦除导致两个看起来不同的泛型声明,在擦除之后变成相同的类型而导致冲突时,就会发生模糊性错误。下面是一个涉及方法重载的例子:
//Ambiguity caused by erasure on
//overload methods.
public class MyGenClass<T,V> {
T ob1;
V ob2;
//These two overloaded methods are ambiguous
//and will not compile.
void set(V o){
ob1 = o;
}
void set(V o){
ob2 = o;
}
}
注意MyGenClass声明了两个泛型类型的参数:T和V。在MyGenClass中,试图根据类型参数T和V重载set()方法。这看起来是合理的,因为T和V表面上是不同的类型。但是,在此有两个模糊性问题。
首先,当编写MyGenClass时,T和V实际上不需要是不同的类型。例如,像下面这样构造MyGenClass对象(在原则上)是完全正确的:
MyGenClass<String,String> obj = new MyGenClass<String,String>()
对于这种情况,T和V都被String替换。这使得set()方法的两个版本完全相同,这当然是错误。
第二个问题,也是更基础的问题,对set()方法的类型擦除会使两个版本都变为如下形式:
void set(Object o){//...
因此,在MyGenClass中试图重载set()方法本身就是含糊不清的。
修复模糊性错误很棘手。例如,如果知道V总是某种Number类型,那么您可能会尝试向下面这样改写其声明,从而修复MyGenClass:
class MyGenClass<T,V extends Number>{//almost OK!
上述修改使MyGenClass可以通过编译,并且甚至可以像下面这样实例化对象:
MyGenClass<String,Number> x = new MyGenClass<String,Number>();
这种方式可以工作,因为Java能够准确地确定调用哪个方法。但是,当您试图使用下面这行代码时,会出现模糊性问题:
MyGenClass<Number,Number> x = new MyGenClass<Number,Number>();
对于这种情况,T和V都是Number,将调用哪个版本的set()方法呢?现在,对set()方法的调用是模糊不清的。
坦白地说,在前面的例中,使用两个独立的方法名会更好些,而不是试图重载set()方法。通常,模糊性错误的解决方案及调整代码结构,因为模糊性通常意味着在设计中存在概念性错误。
1.13 使用泛型的一些限制
使用泛型时有几个限制需要牢记。这些限制涉及类型参数的创建、静态成员、异常以及数组。下面逐一分析这些限制。
1.13.1 不能实例化类型参数
不能创建类型参数的实例。例如,分析下面这个类:
//Can't create an instance of T.
class Gen<T>{
T ob;
Gen(){
ob = new T();//Illegal!!!
}
}
在此,试图创建T的实例,这是非法的。原因很容易理解:编译器不知道创建哪种类型的对象。T只是一个占位符。
1.13.2 对静态成员的一些限制
静态成员不能使用在类中声明的类型参数。例如,下面这个类中的两个静态成员都是非法的:
class Wrong<T>{
//Wrong,no static variables of type T.
static T ob;
//Wrong,no static method an use T.
static T getOb(){
return ob;
}
}
尽管不能声明某些静态成员,它们使用由类声明的类型参数,但是可以声明静态的泛型方法,这种方法可以定义它们自己的泛型参数,就像本篇前面所做的那样。
1.13.3 对泛型数组的一些限制
对数组有两条重要的泛型限制。首先,不能实例化元素类型为类型参数的数组。其次,不能创建特定类型的泛型引用数组。下面的简短程序演示了这两种情况:
//Generics and arrays.
public class Gen<T extends Number> {
T ob;
T vals[];//OK
Gen(T o,T[] nums)
{
ob = o;
//This statement is illegal.
//vals = new T[10];//can't create an array of T
//But,this statement is OK.
vals = nums;//OK to assign reference to existent array
}
}
public class GenArrays {
public static void main(String[] args) {
Integer n[]={1,2,3,4,5};
Gen<Integer> iOb = new Gen<Integer>(50,n);
//Can't create an array of type-specific generic reference.
//Gen<Integer> gens[] = new Gen<Integer>[10];//Wrong!
//This is OK
Gen<?> gens[] = new Gen<?>[10];//OK
}
}
正如该程序所显示的,声明指向类型T的数组的引用是合法的,就像下面这行代码这样:
T vals[];//OK
但是,不能实例化T的数组,就像注释掉的这行代码试图所做的那样:
//vals = new T[10];//can't create an array of T
不能创建T的数组,原因是编译器无法知道实际创建什么类型的数组。
然而,当创建泛型类的对象时,可以向Gen()方法传递对类型兼容的数组的引用,并将引用赋给vals,就像程序在下面这行代码中所做的那样:
vals = nums;//OK to assign reference to existent array
这行代码可以工作,因为传递给Gen的数组的类型是已知的,和创建对象时T的类型相同。
在main()方法中,注意不能声明指定特定泛型类型的引用的数组。也就是说,下面这行代码不能编译:
//Gen<Integer> gens[] = new Gen<Integer>[10];//Wrong!在这里插入代码片
不过,如果使用通配符的话,可以创建指定泛型类型的引用的数组,如下所示:
Gen<?> gens[] = new Gen<?>[10];//OK
相对于使用原始类型数组,这种方式更好些,因为至少仍然会强制进行某些类型检查。
1.13.4 对泛型异常的限制
泛型类不能扩展Throwable,这意味着不能创建泛型异常类。