Java泛型
1.为什么要使用泛型程序设计?
泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。举个例子:利用泛型,我们可以不用为了聚集String
和File
对象分别设计不同的类。
下面从类型参数的好处的角度来简单谈谈泛型程序设计机制的演化,这能帮助我们更好的理解泛型。
在Java中增加泛型类之前,泛型程序设计是用继承来实现的。ArrayList
类只维护一个Object
引用的数组:
//before generic classes
public class ArrayList {
private Object[] elementData;
...
public Object get(int i){...}
public void add(Object o){...}
}
这种方法有两个问题:
-
当获取一个值时必须进行强制类型转换
ArrayList files = new ArrayList(); String fileName = (String) files.get(0);
-
这里没有错误检查,可以向数组列表中添加任何类的对象,对于这个调用,编译和运行都不会出错,但在其他地方,如果将
get
的结果强制类型转换为String
类型,就会产生一个错误,因为添加的是个File
对象,File
对象无法转换为String
对象:file.add(new File("..."));
于是泛型提供了一个更好的解决方案:类型参数(type parameters)。ArrayList
类有一个类型参数用来指示元素的类型,这使得代码具有更好的可读性,人们一看就知道这个数组列表中包含的是String
对象:
ArrayList<String> files = new ArrayList<String>();
注:在Java SE 7及以后的版本中,构造函数中可以省略泛型类型:
ArrayList<String> files = new ArrayList<>();
2. 定义简单泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类。下面使用一个简单的Pair
类作为例子:
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 this.first;
}
public T getSecond(){
return this.second;
}
public void setFirst(T newValue){
this.first = newValue;
}
public void setSecond(T newValue){
this.second = newValue
}
}
Pair
类引入了一个类型变量T
,用尖括号<>
括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义第一个域和第二个域使用不同类型变量的Pair
类:
public class Pair<T, U>{...}
用具体的类型替换类型变量就可以实例化泛型类型,可以把结果想象成带有构造器的普通类:
Pair<String>();
Pair<String>(String, String);
程序示例:
public class PairTest1 {
public static void main(String[] args) {
String[] words = {"Marry", "had", "a", "little", "lamb"};
Pair<String> mm = ArrayAlg.minmax(words);
System.out.println("min = " + mm.getFirst());
System.out.println("max = " + mm.getSecond());
}
}
class ArrayAlg{
/**
*Gets the minimum and maximum of an array of Strings
*/
public static Pair<String> minmax(String[] a){
if(a == null || a.length == 0) return null;
String min = a[0], max = a[0];
for(int i = 1; i < a.length; i++){
if(min.compareTo(a[i]) > 0) min = a[i];
if(max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
3. 泛型方法
前面介绍了如何定义一个泛型类。实际上,还可以定义一个带有类型参数的简单方法:
class ArrayAlg{
public static <T> T getMiddle(T... a){
return a[a.length / 2];
}
}
泛型方法可以定义在普通类中,也可以定义在泛型类中。注意,类型变量放在修饰符(这里是public static
)的后面, 返回类型的前面。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
实际上,方法调用中可以省略<String>
类型参数,编译器有足够的信息能够推断出所调用的方法,也就是说,可以调用:
String middle = ArrayAlg.getMiddle("John", "Q.", "Public");
4. 类型变量的限定
有时, 类或方法需要对类型变量加以约束。下面是一个典型的例子,我们要计算数组中的最小元素:
Class ArrayAlg{
public static <T> T min(T[] a){
if(a == null || a.length == 0){
return null;
}
T smallest = a[0];
for(int i = 1; i < a.length; i++){
if(smallest.compareTo(a[i]) > 0){
smallest = a[i];
}
}
return smallest;
}
}
事实上,这个程序有个问题。在min
方法的代码内部,变量smallest
类型为T
,这意味着它可以是任何一个类的对象,那么我们怎么确保T
所属的类有compareTo
方法呢?
解决这个问题的方案是将T
限制为实现了Comparable
接口(只含一个方法compareTo
的标准接口)的类。可以通过对类型变量T
设置限定(bound)实现这一点:
public static <T extends Comparable> T min(T[] a) ...
现在,泛型的min
方法只能被实现了Comparable
接口的类的数组调用。
一个类型变量或通配符可以有多个限定,例如:
T extends Comparable & Serializable
限定类型用&
分隔,而逗号用来分隔类型变量。
在Java的继承中,可以根据需要拥有多个接口超类型,但限定中至多只能有一个类,如果用一个类作为限定,那么这个类必须是限定列表中的第一个。
程序示例:
import java.time.LocalDate;
public class PairTest2 {
public static void main(String[] args) {
LocalDate[] birthDays = {
LocalDate.of(1906, 12,9),
LocalDate.of(1815,12,10),
LocalDate.of(1903, 12, 3),
LocalDate.of(1910, 6, 22)
};
Pari<LocalDate> mm = ArrayAlg.minmax(birthDays);
System.out.println("min = " + mm.getFirst());
System.out.println("max = " + mm.getSecond());
}
}
class ArrayAlg{
/**
* Gets the minimum and maximum of an array of objects of type T.
*/
public static <T extends Comparable> Pair<T> minmax(T[] a){
if(a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for(int i = 1; i < a.length; i++){
if(min.compareTo(a[i]) > 0) min = a[i];
if(max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min, max);
}
}
5. 泛型代码和虚拟机
虚拟机没有泛型类型对象, 所有对象都属于普通类。
(1)类型擦除
无论何时定义一个泛型类型,都自动提供一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object
)。
例如,Pair<T>
的原始类型如下所示:
public class Pair{
private Object first;
private Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst(){
return this.first;
}
public Object getSecond(){
return this.second;
}
public void setFirst(Object newValue){
this.first = newValue;
}
public void setSecond(Object newValue){
this.second = newValue;
}
}
这是因为T
是一个无限定的变量,所以直接用Object
替换。替换的结果是一个普通的类,就好像泛型引入Java
语言之前已经实现的那样。
原始类型要用第一个限定的类型变量来替换,如果没有给定限定就要用Object
替换。如上例所示。现在假定有一个不同的类型:
public class Interval <T extends Comparable & Serializable> implements Serializeble{
private T lower;
private T upper;
...
private Interval(T first, T second){
if(first.compareTo(second) <= 0){
lower = first;
upper = second;
}
else{
lower = second;
upper = first;
}
}
}
原始类型Interval
如下所示:
public class Interval implements Serializable{
private Comparable lower;
private Comparable upper;
...
private Interval(Comparable first, Comparable second){...}
}
注: 如果切换限定:class Interval<T extends Serializable & Comparable>
, 那么原始类型用Serializable
替换T
,而编译器在必要时要向Comparable
插入强制类型转换。为了提高效率,应该将标签(tagging
)接口(即没有方法的接口)放在边界列表的末尾。
(2)翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如下面这个语句序列:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
擦除getFirst
的返回类型后将返回Object
类型。编译器自动插入Employee
的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法
Pari.getFirst
的调用 - 将返回的
Object
类型强制转换为Employee
类型
当存取一个泛型域是也要插入强制类型转换。假设Pair
类的first
域和second
域都是公有的,表达式Employee buddy = buddies.first
也会在结果字节码中插入强制类型转换。
(3)翻译泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为public static <T extends Comparable> T min(T[] a)
是一个完整的方法族,而擦除类型之后,只剩下一个方法:public static Comparable min(Comparable[] a)
,此时类型参数T
已经被擦除了, 只留下了限定类型Comparable
。
方法的擦除带来了两个复杂问题,看一看下面的示例:
class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate second){
if(second.compareTo(getFirst()) >= 0){
super.setSecond(second);
}
...
}
}
一个日期区间是一对LocalDate
对象,并且需要覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除后变成:
class DateInterval extends Pair{
public void setSecond(LocalDate second){
...
}
}
这就很奇怪了,因为存在一个从Pair
继承的setSecond
方法,即:
public void setSecond(Object second)
这显然是一个不同的方法,因为他有一个不同类型的参数Object
,而不是LocalDate
。然而,不应该不一样。考虑下面的语句序列:
DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);
在这里,希望对setSecond
的调用具有多态性,并调用最合适的那个方法。由于pair
引用DateInterval
对象,所以应该调用DateInterval.setSeconde
。问题在于类型擦除与 多态发生了冲突。要解决这个问题,就需要编译器在DateInterval
类中生成一个桥方法(bridge method):
public void setSecond(Object second){
setSecond(Date) second;
}
要想了解它的工作原理,要仔细跟踪下列语句的执行pair.setSecond(aDate)
变量pair
已经声明为类型Pair<LocalDate>
,并且这个类型只有一个简单的方法叫setSecond
,即setSecond(Object)
。虚拟机用pair
引用的对象调用这个方法。这个对象是DateInterval
类型的,因而将会调用DateInterval.setSecond(Object)
方法。这个方法是合成的桥方法,他将调用DateInterval.setSecond(Date)
,而这正是我们所期望的操作效果。
在DateInterval
类中,有两个getSecond
方法:
LocalDate getSecond() //define in DateInterval
Object getSecond() //override the method defined in Pair to call the first method
我们在程序中不能这样编写java
代码,因为具有相同参数类型的两个方法是不合法的。它们都没有参数。但是在虚拟机中可以用参数类型和返回类型确定一个方法。因此编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确的处理这一情况。
总之,需要记住有关Java
泛型转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都用他们的限定类型替换
- 桥方法被用来合成保持多态
- 为保持类型安全性,必要时插入强制类型转换
6. 约束和局限性
(1)不能用基本类型实例化类型参数
例如,没有Pair<double>
,只有Pair<Double>
。原因就在于类型擦除。擦除之后,Pair
类含有Object
类型的域,而Object
不能存储double
值。
(2)运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型,因此,所有的类型查询只产生原始类型。
(3) 不能创建参数化类型的数组
不能实例化参数化类型数组,例如
Pair<String>[] table = new Pari<String>[10];//Error
如果需要收集参数化类型对象,只有一种安全而有效的方法:使用ArrayList: ArrayList<Pair<String>>
。
(4) Varargs 警告
这里讨论一个向参数个数可变的方法传递一个泛型类型的实例
考虑下面这个简单的方法,它的参数个数是可变的:
public static <T> void addAll(Collection<T> coll, T... ts){
for(t : ts){
coll.add(t);
}
}
现在考虑以下调用:
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);
为了调用这个方法,Java虚拟机必须建立一个Pair<String>
数组,这就违反了前面的规则。不过对于这种情况,规则有所方式,只会得到一个警告,而不是错误。
可以采用两种方法来抑制这个警告。一种方法是为包含addAll
调用的方法增加注解@SuppressWarnings("unchecked")
。或者在Java SE 7中,还可以用@SafeVarargs
直接标注addAll
方法:
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)
这样就可以提供泛型类型来调用这个方法了。
(5) 不能实例化类型变量
不能使用像new T(...)
, new T[...]
或T.class
这样的表达式中的类型变量。
(6) 不能构造泛型数组
(7)泛型类的静态上下文中类型变量无效
(8) 不能抛出或捕获泛型类的实例
(9) 可以消除对受查异常的检查
(10) 注意擦除后的冲突
7. 泛型类型的继承规则
在使用泛型类时,需要了解一些有关继承和子类型的准则。考虑一个类和一个子类,如Employee
和Manager
。Pair<Manager>
是Pair<Employee>
的一个子类吗?不是!
事实上,无论S
与T
有什么联系,Pair<S>
与Pair<T>
没有任何联系。
需要说明的是,泛型类可以扩展或者实现其他的泛型类。例如ArrayList<T>
类实现List<T>
接口。
8. 通配符类型
(1) 通配符概念
通配符类型中,允许类型参数变化,例如通配符类型Pair<? extends Employee>
表示任何泛型Pair
类型, 它的类型参数是Employee
的子类, 如Pair<Manager>
,但不是Pair<String>
。
假设要编写一个打印雇员对的方法:
public static void printBuddies(Pair<Employee> p){
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + "and" + second.getName() + " are buddies");
}
不能将Pair<Manager>
传递给这个方法,这一点很受限制。解决的办法也很简单,使用通配符类型:
public static void printBuddiles(Pair<? extends Employee> p)
(2) 通配符的超类型限定
通配符限定与类型变量限定十分类时,但还有一个附加的能力,即可以指定一个超类型限定(supertype bound),例如? super Manager
这个通配符限制为Manager
的所有超类型。带有超类型限定的通配符可以为方法提供参数,但不能使用返回值。
直观来说,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
(3) 无限定通配符
还可以使用无限定的通配符,例如Pair<?>
。初看起来,这好像与原始的Pair
类型一样。实际上,有很大的不同。类型Pair<?>
有一下方法:
? getFirst()
void setFirst(?)
getFirst
的返回值只能赋给一个Object
。setFirst
方法不能被调用,甚至不能用Object
调用。Pair<?>
和Pari
本质的不同在于:可以用任意Object
对象调用原始Pair
类的setObject
方法。
这样脆弱的类型对于许多简单的操作非常有用。例如下面这个方法将用来测试一个Pair
是否包含一个null
引用,它不需要实际的类型。
public static boolean hasNulls(Pair<?> p){
return p.getFirst() == null || p.getSecond() == null;
}
通过将hasNulls
转换为泛型方法,可以避免使用通配符类型:
public static <T> boolean hasNulls(Pair<T> p)
但是,带有通配符的版本可读性更强。
(4) 通配符捕获
编写一个交换成对元素的方法:
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
调用swapHelper
:
public static void swap(Pair<?> p){
swapHelper(p);
}
在这种情况下,swapHelper
方法的参数T
捕获通配符。它不知道是哪种类型的通配符,但是这是一个明确的类型,并且<T>swapHelper
的定义只有在T
指出类型时才有明确的含义。
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、 确定的类型。 例如, ArrayList<Pair<T>>
中的T
永远不能捕获ArrayList<Pair<?>>
中的通配符。
9. 反射和泛型
(1)泛型Class类
Class
类是泛型的,例如,String.class
实际上是一个Class<String>
类的对象(事实上,是唯一的对象)。
类型参数十分有用,这是因为它允许Class<T>
方法返回的类型更加具有针对性。下面Class<T>
中的方法就使用了类型参数:
T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? super T> getSuperclass()
Constructor<T> getConstructor(Class... parameterTypes)
Constructor<T> getDeclaredConstructor(Class... parameterTypes)
newInstance
方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型目前被声明为T
,其类型与Class<T>
描述的类相同,这样就免除了类型转换。
(2) 使用Class<T>
参数进行类型匹配
有时,匹配泛型方法中的Class<T>
参数的类型变量很有实用价值。下面是一个标准的示例:
public static <T> pair<T> makePair(Class<T> c) throws InstantiationException, IllegalAccessException{
return new Pair<>(c.newInstance(), c.newInstance());
}
如果调用makePair(Employee.class)
,makePair
方法的类型参数T
同Employee
匹配,并且编译器可以推断出这个方法将返回一个Pair<Employee>
。