泛型程序设计
为什么要使用泛型程序设计?
泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。
例如ArrayList<T>
,其中T
可以为任意对象,这也就是说该ArrayList
的操作都是以T
为基本单位的,T
称为类型参数(type parameters)。ArrayList
类有一个类型参数用来指示元素的类型
ArrayList<T> list = new ArrayList<T>();
在使用时一看就知道list
包含的是T
类型对象
在JAVA SE7及以后的版本,构造函数中可以省略泛型类型
ArrayList<T> list = new ArrayList<>();
省略的类型可以从变量的类型推断得出
泛型程序设计使在对list
中的对象进行使用时,可以避免使用错误的方法或参数,让程序具有更好额可读性和安全性。
定义简单泛型类
一个泛型类(generic class)就是具有一个或者多个类型变量的类。对于这个类来说,我们只关注泛型,而不会为数据储存的细节烦恼。
public class Pair<T>{
private T first;
private T second;
public Pair(){
first = null; second = null;
}
public Pair(T first , T second){
this.first = first;
this.second = second;
}
public T getFirst(){return first;}
public T getSecond(){return second;}
public void setFirst(T newValue){first =newValue;}
public void setSecond(T newValue){second = newValue;}
}
pair
类引入一个类型变量T
,用尖括号<>
括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义Pair
类,其中第一个域和第二个域使用不同的类型
public class Pair <T,U> {...}
在java中,类型变量使用大写形式且较短为常见。在java库中,使用变量
E
表示集合的元素类型,K
和V
分别表示表的关键字(key)和值(Value)的类型。T
以及附近U
、S
表示任意类型
泛型方法
定义一个带有类型参数的简单方法
class ArrayAlg{
public static <T> T getMiddle(T...a){
return a[a.length/2];
}
}
这个方法使在普通类中定义的,而不是在泛型类中定义的,但这是一个泛型方法。注意,类型变量放在修饰符的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
String middle = ArrayAlg.<String>getMiddle("aa","bb","cc");
在这种情况下(也是大多数情况下),方法调用中可以省略<String>
类型参数。编译器有足够的信息能够推断出所调用的方法。它用String[]
与泛型类型T[]
进行匹配并推断出T
一定是String
。也就是说,可以调用
String middle = ArrayAlg.getMiddle("aa","bb","cc");
几乎在大多数情况下,对于泛型方法的类型引用没有问题。偶尔,编译器也会给出错误提示:
double middle = ArrayAlg.getMiddle(3.14 , 123 , 0);
错误消息会以晦涩的方法指出:解释这句代码有两种方法,而且这两种方法都是合法的。简单地说,编译器会自动打包参数为一个Double
和两个Integer
对象,而后寻找这些类的共同超类型。事实上,找到2个这样的超类:Number
和Comparable
接口,其本身也是一个泛型类型。这种情况下的措施就是将所有参数改为Double
类型
类型变量的限定
有时候,类或方法需要对类型变量加以束缚。例如求数组中最小元素
class Test{
public static <T> T min(T[] a){
if(a ==null||a.length==0){
return null;
}else{
T smallest = a[0];
for(int i = 1 ; i<a.length ;i++ ){
if(smallest.compareTo(a[i]>0))smallest = a[i];
return smallest;
}
}
}
}
但是变量smallest
的类型为T
,这意味着它可以是任何一类的对象,如何确信T
所属的类有compareTo
方法?
解决问题的方法是将T
限制为实现了Comparable
接口的类。可以通过对类型变量T
设置限定实现这一点
public static <T extends Comparable> T min(T[] a){...}
现在,泛型的min
方法只能被实现了Comparable
接口的类的数组调用。
但为什么实现接口的关键字是extends
而不是implements
?毕竟Comparable
是一个接口
<T extends BoundingType>
表示T
应该是绑定类型的子类型(subType)。T
和绑定类型可以是累,也可以是接口。选择extends
是因为更接近子类的概念。
一个类型变量或通配符可以有多个限定,例如
T extends Comparable & Serializable
限定类型用&
分隔,而类型变量用,
分隔。
Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
public class A <T extends List&Comparable&Serializable> implements Serializable{}
约束与局限性
不能使用基本类型实例化类型参数
没有
Pair<double>
,只有Pair<Double>
。原因是类型擦除,擦除之后Object
类型不能储存double
的值。而当包装器类型不能接受替换时,可以使用独立的类和方法处理
运行时类型查询只适用于原始类型
所有类型查询只能产生原始类型。
例如
if(a instanceof Pair<String>) //ERROR if(a instanceof Pair<T> )//ERROR Pair<String> p = (Pair<String>) a;//WARNING
无论何时使用
instance
或涉及泛型类型的强制类型转换表达式都会看到一个警告。同样,getClass总是返回原始类型
Pair<String> a = ...; Pair<Double> b = ...; if(a.getClass()==b.getClass()) //always true
因为
getClass
的返回结果都是Pair.class
不能创建参数化的数组
例如
Pair<String>[] table = new Pair<String>[10];//ERROR
擦除之后,table的类型是Pair[],可以转换为Object[]:
Object[] objarray = table;
数组会记住它的元素类型,如果试图储存其他类型的元素,就会跑出一个
ArrayStoreException
异常objarray[0] = "hello" //ERROR component type is Pair
Varargs警告
向参数个数可变的方法传递一个泛型类型的实例。例如下面这个方法
public static <T> void addAll(Collection<T> coll,T ... ts){ for(t:ts) coll.add(t); }
实际上参数
ts
是一个数组,包含提供的所有实参Collection <Pair<String>> table = ...; Pair<String> pair1 = ...; Pair<String> pair2 = ...; addAll(table,pair1,pair2);
为了调用这个方法,JVM必须建立一个
Pair<String>
数组,但这违反了前面的规则。但你只会得到一个警告。可以使用
@SuppressWarnings("unchecked")
或者在Java SE7中使用@SafeVarargs
来标注addAll
方法
不能实例化类型变量
不能使用
new T(...)
,new T[...]
或者T.class
这样的表达式中的类型变量。例如public Pair(){first = new T();} //ERROR
因为在类型擦除后会使得
first
为Object
,而本意肯定并非如此。不过可以使用烦着调用Class.newInstance
方法来构造泛型对象。但非下面这样first = T.class.newInstance(); //ERROR
表达式是不合法的,必须像下面这样
public static <T> Pair<T> makePair(Class<T> cl){ try{ return new Pair<>(cl.newInstance(),cl.newInstance()); }catch(Exception e){ return null; } }
这个方法可以按照下列方式调用
Pair<String> p = Pair.makePair(String.class);
注意,
Class
类本身是泛型。例如,String.class
是一个也是唯一Class<String>
的实例。因此,makePair
可以判断pair
的类型。不能构造一个泛型数组,类型参数会让这个方法永远构造
Object[]
数组。如果数组仅仅作为一个类的私有实例,就可以将这个数组声明为
Object[]
,并在获取元素时进行类型转换。
泛型类的静态上下文中类型变量无效
不能在静态实例域或方法中引用类型变量
public class Pair<T>{ private T pair1; //ERROR public T getPair1(){ //ERROR return pair1; } }
不能抛出或捕获泛型类的实例
不能抛出或捕获泛型类对象,甚至泛型类扩展
Throwable
都是不合法的catch
子句中不能使用类型变量public static <T extends Throwable> void do(Class<T > t){ try{ ... }catch(T e){ //ERROR ... } }
不过,在异常规范中使用类型变量是允许的
public static <T extends Throwable> void do(Class<T > t) throws T{ //OK try{ ... }catch(Throwable cause){ t.initCause(cause); throw t; } }
擦除后的冲突
要想支持擦除的转换,就需要强行限制一个类或者类型变量不能同时成为两个接口类型的子类,而这两个接口是同一接口的不同参数化
泛型类型的继承规则
永远可以将一个参数化类型转换为一个原始类型。
例如Student
是Person
的一个子类,但是Pair<Student>
并不是Pair<Person>
的子类,他们抽象化后都是Pair<T>
通配符类型
Pair<? extends Employee>
表示任何泛型Pair
类型,它的类型参数是Employee
的子类,如Pair<Manager>
,但不是Pair<String>
假如要编写一个打印雇员的方法,例如
public static void printBuddies(Pair<Employee> p){
System.out.print(p.getFirst.getName());
}
正如泛型类型的继承规则,不能将Pair<Manager>
传递给这个方法,这一点很受限,但是可以用通配符解决
public static void printBuddies(Pair<? extends Employee> p )
类型Pair<Manager>
是Pair<? extends Employee>
的子类型
通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定(supertype bound):
? super Manager
这个通配符限制为Manager
的所有超类型。
带有超类型限定的通配符,可以为方法提供参数,但不能使用返回值。例如,Pair<? super Manager>
有方法
void setFirst(? super Manager)
? super Manager getFirst()
编译器不知道setFirst
方法的确切类型,但是可以用任意Manager
对象调用它,而不能用Employee
对象调用。然而,如果调用getFirst
,返回的对象类型就不会得到保证,只能把它赋给一个Object
直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取
无限定通配符
无限制的通配符Pair<?>
看起来好像与原始的Pair
类型一样,实际上有很大不同。类型Pair<?>
有方法如下所示:
? getFirst()
void setFirst(?)
getFirst()
的返回值只能赋给一个Object
,setFirst
方法不能被调用,甚至不能用Object
调用Pair<?>
和Pair
本质的不同在于:可以用任意Object
对象调用原始的Pair
类的setObject
方法
可以调用
setFirst(null)
为什么要使用这样的类型?例如,下面这个方法用来测试一个pair
是否包含一个null
引用,它不需要实际的类型。
public static boolean hasNulls(Pair<?> p){
return p.getFirst()==null||p.getSecond()==null;
}
通过将hasNulls
转换成泛型方法,可以避免使用通配符类型
public static <T> boolean hasNulls(Pair<T> p)
但是,带有通配符的版本可读性更强
通配符捕获
一个交换pair
元素的方法
public static void swap(Pair<?> P)
通配符不是类型变量,因此,不能再编写代码中使用“?”作为一种类型。也就是说下列代码是非法的
? t = p.getFirst() //ERROR
p.setFirst(p.getSecond())
p.setSecond(t)
在交换的时候必须临时保存第一个元素。不过我们可以写一个辅助方法swapHelper
public static <T> void swapHelper(Pair<T> p){
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
swapHelper
是一个泛型方法,而swap
不是,它具有固定的Pair<?>
类型参数
现在可以用swap
调用swapHepler
public static void swap{swapHepler(p);}
这种情况下,swapHepler
方法的参数T
捕获通配符。他不知道是哪种类型的通配符,但是,这是明确的类型,而且<T>swapHepler
的定义只有在T
指出类型时才有明确的含义
反射和泛型
现在,Class
类是泛型的。例如,String.class
实际上是一个也是唯一一个Class`类的对象。
类型参数十分有用,这是因为它允许Class<T>
方法的返回类型更加具有针对性。下面Class<T>
中的方法就是用了类型参数
T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? super T> getSuperClass()
Constructor<T> getConstructor(Class ... param)
Constructor<T> getDeclaredConstructor(Class ... param)
newInstance
方法返回一个实例,这个实例所属的类由默认的构造器获得。他的返回类型目前声明为T
,其类型与Class<T>
描述的类相同,这样就免除了类型转换。
如果给定的类型确实是T
的一个子类型,cast
方法就会返回一个现在声明为类型T
的对象,否则,抛出一个BadCastException
异常
如果这个类不是enum
类或者类型T
的枚举值的数组,getEnumConstans
方法将返回null
最后,getConstructor
与getdeclaredConstructor
方法返回一个Constructor<T>
对象。Constructor
类也已经变成泛型,以便newInstance
方法有一个正确的返回类型