核心技术:泛型
为什么要使用泛型
泛型程序设计(Generic programming) 意味着编写的代码可以被很多不同类型的对象所重用。
类型参数( type parameters )。ArrayList 类有一个类型参数用来指示元素的类型:
ArrayList<String> files = new ArrayList<String>();
- 在Java SE 7 及以后的版本中, 构造函数中可以省略泛型类型:
ArrayList<String> files = new ArrayList<>();
定义简单泛型类
一个泛型类( generic class ) 就是具有一个或多个类型变量的类。
- 换句话说,泛型类可看作普通类的工厂。
泛型方法
- 类型变量放在修饰符(这里是public static ) 的后面,返回类型的前面。
class ArrayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length / 2];
}
}
泛型变量的限定
是将T 限制为实现了Comparable 接口(只含一个方法compareTo 的标准接口)的类:
public static <T extends Coiparab1e> T min(T[] a) . . .
- 使用关键字extends 而不是implements
- T 和绑定类型可以是类, 也可以是接口
- 一个类型变量或通配符可以有多个限定:
T extends Comparable & Serializable
- 限定类型用“&”分隔,而逗号用来分隔类型变量。
泛型代码和虚拟机
类型擦除
无论何时定义一个泛型类型, 都自动提供了一个相应的原始类型( raw type )。
- 原始类型的名字就是删去类型参数后的泛型类型名。
- 擦除(erased) 类型变量, 并替换为限定类型(无限定的变量用Object)。
如:
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; }
}
擦除为:
public class Pair
{
private Object first;
private Object second;
public Pair() { first = null ; second = null ; }
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(Object newValue) { first = newValue; }
public void setSecond(Object newValue) { second = newValue; }
}
- 原始类型用第一个限定的类型变量来替换, 如果没有给定限定就用Object 替换。
例:
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; }
}
}
擦除为:
public class Interval implements Serializable
{
private Comparable lower;
private Coiparable upper;
...
public Interval (Coiparable first, Coiparable second) { . . . }
}
翻译泛型表达式
对于如下表达式:
Pair<Employee> buddies = . .;
Employee buddy = buddies.getFirst();
编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法Pair.getFirst 的调用。
- 将返回的Object 类型强制转换为Employee 类型。
翻译泛型方法
类型擦除也会出现在泛型方法中:
public static <T extends Comparable〉T min(T[] a)
擦除为:
public static Comparable min(Comparable[] a)
方法的擦除带来了两个复杂问题。如:
class Datelnterval extends Pair<LocalDate>
{
public void setSecond (Local Date second)
{
if (second.compareTo(getFi rst()) >= 0)
super. setSecond (second);
}
...
}
这个类擦除后变成:
class Dateinterval extends Pair// after erasure
{
public void setSecond (Local Date second) { . . . }
...
}
而Datelnterval同时存在一个从Pair继承的setSecond方法,即:
pub1ic void setSecond (Object second)
考虑下面的语句序列:
Datelnterval interval = new Datelnterval (...) ;
Pair<LocalDate> pair= interval; // OK--assignment to superclass
pair.setSecond (aDate);
在这里,类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在Datelnterval类中生成一个桥方法(bridgemethod):
public void setSecond(Object second)
{
setSecond((Date) second);
}
有关Java 泛型转换的事实:
- 虚拟机中没有泛型, 只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插人强制类型转换。
调用遗留代码
约束与局限性
不能使用基本类型实例化类型参数
没有Pair<double>, 只有Pair<Double>。
其原因是类型擦除:擦除之后, Pair 类含有Object 类型的域, 而Object 不能存储double 值;
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此, 所有的类型查询只产生原始类型。
例如:
类型判断:
if (a instanceof Pair<String>) // Error
实际上仅仅测试a 是否是任意类型的一个Pair。
强制类型转换:
Pair<String> p = (Pair<String>) a; // Warning-can only test that a is a Pair
同样的道理, getClass 方法总是返回原始类型。例如:
Pair<String> stringPair = . .
Pair< Employee〉employeePair = . .
if (stringPair.getClassO == employeePair .getClassO) // they are equal
其比较的结果是true, 这是因为两次调用getClass 都将返回Pair.class。
- 为提醒这一风险, 试图查询一个对象是否属于某个泛型类型时, 倘若使用instanceof 会得到一个编译器错误, 如果使用强制类型转换会得到一个警告。
不能创建参数化类型的数组
只是不允许创建这些数组, 而声明类型为“Pair<String>[]”的变量仍是合法的。不过不能用“new Pair<String>[10]”初始化这个变量。
如:
Pair<String>[] table = new Pair<String>[10] ; // Error
擦除之后, table 的类型是Pair[]。 可以把它转换为Object[] :
Object[] objarray = table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStoreException 异常:
- 可以声明通配类型的数组, 然后进行类型转换:
Pair<String>[] table = (Pair<String>[]) new Pair<?>[10];
= Varargs 警告(可变参数警告)
如:
public static <T> void addAll(Collections coll, T... ts)
{
for (t : ts) coll.add⑴;
}
- 实际上参数ts 是一个数组。
调用
Col1ection<Pair<String» table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 = . .
addAll (table, pairl, pair2);
为了调用这个方法,Java 虚拟机必须建立一个Pair<String> 数组, 这就违反了前面的规则。不过,对于这种情况, 规则有所放松, 你只会得到一个警告,而不是错误。
可以采用两种方法来抑制这个警告。
- 为调用addAll的方法增加注解
@SuppressWamings("unchecked")
- 直接标注addAll方法
@SafeVarargs
不能示例化类型变量
不能使用像“new T(...)”,“newT[...]” 或“T.class” 这样的表达式中的类型变量。
- Class 类本身是泛型。例如, String.class 是一个 Class<String> 的实例(事实上,String.class 是一个 Class<String>唯一的实例)。
不能构造泛型数组
泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。
如:
public class Singleton<T>
{
private static T singlelnstance; // Error
public static T getSinglelnstanceO // Error
{
if (singleinstance == null)
... //construct new instance of T
return singlelnstance;
}
}
可以声明一个Singleton<Random> 共享随机数生成器, 声明一个Singlet0n<JFileChooser> 共享文件选择器对话框。但是, 这个程序无法工作。
类型擦除之后, 只剩下Singleton 类, 它只包含一个singlelnstance 域。
不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上, 甚至泛型类扩展Throwable 都是不合法的。
可以消除对受查异常的检查
Java 异常处理的一个基本原则是, 必须为所有受查异常提供一个处理器。不过可以利用泛型消除这个限制。
【??????】
通过使用泛型类、擦除和@SuppressWamings 注解, 就能消除Java 类型系统的部分基本限制。
注意擦除后的冲突
要想支持擦除的转换, 就需要强行限制一个类或类型变量不能同时成为两个接口类型的子类, 而这两个接口是同一接口的不同参数化。
如:
class Employee implements Coinparab1e<Emp1oyee> { . . . }
class Manager extends Employee implements Comparable<Hanager>{ . . . } // Error
Manager 会实现Comparable<Employee> 和Comparable<Manager>, 这是同一接口的不同参数化。
泛型类的继承规则
无论S 与T 有什么联系,通常, Pai<S> 与Pair<T>没有什么联系。
泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。
例如, ArrayList<T> 类实现List<T> 接口: