第十二章 泛型程序设计
泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。
ArrayList<String> files = new ArrayList<String>() ;
String filename = files.get(0) ;
使用泛型前,ArrayList使用Object,取数据需进行强制类型转换。
使用泛型后不需要类型转换。
定义简单泛型
package pair1;
/**
* @version 1.00 2004-05-10
* @author Cay Horstmann
*/
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,用<>括起来,并放在类名后面。
泛型类可以有多个类型变量
public class Pair<T, U>
用具体的类型替换类型变量就可以实例化泛型类型,例如:
Pair<String>
Pair<String> mm = ArrayAlg.minmax(words) ;
泛型方法
定义一个泛型方法
class AraayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length/2] ;
}
}
这个方法是在普通类中定义的,而不是在泛型类中定义的。然而这时一个泛型方法。注意类型变量放在修饰符的后面,返回类型的前面。
调用泛型方法:
String middle = ArrayAlg.<String>getMiddle(“John”, “Q.”, “Public”) ;
大多数情况下,类型参数可以省略,编译器可以判断出所调用的方法。
String middle = ArrayAlggetMiddle(“John”, “Q.”, “Public”) ;
下面的会报错:
double middle = ArrayAlg.getMiddle(3.14, 1729, 0) ;
Double 和 Integer的共同超类不是Double,不能赋值给double变量。他们的共同超类是Number和Comparable接口。
类型变量的限定
public static <T extends Comparable> T min(T[] a)
使用上面的方式,可以确保T实例具有compareTo方法。
现在泛型的min方法只能被实现了Comparable接口的类(String 、Date)的数组调用。
T可以为类或者接口。
一个变量可以或通配符可以有多个限定,例如:
T extends Comparable & Serializable
限定类型用“&”分隔,而逗号用来分隔类型变量
泛型代码和虚拟机
虚拟机没有泛型类型——所有的对象都属于普通类。
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型就是删去类型参数后的泛型类型名。擦出类型变量,并替换为限定类型(无限定的变量用Object)。例如Pair<T>的原始类型如下所示:
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 Comoparable & 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<Employee> buddies = ...
Employee buddy = buddies.getFirst() ;
擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
l 对原始方法Pair.getFirst的调用
l 将返回的Object类型强制转换为Employee类型、
翻译泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为下述的泛型方法
public static <T extends Comparable> T min(T[] a)
是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
注意,类型参数T已经被擦除了。只留下来限定类型Comparable。
P536
重写方法时,类型擦除与多态存在冲突,要解决这个问题,需要编译器在DateInterval类中生成一个桥方法(bridge method):
public void setSecond(Object second) {setSecond(Date) second} ;
重写方法,擦除类型后可能会出现以下的情况:
Date getSecond()
Object getSecond()
不能这样编写Java代码,但在虚拟机中,可以通过返回值分辨。
需要记住Java泛型转换的事实:
l 虚拟机中没有泛型,只有普通的类和方法
l 所有的类型参数都用它们的限定类型替换
l 桥方法被合成来保持多态
l 为保持类型安全性,必要时插入强制类型转换
约束与局限性
1.不能用基本类型实例化类型参数
没有Pair<double>,只有Pair<Double>.因为类型擦除后,Pair含有Object类型的域,Object不能存储double值。
2.运行时类型查询只适用于原始类型
if(a instanceof Pair<T>)
实际上仅仅测试是否是任意类型的一个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.getClass() == employeePair.getClass()) //they are equal
3.不能创建参数化数组
不能实例化参数类型数组
Pair<String> table = new Pair<String>[10] ; //ERROR
因为可以将new Pair<Employee>赋值给table中的一个元素,这样会导致类型错误。
不过仅仅声明Pair<String>[]的变量是合法的。
4.Varargs警告
public static <T> void addAll(Collection<T>, T... ts)
T... ts相当于一个参数化数组,不过作为参数只会警告,可以编译。
可以使用@SuppressWarning 消除警告
Java 7 中也可以使用@SafeVarargs 消除警告
5.不能实例化类型变量
不能使用像new T(...) , new T[...] 或 T.class这样的表达式中的类型变量。
通过一些特殊的设计可以在一定范围内解决上述问题,详情:P541
6.泛型类的静态上下文中类型变量无效
不能再静态域或方法中引用类型变量。
public class Singleton<T>
{
private static T singleInstance ; //ERROR
public static T getSingleInstance() //ERROR
{
if (singleInstance == null) construct new instance of T
return singleInstance ;
}
}
7.不能抛出或捕获泛型类实例
既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展Throwable都是不合法的。
public class Problem<T> extends Exception {...} //ERROR --can’t extnds Trowable
catch 字句中不能使用类型变量
不过在异常规范中使用类型变量是允许的。
public static <T extends Throwable> void doWork(Class<T> t) //OK
Java 异常处理的一个基本原则是,必须为所有已检查异常提供一个处理器。不过可以利用泛型消除这个限制。
详情参考P543
8.注意擦除后的冲突
当泛型类型被擦除时,无法创建引发冲突的条件。
public class Pair<T>
{
public boolean equals(T value) { return first.equals(value) && second.equals(value) ;}
...
}
类型擦除后Pair类中有两个boolean equal(Object) 方法,这样会造成冲突。
泛型规范规定:要想支持擦除的转换,就需要强制限制一个类或类型变量不能同时成为两个接口类型的子类,而这两个接口是同一个接口的不同参数化。例如下列代码是非法的:
class Calendar implements Comparable<Calendar> {...}
class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>
{...} //ERROR
GregorianCalendar 会实现Comparable<Calendar>和Comparable<GregorianCalendar>,这是统一接口的不同参数化。
上面的例子,非泛型版本是合法的。
原因是可能是产生桥方法时会产生冲突。
泛型类的继承规则
永远可以将参数化类型转换为一个原始类型。
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo) ;
Pair rawBuddies = managerBuddies ; // OK
rawBuddies.setFirst(new File(“...”)) ; //only a compile-time warning
通配符类型
Pair<? extends Employee>
表示任何泛型Pair类,它的类型参数是Employee的子类,如Pair<Manager>
Pair<? extends Employee> ,其方法似乎是:
? extends Employee getFirst()
void setFirst(? extends Employee)
setFirst不能使用,因为不知道具体类型
getFirst可以使用,因为可以将返回值赋给Employee的引用
通配符的超类限定
通配符限定与类型变量限定十分类似,但是,还有一个附加能力,即可以指定一个超类型限定。
? super Manager
这个通配符限制为Manager的所有超类。
Pair<? super Manager>有方法
void setFirst(? superManager)
? super Manager getFirst()
编译器不知道setFirst方法的确切类型,但是可以用任意Manager对象(或子类型)调用它,而不能用Employee对象调用。然而,如果调用getFirst,返回的对象类型就不能得到保证。只能把它赋给一个Object对象。
直观的讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
public interface Comparable<T>
{
public int compareTo(T other) ;
}
public static <T extends Comparable<T>> T min(T[] a)
public static <T extends Comparable<? super T>> T min(T[] a)
无限定通配符
还可以使用无限定通配符。Pair<?>
类型Pair<?>具有的方法:
? getFirst()
void setFirst(?)
getFirst的返回值只能赋给Object。setFirst不能调用,甚至不能用Object调用。
Pair<?>和Pair本质的不同在于:可以用任意Object对象调用原始的Pair类的setObject方法。
这种形式对许多简单地操作非常有用。
public static boolean hasNull(Pair<?> p)
{
return p.getFirst() == null || p.getSecond() == null ;
}
通配符捕获
package pair3;
class PairAlg
{
public static boolean hasNulls(Pair<?> p)
{
return p.getFirst() == null || p.getSecond() == null;
}
public static void swap(Pair<?> p) { swapHelper(p); }
public static <T> void swapHelper(Pair<T> p)
{
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
}
swapHelper方法的参数T捕获通配符。它不知道是哪种类型的通配符,但是,这是一个明确的类型,并且<T>swapHelper的定义只有在T指出类型时才有明确的含义。
反射和泛型
现在,Class类时泛型的。例如String.class实际上是一个Class<String>类的对象(事实上,唯一的对象)。
类型参数十分有用,这是因为它允许Class<T>方法的返回类型更加具有针对性。
使用Class<T>参数进行类型匹配
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException,
IllegalAcessException
{
return new Pair<>(c.newInstance(), c.newInstance()) ;
}
makePair(Employee.class)