泛型程序设计分为3个能力级别。基本级别是,仅仅使用泛型类型,比如典型的ArrayList这样的集合,不需要考虑它们的工作方式和原因。我们大多数的程序员都停留在这一级别上,直到出现了问题。当不同的泛型类混合在一起时,或是在与对类型参数一无所知的遗留的代码进行衔接时,可能会看到含糊不清的错误消息。如果是这样的话,我们就需要学习Java泛型来系统地解决这些问题,而不是胡乱猜测。
定义简单的泛型类
Java 泛型是 JDK 5 中引入的一个新特性。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
一个简单的泛型类就是具有一个或多个类型变量的类。对于这样的类,我们只关注泛型,而不会为数据存储的细节烦恼。
package pk;
public class Pair<T> {
private T first;
private T 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,用尖括号括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以在Pair类,其中第一个域和第二个域使用不同的类型:
public class Pair<T,U> {...}
类定义的类型变量可以指定方法返回类型以及域和局部变量的类型。例如:
private T first;
注:在我们使用Java时大家有没有发现,使用变量E表示的是集合元素类型,K和V则表示关键字与值的类型,T(U和S)表示任意类型。其实,我们可以把泛型类看作是普通类的工厂。
泛型方法
泛型方法可以定义在普通类中,也可以定义在泛型类中。当调用一个泛型方法时,在方法名前的尖括号放入具体的类型:
package pk;
public class ArrayAlg {
public static <T> T getMiddle(T... a){
return a[a.length / 2];
}
String middle = ArrayAlg.getMiddle("John","Q","Public");
}
在大多数情况下,对于泛型方法的类型引用是没有问题的。不过偶尔,编译器会提示错误,比如说:
double middles = ArrayAlg.getMiddle(3.14,666,0);
我们可以通过错误,查看2个类的超类:Number和Comparable接口,其本身也就是泛型类型。在这种情况下,我们可以把所有参数类型写为double值。
类型变量的限定
有时候,类或方法需要对泛型变量加以约束。下面有一个经典的例子。我们要计算数组中最小的元素:
public static<T extends Comparable> T getMin(T[] a) {
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;
}
通过对类型变量T设置了限定,才能使用compareTo,在这里大家有没有发现为什么是使用extends而不是implements呢?毕竟Comparable是一个接口。
<T extends Comparable>
T是绑定类型的子类型。T绑定的类型可以是类,也可以是接口。至于为什么要选择extends的原因是因为它更接近子类的概念,一个类型变量或通配符可以有多个限定,例如:
<T extends Comparable & Serializable>
限定类型用&分隔,而逗号用来分隔类型变量。在Java继承中,可以根据需要拥有多个接口超类型,但限定中至多有一个类。如果用一个类作为限定,它必须是限定列表的第一个。
泛型代码和虚拟机
虚拟机中没有泛型类型对象,所有对象都属于普通类。
类型擦除
类型擦除可以理解为将泛型代码转换为普通代码。简单来说,类型参数只存在于编译期,在运行时,Java 的虚拟机 ( JVM ) 并不知道泛型的存在。无论什么时候创建一个泛型类型,都会自动提供一个相对应的原始类型。原始类型的名字就是删去类型参数的泛型类型名。擦除类型变量替换成限定类型(无限定类型用Object类型表示)。Pair<T>的原始类型如下:
package pk;
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 void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
}
因为T是无限定的变量,用Object替换。如果指定了上限如 <T extends String> 则类型参数就被替换成类型上限。如果大家想深入了解类型擦除,可以看看这篇博客,我觉得写的挺不错的。
翻译泛型表达式
其实就是表达式前面自动加上强制类型转换而已。当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面的代码示例:
Pair<Emploee> buddies = ...;
Emploee emploee = buddies.getFirst();
擦除getFirst的返回类型后将返回Object类型,编译器自动插入Emploee的强制类型转换。也就是说编译器把这个方法调用翻译成两条虚拟机命令:
- 对原始方法Pair.getFirst方法的调用
- 将返回的Object类型转换成Emploee类型
翻译泛型方法
类型擦除后,可能产生方法冲突的问题,但是Java编译器会帮你解决的。桥方法:在一个方法覆盖另外一个方法时就可以指定一个更严格的返回类型。例如:
package pk;
public class Emploee implements Cloneable {
// @Override
// protected Object clone() throws CloneNotSupportedException {
// return super.clone();
// }
public Emploee clone() throws CloneNotSupportedException {
//...
return null;
}
}
现在Emploee类有两个克隆的方法:
Emploee.clone();
Object.clone();
合成的桥方法调用了新定义的方法。总而言之,我们需要注意Java泛型的转换:
- 虚拟机中没有泛型,只有普通的类和方法。
- 在编译阶段,所有泛型类的类型参数都会被Object或者它们的限定边界来替换。
- 在继承泛型类型的时候,桥方法被合成来保持多态。
- 为保持类型的安全性,必要时插入强制类型转换。
调用遗留代码
就是允许泛型代码和遗留代码之间能够互相进行操作而已。
-------------------------------明天与意外你永远不知道谁先到,我们要做的就是好好的生活下去。-------------------------------