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 num; NumericFns(T t) { num = t; } double getDoubleValue() { //这是错误的写法,因为编译后num被当做Object类型,Object类型没有doubleValue()方法。 return num.doubleValue(); }}
正确的写法:
class NumericFns{ T num; NumericFns(T t) { num = t; } double getDoubleValue() { //这是正确的写法,因为编译后num被当做Number类型,Number类型有doubleValue()方法。 return num.doubleValue(); }}
上面这个例子类型形参T被限定为Number类的子类。
另外,一个形参可以用来约束另一个形参:
class Pair{ T first; V second; Pair(T a,V b) { first = a; second = b; }}
上面这个例子中,T可以是任何类型,但是一旦指定了T,V就必须是T的子类型。下面这段使用代码就是错的,因为String不是Number的子类:
Pairz = new Pair (10,"10");
3. 使用通配符实参以及约束通配符
如果要写一个算法,要对一个浮点数的绝对值和一个双精度数的绝对值进行比较,该怎么办呢?看下面的代码示例:
class NumericFns{ 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; }}
根据上面的代码,我们可以写出使用代码:
NumericFnsf = new NumericFns (1.25f);NumericFns d = new NumericFns (-1.25);f.absEqual(d);
注意,代码中?代表的是实参,表示该方法可以传入任何Number类型的实参。这样就达到了让不同类型数据进行比较的目的。通配符实参在不确定实参类型的场景下非常实用。
另外,也可以对通配符进行约束:
//上层约束 //下层约束
4. 使用泛型需要注意的地方
4.1 泛型的作用范围
① 泛型的形参可以在类、接口、构造函数、方法(包括成员方法和静态方法)中声明。
② 类和接口中声明的形参可以使用在:成员变量、成员方法参数、成员方法返回类型。不能用在静态变量和静态方法上(虽然静态方法可以定义自己的形参,但是不能用类和接口中声明的形参,因为类和接口中声明的形参是给对象用的,而不是给类用的)。
class NumericFns{ //这是对的 T num; //这是错的 static T num1; NumericFns(T t) { num = t; } //这是对的 T getNum() { return num; } //这是错的 static T getNum1() { return num; } //这是对的 static V getNum2(V v) { return v; }}
③ 泛型的形参不可以是基本数据类型。
4.2 类型形参不能实例化
下面这段代码是错误的,因为编译器不知道要创建哪一种对象,T只是一个占位符:
class Gen{ T ob; T vals[]; Gen() { //这是错的 ob = new T(); //这也是错的 vals = new T[10]; }}
对于数据,还有一个限制:
//这是错误的Gengens[] = new Gen [10];//这是对的Gen gens[] = new Gen[10];
4.3 歧义错误
下面这两个set方法就会产生冲突,如果T和V都传入String实参,就会产生歧义,这是编译不能通过的。
class MyGenClass{ 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 staticPair makePair(Class c) throws InstantiationException,IllegalAccessException { return new Pair<>(c.newInstance(),c.newInstance());}
makePair方法如果传入一个String.class,就会返回一个Pair<String>实例。这在构建框架代码时,非常常见。
反射是一个非常大的动态编程主题,后面会专门讨论它。