本文内容基于《数据结构与算法分析 Java语言描述》第三版,冯舜玺等译。
1. 实现泛型构件 pre-Java 5
面向对象的一个重要目标是对代码重用的支持,支持这个目标的一个重要的机制就是泛型机制:如果除去对象的基本类型外,实现方法是相同的,那么就可以用泛型实现来描述这种基本的功能。
Java 5以前,Java不直接支持泛型实现,泛型编程的实现是通过使用继承的一些基本概念来完成的。
1.1 使用Object表示泛型
Java中的基本思想就是可以通过使用像Object这样适当的超类来实现泛型类。
class Test {
private Object storedValue;
public Object read() {
return storedValue;
}
public void write(Object x) {
storedValue = x;
}
}
有两个细节必须考虑:
- 强制转型
MemoryCell memoryCell = new MemoryCell();
memoryCell.write("abc");
String value = (String) memoryCell.read();
- 不能使用基本类型
1.2 基本类型的包装
Java中每一种引用类型都和Obect相容,但是8种基本类型却不能。于是,Java为这8种基本类型中的每一种都提供了一个包装类。每一个包装对象都是不可变的,就是说它的状态绝不能改变,它存储一种当该对象被构建时所设置的原值,并提供一种方法以重新得到该值。
MemoryCell memoryCell = new MemoryCell();
memoryCell.write(new Integer(37));
int value = ((Integer) memoryCell.read()).intValue();
1.3 使用接口类型表示泛型
只有在使用Object类中已有的那些方法能够表示所执行的操作的时候,才能使用Object作为泛型类型来工作。
例如:考虑在由一些项组成的数组中找出最大项的问题。基本的代码是类型无关的,但是它的确需要一种能力来比较任意两个对象,并确定哪个是大的,哪个是小的。因此,不能直接找出Object的数组中的最大元素,而需要更多的信息。
使用Comparable接口的compareTo方法的几点注意事项:
- 只有实现Comparabale接口的对象才能作为Comparable数组的元素被传递;
- 如果Comparable数组有两个不相容的对象,例如一个String和一个Shape,那么CompareTo方法将抛出异常ClassCastException;
- 基本类型不能作为Comparable传递,但是包装类则可以;
- 接口究竟是不是标准的库接口倒不是必须的。
1.4 数组类型的兼容性
public class Test {
public static void func1(Super[] supers) {
System.out.println(Arrays.toString(supers));
}
public static void main(String[] args) {
Super[] supers = new Super[] {
new Super("super1"),
new Super("super2")
};
Sub[] subs = new Sub[] {
new Sub("sub1"),
new Sub("sub1")
};
func1(supers);
func1(subs);
}
}
class Super {
String name;
public Super() {
}
public Super(String name) {
this.name = name;
}
@Override
public String toString() {
return "Super{" +
"name='" + name + '\'' +
'}';
}
}
class Sub extends Super {
String name;
public Sub(String name) {
this.name = name;
}
@Override
public String toString() {
return "Sub{" +
"name='" + name + '\'' +
'}';
}
}
Sub继承Super,可以说Sub IS-A Super。func1()方法的形参是Super[],代码中给func()方法传递实参Sub[]也是可行的,可以说明Sub[] IS-A Super[]。
Java中的数组是类型兼容的,叫做协变数组类型,每个数组都明了它所允许存储的对象的类型,如果将一个不兼容的类型插入到数组中,那么虚拟机将抛出一个ArrayStoreException异常。
2. 利用Java 5泛型特性实现泛型构件
2.1 简单的泛型类和接口
public class Test<AnyType> {
private AnyType storedValue;
public AnyType read() {
return storedValue;
}
public void write(AnyType x) {
storedValue = x;
}
}
当指定一个泛型类时,类的声明则包含一个或多个类型参数,这些参数被放在类名后面的一对尖括号里。在类声明内部,可以声明泛型类型的域和使用泛型类型作为参数或返回类型的方法。
也可以声明接口是泛型的,Java 5以前,Comparable接口不是泛型的,它的compareTo方法需要一个Object作为参数,Java 5中,Comparable接口是泛型的,以前只有在运行时才能报告的许多错误如今变成了编译时的错误。
public interface Comparable<T> {
public int compareTo(T o);
}
2.2 自动装箱/拆箱
Java 5中有自动装箱/拆箱的功能。例如:如果一个int型变量被传递到需要一个Integer对象的地方,那么,编译器将在幕后插入一个对Integer构造方法的调用,这就叫做自动装箱;而如果一个Integer对象被放到需要int型变量的地方,则编译器将在幕后插入一个对intValue方法的调用,这就叫做自动拆箱。
Test<Integer> m = new Test<Integer>();
m.write(37);
int value = m.read();
2.3 菱形运算符
Java 7中增加了一种新的语言特性,称为菱形运算符,使得
Test<Integer> m = new Test<Integer>();
可以写为:
Test<Integer> m = new Test<>();
2.4 带有限制的通配符
1.4中已经说明了Java中的数组是协变的,但是泛型集合不是协变的。
public class Test {
public static void func2(List<Super> superList) {
System.out.println(superList);
}
public static void main(String[] args) {
List<Super> superList = new ArrayList<>();
superList.add(new Super("super1"));
superList.add(new Super("super2"));
List<Sub> subList = new ArrayList<>();
subList.add(new Sub("sub1"));
subList.add(new Sub("sub1"));
func2(superList);
func2(subList);
}
}
class Super {
String name;
public Super() {
}
public Super(String name) {
this.name = name;
}
@Override
public String toString() {
return "Super{" +
"name='" + name + '\'' +
'}';
}
}
class Sub extends Super {
String name;
public Sub(String name) {
this.name = name;
}
@Override
public String toString() {
return "Sub{" +
"name='" + name + '\'' +
'}';
}
}
说明List<Sub> IS-NOT-A List<Super>。
Java 5中用通配符来弥补这个不足,通配符用来表示参数类型的子类或超类,通配符还可以不带限制使用(extends Object),或不用extends而用super来表示超类而不是子类。
public static void func2(List<? extends Super> superList) {
System.out.println(superList);
}
2.5 泛型static方法
有时候特定类型很重要,如果满足以下某个条件,那么必须声明名一种带有若干类型参数的显示泛型方法:
- 该特定类型用做返回类型;
- 该类型用在多于一个的参数类型中;
- 该类型用于声明一个局部变量。
public static <AnyType> boolean contains(AnyType[] arr, AnyType x) {
for (AnyType value : arr) {
if (x.equals(value)) {
return true;
}
}
return false;
}
泛型方法特别像是泛型类,因为类型参数表使用相同的语法,但在泛型方法中的类型参数位于返回类型之前。
2.6 类型限界
public static <AnyType> AnyType findMax(AnyType[] arr) {
int maxIndex = 0;
for (int i = 1; i < arr.length; i++) {
if (arr[i].compareTo(arr[maxIndex]) > 0) {
maxIndex = i;
}
}
return arr[maxIndex];
}
上面代码中只有在AnyType是Comparable的情况下才能保证compareTo存在,可以使用类型限界来解决,类型限界在尖括号内指定,它指定参数类型必须具有的性质。
public static <AnyType extends Comparable<? super AnyType>> AnyType findMax(AnyType[] arr) {
int maxIndex = 0;
for (int i = 1; i < arr.length; i++) {
if (arr[i].compareTo(arr[maxIndex]) > 0) {
maxIndex = i;
}
}
return arr[maxIndex];
}
2.7 类型擦除
泛型类可以由编译器通过所谓的类型擦除过程而转变成非泛型类,这样,编译器就生成一种与泛型类同名的原始类,但是类型参数都被删去了,类型变量由它们的类型限界来代替,当一个具有擦除返回类型的泛型方法被调用的时候,一些特性被自动地插入,如果使用一个泛型类而不带类型参数,那么使用的是原始类。
类型擦除的一个重要推论是:所生成的代码与程序员在泛型之前所写的代码并没有太多的差异,而且事实上运行的也并不快。其显著的优点在于,程序员不必把一些类型转换放到代码中,编译器将进行重要的类型检验。
2.8 对于泛型的限制
- 基本类型不能用做类型参数;
- instanceof检测和类型转换工作只对原始类型进行;
- 在一个泛型类中,static方法和static域均不可引用类的类型变量,因为在类型擦除后类型变量就不存在了,另外,由于实际上只存在一个原始的类,因此static域在该类的诸泛型实例之间是共享的;
- 不能创建一个泛型类型的实例;
- 也不能创建一个泛型的数组;
- 参数化类型的数组的实例化是非法的。
3. 函数对象
一个函数通过将其放在一个对象内部而被传递,这样的对象通常叫做函数对象。
public class Test {
public static <AnyType> AnyType findMax(AnyType[] arr, Comparator<? super AnyType> cmp) {
int maxIndex = 0;
for (int i = 1; i < arr.length; i++) {
if (cmp.compare(arr[i], arr[maxIndex]) > 0) {
maxIndex = i;
}
}
return arr[maxIndex];
}
public static void main(String[] args) {
String[] arr = {"AEBAR", "alligator", "crocodile"};
System.out.println(findMax(arr, new CaseInsensitiveCompare()));
}
}
class CaseInsensitiveCompare implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.compareToIgnoreCase(o2);
}
}