1. 泛型的概念
泛型本质上就是参数化类型(parameterized type)。在定义类、接口、方法的时候,把将要操作的数据类型声明为形参。在实例化的时候,再传入实际的数据类型,就是由类型实参指定真实数据类型。这就是泛型。泛型主要目的是复用算法。对于不同的数据类型,算法逻辑一样,就不用针对不同数据类型写不同的代码。可以增强代码的可扩展性,减少工作量。比如ArrayList,可以保存任何数据类型,不需要针对每个数据类型写一个ArrayList类。
在Java 5之前,没有泛型。在这之前,复用算法的主要手段是使用超类。比如可以将ArrayList里面的数据类型定义为Object,这样就可以保存任何数据类型了。但是,这样容许使用者在同一个ArrayList保存不同的数据类型,在代码进行强制类型转换的时候,就会发生类型错误。泛型可以保证类型安全,因为它可以让强制类型转换自动地、隐式地进行。
但是,因为Java 5之前没有泛型,所以泛型需要兼容以前非泛型的代码。就是非泛型代码必须能处理泛型,泛型代码也必须能处理非泛型代码。Java是使用擦除特性(erasure)来实现的。当Java代码编译时,所有泛型类型信息都将被删除(擦除)。也就是说,当java文件编译成class文件后,会使用类型形参的约束类型来替换类型形参。举个例子,ArrayList<E>中保存的元素,在class文件看来就是Object。然后使用隐式地强制转换来与类型实参指定的类型保持兼容。
总结一下就是:泛型扩展了复用算法的能力,并且可以保证类型安全。通过编译后类型擦除和隐式强制转换,保证和以前的非泛型代码的兼容性。
2. 对类型形参进行约束
有些情况下,泛型类型可以没任何约束,也就是任何Object都可以。但是在另外一些情况下,算法只和特定的数据类型有关。比如你希望对任何数值(包括整数,浮点数和双精度数)执行计算。这时候需要用到Number类中的一些方法。如果没有进行约束,泛型类型是不能调用Number类中的方法的。下面是对类型形参进行约束的代码示例:
错误的写法:
class NumericFns<T> {
T num;
NumericFns(T t) {
num = t;
}
double getDoubleValue() {
//这是错误的写法,因为编译后num被当做Object类型,Object类型没有doubleValue()方法。
return num.doubleValue();
}
}
正确的写法:
class NumericFns<T extends Number> {
T num;
NumericFns(T t) {
num = t;
}
double getDoubleValue() {
//这是正确的写法,因为编译后num被当做Number类型,Number类型有doubleValue()方法。
return num.doubleValue();
}
}
上面这个例子类型形参T被限定为Number类的子类。
另外,一个形参可以用来约束另一个形参:
class Pair<T,V extends T> {
T first;
V second;
Pair(T a,V b) {
first = a;
second = b;
}
}
上面这个例子中,T可以是任何类型,但是一旦指定了T,V就必须是T的子类型。下面这段使用代码就是错的,因为String不是Number的子类:
Pair<Number,String> z = new Pair<Number,String>(10,"10");
3. 使用通配符实参以及约束通配符
如果要写一个算法,要对一个浮点数的绝对值和一个双精度数的绝对值进行比较,该怎么办呢?看下面的代码示例:
class NumericFns<T extends Number> {
T num;
NumericFns(T t) {
num = t;
}
boolean absEqual(NumericFns<?> ob) {
if(Math.abs(num.doubleValue())==Math.abs(ob.num.doubleValue())) {
return true;
}
return false;
}
}
根据上面的代码,我们可以写出使用代码:
NumericFns<Float> f = new NumericFns<Float>(1.25f);
NumericFns<Double> d = new NumericFns<Double>(-1.25);
f.absEqual(d);
注意,代码中?代表的是实参,表示该方法可以传入任何Number类型的实参。这样就达到了让不同类型数据进行比较的目的。通配符实参在不确定实参类型的场景下非常实用。
另外,也可以对通配符进行约束:
//上层约束
<? extends superclass>
//下层约束
<? super subclass>
4. 使用泛型需要注意的地方
4.1 泛型的作用范围
① 泛型的形参可以在类、接口、构造函数、方法(包括成员方法和静态方法)中声明。
② 类和接口中声明的形参可以使用在:成员变量、成员方法参数、成员方法返回类型。不能用在静态变量和静态方法上(虽然静态方法可以定义自己的形参,但是不能用类和接口中声明的形参,因为类和接口中声明的形参是给对象用的,而不是给类用的)。
class NumericFns<T extends Number> {
//这是对的
T num;
//这是错的
static T num1;
NumericFns(T t) {
num = t;
}
//这是对的
T getNum() {
return num;
}
//这是错的
static T getNum1() {
return num;
}
//这是对的
static <V extends Number> V getNum2(V v) {
return v;
}
}
③ 泛型的形参不可以是基本数据类型。
4.2 类型形参不能实例化
下面这段代码是错误的,因为编译器不知道要创建哪一种对象,T只是一个占位符:
class Gen<T> {
T ob;
T vals[];
Gen() {
//这是错的
ob = new T();
//这也是错的
vals = new T[10];
}
}
对于数据,还有一个限制:
//这是错误的
Gen<Integer> gens[] = new Gen<Integer>[10];
//这是对的
Gen gens[] = new Gen[10];
4.3 歧义错误
下面这两个set方法就会产生冲突,如果T和V都传入String实参,就会产生歧义,这是编译不能通过的。
class MyGenClass<T,V> {
T ob1;
V ob2;
void set(T o) {
ob1 = o;
}
void set(V o) {
ob2 = o;
}
}
4.4 实现了泛型接口的类,其自身也必须是泛型的
4.5 方法中声明的形参,一定要用在方法参数中
4.6 对于形参的声明都放在<>里面
4.7 扩展了Throwable的类都不能使用泛型,也就是说不支持泛型异常类
5. 反射和泛型
从Java 5以后,Class类是泛型的。String.class实际上是Class<String>的一个实例(唯一的实例)。
public static <T> Pair<T> makePair(Class<T> c)
throws InstantiationException,IllegalAccessException {
return new Pair<>(c.newInstance(),c.newInstance());
}
makePair方法如果传入一个String.class,就会返回一个Pair<String>实例。这在构建框架代码时,非常常见。
反射是一个非常大的动态编程主题,后面会专门讨论它。