泛型程序设计(generic programming)意味着编写的代码可以对多种不同类型的对象重用。
类型参数的好处
泛型提供了一个更好的解决方案:类型参数(type parameter)。ArrayList类有一个类型参数用来指示元素的类型:
var files = new ArrayList<String>;
编译器也可以充分利用这个类型信息。调用get的时候,不需要进行强制类型转换。编译器知道返回值类型为String,而不是Object:
String filename = files.get(0);
定义简单泛型类
泛型类(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 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>{...}
类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型,例如:
private T first;//uses the type variable
可以用具体的类型替换类型变量来实例化(instantiate)泛型类型,例如:
Pair<String>
可以把结果想象成一个普通类,它有以下构造器:
Pair<String>()
Pair<String>(String,String)
或:
String getFirst()
String getSecond()
void setFirst(String)
void setSecond(String)
泛型方法
可以定义一个带有类型参数的方法:
class ArrayAlg
{
public static <T> getMiddle(T...a)
{
return a[a.length /2];
}
}
泛型方法可以在普通类中定义,也可以在泛型类中定义。
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");
在这种情况下,方法调用中可以省略<String>类型参数。编译器将参数的类型与泛型类型T进行匹配,推断出T一定是String,即
String middle = ArrayAlg.getMiddle("John","Q.","Public");
类型变量的限定
有时,类或方法需要对类型变量加以约束。
以下例子,要计算数组中的最小元素:
class ArrayAlg
{
public static <T> T min(T[] a)//almost correct
{
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;
}
}
泛型代码和虚拟机
类型擦除:
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦拭(erased),并替换为其限定类型。
例如,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 first;}
public Object getSecond(){ return second;}
public void setFirst(Obect newValue){ first = newValue;}
public void setSecond(Object newValue){ second = newValue;}
}
因为T是一个无限定的变量,所以直接用Object替换。
原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为Object。
例如,类Pair<T>中的类型变量没有显式的限定,因此,原始类型用Object替换T。假定我们声明了一个稍有不同的类型:
public class Interval<T extends Comparable&Serializable> implements Serializable
{
private T lower;
private T upper;
...
public 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;
...
public Interval(Comparable first,Comparable second){...}
}
转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。
Pair buddies = …;
Employee buddy = buddies.getFirst();
转换泛型方法:
类型擦除也会出现在泛型方法中。
由于pair引用一个DateInterval对象,所以应该调用DateInterval.setSecond。为了解决这个问题,编译器在DateInterval类中生成一个桥方法(bridge method):
public void setSecond(Object second){ setSecond((LocalDate) second);}
调用遗留代码
Swing用户界面工具包提供了一个JSlider类,它的“刻度”(tick)可以定制为包含文本或图像的标签。这些标签用以下调用设置:
void setLabelTable(Dictionary table)
限制与局限性
虚拟机中的对象总有一个特定的非泛型类型,因此,所有的类型查询只产生原始类型。
例如:if(a instnaceof Pair<String>) //error
实际上仅仅测试a是否是任意类型的一个Pair,下面的测试同样如此:
if (a instanceof Pair<T>)//error
或强制类型转换:
Pair<String> p = (Pair<String>) a;
不能创建参数化类型的数组:
不能实例化参数化类型的数组,例如:
var table = new Pair<String>[10];//error
不能构造泛型数组
如果数组仅仅作为一个类的私有实例字段,那么可以将这个数组的元素类型声明为擦除的类型并使用强制类型转换。
例如,ArrayList类可以如下实现:
public class ArrayList<E>
{
private Object[] elements;
...
@SuppressWarnings("unchecked") public E get (int n){ return(E) elements[n];}
public void set(int n,E e) { elements[n] = e;}//no cast needed
}
但实际的实现没有这么清晰:
public class ArrayList<E>
{
private E[] elements;
...
public ArrayList(){ elements = (E[]) new Object[10];}
}
这里强制类型转换E[]是一个假象,而类型擦除使其无法察觉。
可以取消对检查型异常的检查
Java异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。
不过可以利用泛型取消这个机制。关键在于以下方法:
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T
{
throw (T) t;
}
假设这个方法包含在接口Task中,如果有一个检查型异常e,并调用
Task.<RuntimeException>throwAs(e);
编译器就会认为e是一个非检查型异常。以下代码会把所有异常都转换为编译器所认为的非检查型异常:
try
{
do work
}
catch(Throwable t)
{
Task.<RuntimeException>throwAs(t);
}
下面使用这个技术解决一个棘手的问题。要在一个线程中运行代码,需要把代码放在一个实现了Runnable接口的类的run方法中。不过这个方法不允许抛出检查型异常。我们将提供一个从Task到Runnable的适配器,它的run方法可以抛出任意异常。
interface Task
{
void run() throws Exception;
@SuppressWarnings("unchecked")
static<T extends Throwable> void throwAs(Throwable t) throws T
{
throw(T) t;
}
static Runnable asRunnable(Task task)
{
return () ->
{
try
{
task.run();
}
catch (Exception e)
{
Task.<RuntimeException>throwAs(e);
}
};
}
}
注意擦除后的冲突
当泛型类型被擦除后,不允许创建引发冲突的条件。
假定为Pair类增加一个equals方法:
public class Pair<T>
{
public boolean equals(T value) { return first.equals(value)&&second.equals(value);}
...
}
考虑一个Pair<String>。从概念上讲,它有两个equals方法:
boolean equals(String)//defined in Pair<T>
boolean equals(Object)//inherited from Object
但是,直觉把我们引入歧途。方法
boolean equals(T)
擦除后就是
boolean equals(Object)
这会与Object.equals方法发生冲突。补救的办法就是重新命名引发冲突的方法。
通配符类型
在通配符类型中,允许类型参数发生变化。例如,通配符类型
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 printBuddies(Pair<? extends Employee> p)
通配符的超类型限定
通配符现代与类型变量限定十分类似,但是,还有一个附加的能力,既可以指定一个超类型限定(supertype bound)即:?super Manager
这个通配符限制为Manager的所有超类型。
public static void minmaxBonus(Manager[] a,Pair<? super Manager> result)
{
if(a.length == 0) return;
Manager min = a[0];
Manager max = a[0];
for(int i = 1; i<a.length; i++)
{
if(min.getBonus()>a[i].getBonus()) min = a[i];
if(max.getBonus()<a[i].getBonus()) max = a[i];
}
result.setFirst(min);
result.setSecond(max);
}
通配符捕获
可以用swap调用swapHelper:
public static void swap(Pair<?>p){ swapHelper(P);}
swapHelper方法的参数T捕获通配符。
反射和泛型
泛型Class类:现在,Class类是泛型的。例如,Srting.class
实际上是一个Class<Srting>
类的对象。
类型参数十分有用,这是因为它允许Class方法的返回类型更加具有特定性。Class<T>
的以下方法就使用了类型参数:
T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? super T>getSuperclass()
Constructor<T> getConstructor(Class...parameterTypes)
Constructor<T> getDeclaredConstructor(Class...parameterTypes)
类型字面量
TypeLiteral构造器会捕获泛型超类型:
class TypeLiteral
{
public TypeLiteral()
{
Type parentType = getClass().getGenericSuperclass();
if(parentType instanceof ParameterizedType)
{
type = {(ParameterizedType) parentType).getActualTypeArguments()[0];
}
else
throw new UnsupportedOperationException(
"Construct as new TypeLiteral<...>(){}");
}
...
}