1.为什么要使用泛型程序设计
==泛型设计程序(Generic programming)==意味着编写的代码可以被不同类型的对象所重用。
类型参数的好处
在Java增加泛型类之前,泛型程序设计用继承来实现。ArrayList类只维护一个Object引用的数组:
// 引入泛型之前的ArrayList
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的结果强制类型转换成其他类型,就会产生一个错误。
泛型提供了更好的解决方案:类型参数(type parameters):
ArrayList<String> files = new ArrayList<String>();
在Java SE 7之后的版本构造函数中可以省略泛型类型new ArrayList<>()。
编译器知道ArrayList< String >add方法有一个类型为String的参数,避免插入错误的对象(无法通过编译)。
类型参数的魅力在于:使得程序具有更好的可读性和安全性。
谁想成为泛型程序员
一个泛型程序员的任务就是预测出所有类的未来可能有的所有用途。
可以将ArrayList< Manager >中的所有元素添加到ArrayList< Employee >中去,但是反过来就不行。Java语言有一个独创性的新概念通配符类型(wildcard type),解决了这个问题。
2.定义简单泛型类
==泛型类(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 void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
Pair类引入了一个类型变量T,用==尖括号(< >)==括起来,放在类名后面。泛型类可以引入多个类型变量:
public class Pair<T, U> {...}
类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。
类型变量使用大写形式,且比较短。Java库中,使用变量E表示集合的元素类型,K和V表示关键字和值的类型。T(有时是临近的U和S)便是任意类型。
用具体的类型可以替换掉类型变量就可以实例化泛型类型(带构造器和方法的普通类):
Pair< String >
Pair(String, String)
String getFirst()
String getSecond()
void setFirst(String)
void setSecond(String)
泛型类可以看做普通类的工厂。
之前使用内部类实现的同时比较最大最小值的方法,可以使用泛型:
public class PairTest1 {
public static void main(String[] args) {
String[] words = { "Mary", "had", "a", "little", "lamb" };
Pair<String> mm = ArrayAlg.minmax(words);
System.out.println("最小值:" + mm.getFirst());
System.out.println("最大值:" + mm.getSecond());
}
}
class ArrayAlg {
public static Pair<String> minmax(String[] a) {
if (a == null || a.length == 0) {
return null;
}
String min = a[0];
String max = a[0];
for (int i = 0; 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.泛型方法
可以定义一个带有类型参数的简单方法:
calss ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
类型变量放在修饰符的后面,返回类型的前面。泛型方法可以定义在普通类中,也可以定义在泛型类中。
调用时在方法名前面的尖括号中放入具体类型:
String middle = ArrayAlg..<String>getMiddle("John", "Q", "public");
大多数情况下可省略< String >类型参数。编译器有足够的信息能够推断出所调用的方法。用Strintg[]与泛型类型T[]进行匹配并推断出T一定是String。
大多数情况下没有问题,偶尔编译器也会有提示错误:
double m = ArrayAlg.getMiddle(3.14, 1729, 0);
错误信息:解释这句代码有两种方法,而且两种方法都是合法的。1个Double和2个Integer对象,而后寻找共同的超类型,找到了两个超类型:Number和Comparable接口,其本身也是一个泛型类型。这时候我们需要补救措施将所有参数写为double值。
4.类型变量的限定
有时,类或方法需要对类型遍历加以约束。
public static <T extends Comparable> T min(T[] a) {
if (a == null || a.length == 0) {
return null;
}
T smallest = a[0];
for (int i = 0; i < a.length; i++) {
if (smallest.compareTo(a[i]) > 0) {
smallest = a[i];
}
}
return smallest;
}
为了确信变量T具有conpareTo方法,将T限制为实现了Comparable接口(只含一个方法compareTo方法)的类。实际上Comparable接口本身就是一个泛型类型。现在,泛型的min方法只能被实现了Comparable接口的类的数组调用,否则会产生编译错误。
==< T extends BoundingType >==表示T应该是绑定类型的子类型。T和绑定类型可以是类,也可以是接口。选择extends关键字是更接近子类的概念,而非implements,并且Java设计者并不打算添加新的关键字。
一个类型变量或通配符可以有多个限定,限定类型用“&”分隔,而逗号用来分隔类型变量:
T extends Comparable & Serializable
5.泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object)。
public class Pair {
private Object first;
private Object second;
...
}
因为T是一个无限定的变量,所以直接用Object替换。结果是一个普通的类,就好像泛型引入Java语言之前已经实现的那样。
在程序中可以包含不同类型的Pair。而擦除类型后就变成原始的Pair类型了。原始类型用第一个限定的类型变量来替换,如果没有给定限定就用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;
}
}
}
原始类型Interval如下:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second) {...}
}
如果class Interval <T extends Serializable & Comparable>,原始类型用Serializable替换T,而编译器在必要时要向Comparable插入强制类型转换。为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在边界列表的末尾。
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类,编译器插入强制类型转换。
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。编译器将这个方法调用翻译为两条虚拟机指令:
1.对原始方法Pair.getFirst的调用
2.将返回的Object类型强制转换为Employee类型
当存取一个泛型域时也要插入强制类型转换。
翻译泛型方法
泛型方法看似是一个完整的方法族,而擦除方法之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
方法擦除带来了两个复杂问题:
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}
}
擦除后变成:
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.setSecond。问题在于类型擦除与多态发生了冲突。编译器在DateInterval类中生成了一个桥方法(bridge method):
public void setSecond(Object second) {setSecond((Date) second);}
变量pair已经声明为类型Pair< LocalDate >,并且这个类型只有一个简单方法叫setSecond(Object)。虚拟机用pair引用对象调用方法。这个对象是DateInterval类型,因而将会调用DateInterval.setSecond(Object)方法,即合成的桥方法。
假设:
class DateInterval extends Pair<LocalDate> {
public LocalDate getSecond() {
return (Date)super.getSecond().clone();
}
}
DateInterval类中有两个getSecond方法:
LocalDate getSecond()
Object getSecond()
在虚拟机中用参数类型和返回类型确定一个方法。因此编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确处理。但不能这么编写Java代码(具有相同参数类型的两个方法是不合法的),它们都没有参数。
桥方法不仅用于泛型类型。在一个方法覆盖另一个方法时可以指定一个更严格的返回类型:
public class Employee implements Cloneable{
public Employee clone() {...}
}
Object.clone和Employee.clone方法被说成具有协变的返回类型(covariant return types),实际上Employee类有两个克隆方法:
Employee clone()
Object Clone()
合成的桥方法调用了新定义的方法。
总之,需要记住有关Java泛型转换的事实:
1.虚拟机中没有泛型,只有普通类和方法
2.所有的类型参数都用他们的限定类型替换
3.桥方法被合成来保持多态
4.为保持类型安全性,必要时插入强制类型转换
调用遗留代码
设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作。
Dictionary<Integer, Component> labelTable = new Hashtable<>()
labelTable.put(0, new JLabel(new ImageIcon("nine.gif")));
labelTable.put(1, new JLabel(new ImageIcon("ten.gif")));
JSlider jSlider = new JSlider();
jSlider.setLabelTable(labelTable);
@SuppressWarnings("unchecked")
Dictionary<Integer, Component> lt = jSlider.getLabelTable();
调用setLabelTable时,比编译器会发出一个警告。这个警告不会产生什么影响,因此可以忽略。
相反的情形,由一个遗留的类得到一个原始类型的对象Dictionary<Integer, Component> lt = jSlider.getLabelTable(),或看到一个警告,确保标签表已经包含了Integer和Component对象,当然从来也不会有绝对的承诺,最差的情况将抛出一个异常。
在查看警告之后,可以利用==注解(annotation)==使之消失,@SuppressWarnings(“unchecked”)。
注解必须放在生成这个警告的代码所在方法之前,或者标注整个方法(关闭对方法所有代码的检查)。