这章的标题就是泛型程序设计,所以很显然就是讲Java的泛型机制的。我觉得这个机制是Java一个非常重要的机制,这里我不讲怎样很好的使用,只总结一下书中的内容,并且说一下我个人的理解。
1.使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。
2.泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用。
3.在Java中增加泛型类之前,ArrayList是用一个Object引用数组来实现的。因此也能存储任何类型的对象。但是有两个不好的地 方:
① 当获取一个值的时候必须进行强制类型转换:
ArrayList files = new ArrayList();
String filename = (String) files.get(0);
② 没有错误检查,可以向数组列表中添加任何类的对象:
files.add(new File("..."));
对于这个调用,编译和运行都不会出错。然而,如果将这个对象取出并通过强制类型转换赋给一个String引用就会产生一个异常。
4.一个泛型类(generic class)就是具有一个或多个类型变量的类。
下面给出一个例子:
public class Pair<T>
{
private T first;
private T second;
public Pair() { first = null; second = null;}
//注意下面这两个方法都不是泛型方法
public T getFirst() { return first;}
public void setFirst(T newValue ) { first = newValue;}
......
}
泛型类还可以有多个类型变量。例如,public class Pair<T,U>{...}
5.Java与C++的泛型机制有着本质的不同!
6. 下面说下泛型方法的语法。
一个例子:
class ArrayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length/2];
}
}
这个是在一个普通类中定义的泛型方法。语法特征就是方在修饰符后面和返回类型前面的类型变量。
注意,泛型方法可以定义在普通类中,也可以定义在泛型类中。其实,泛型方法和泛型类没什么关系。不是说泛型类的方法都是泛型方法,其实往往都不是,就像上面的例子。
至于调用的方法是:
String middle = ArrayAlg.<String>getMiddle("John","Q","Public");
//其实也可以这样,直接让编译器通过参数来推断出T的类型
String middle = ArrayAlg.getMiddle("John","Q","Public");
7. 有时候,我们需要对类型变量的范围进行限制。这时就可以使用关键字extends。
具体的例子是:
public static <T extends Comparable> T min(T[] a) ...
这时,能用来调用这个方法的类型必须是实现了Comparable接口的类型,其它的类型编译器会报错。
至于为何不是使用implements关键字,因为T的实际绑定类型可以是类,也可以是接口,而extends更加接近子类型的概念。
8. 一个类型变量的限定可以有多个,用"&"符号来分隔。
具体的例子是:
T extends Comparable & Serializable
限定中,可以有多个接口超类型,但是至多有一个类。如果用一个类作为限定,它必须是限定列表中的第一个。
9. 下面开始说明Java泛型的基本实现原理。(类型擦除——erased)
虚拟机没有泛型类型对象——所有对象都属于普通类。
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。 擦除类型变量,并替换为限定类型。如果无限定则用Object代替,如果有多个限定则用第一个限定类型代替。
就这点而言,Java泛型与C++模板有很大的区别。C++中每个模板的实例化产生不同的类型,这一现象称为“模板代码膨胀”。Java则不存在这个问题。因为,对于不同的类型参数生成的对象,其实都是同一个类型,有相同的Class对象。
当程序调用泛型方法时,如果擦除返回类型,编译器会插入强制类型转换。
例如:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
类型擦除后,getFirst方法的返回类型实际上是Object类型。编译器会自动插入Employee的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法Pair.getFirst的调用
- 将返回的Object类型强制转换为Employee类型。
看到这里就明白了,其实还是有强制类型转换的操作,只不过这个操作由编译器自动地来为我们生成,而不用我们手写了!
10.下面说一下桥方法(bridge method)。
这个桥方法主要用来解决泛型类被继承的多态性实现的问题。
下面用一个例子说明:
class DateInterval extends Pair<Date>
{
public void setSecond(Date second) { ... }
}
上面这个类进行类型擦除后,实际上会变成:
class DateInterval extends Pair
{
public void setSecond(Date second);//这个类自己声明的
public void setSecond(Object second);//从Pair类继承过来的
}
出现这种情况是因为Pair中的方法并不是我们想象中以Date类型为参数的,而是经过类型擦除后以Object类型为参数。所以这两个方法因为参数类型不同,所以只是构成重载而不是覆盖,所以多态性无从谈起。
对于如下的代码:
DateInterval interval = new DateInterval(...);
Pair<Date> pair = interval;
pair.setSecond(aDate);
如果编译器不做一些私下的修改的话,这个pair.setSecond()实际上调用的就是Pair类的方法,而不是DateInterval的相应方法,没有实现多态性。
但是,编译器会做点修改来实现多态性的。方法就是在DateInterval类中生成一个桥方法:
public void setSecond(Object second) { setSecond((Date)second); }
这样就真正覆盖了超类的setSecond方法,顺利实现多态性。
11.这一点说一下使用Java泛型的约束与局限性。大多数的限制都是由类型擦除引起的。
- 不能用基本类型实例化类型参数
这个的原因就是类型擦除后,都会变成Object域,而Object并不能存储基本数据类型的值。
- 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有泛型对象的类型查询只会返回原始类型。
- 不能创建参数化类型的数组
这个原因我也不太明白。但是可以声明类型为参数化数组的引用变量,只是不能new。还有可以new 通配符类型的数组。
- 不能实例化类型变量
不能用像new T(...), new T[...]或T.class这样的表达式中的类型变量。因为类型擦除后,全部T换为Object,结果自然就变 味了。
- 泛型类的静态上下文中类型变量无效
例如:
public class Singleton<T>
{
private static T singleInstance;//ERROR
private static T getSingleInstance()//ERROR
{
if(singleInstance == null) { ...}
return singleInstance;
}
}
如果这个能运行,那么用不能类型生成的Singleton对象其实都是同一个引用。因为实际上它们就是同一个类,而静态域是属于整个类的。这就与我们想要的严重不符。所以从语法上规定这不可行。
- 不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。实际上,甚至泛型类扩展 Throwable都是不合法的。(这点的具体原因我也不明白)
12.这点说一下泛型类型的继承规则。
Pair<T>与Pair<S>没什么关系,即使T和S有继承关系,也是这样。
但是,泛型类可以扩展或实现其他的泛型类。如ArrayList<T>类实现了List<T>接口。所以,一个ArrayList<String>可以被转换为一个List<String>。
13.这点说一下非常重要的通配符类型。也就是“?”这个符号。
一开始我无法理解这个通配符类型,还把它与定义泛型类中的T视为差不多的东西。其实,不是这样的。理解这个通配符概念的关键就是——通配符类型就是一种类型,与什么String、Integer是一样。也就是说通配符?是一个类型实参而不是类型形参。只不过也有其特殊之处,就是能代表很多的真实类型。
所以说:
Pair<String> p1;//可以
Pair<?> p2;//可以
Pair<T> p3;//报错
其实,我觉得这个?有点类似C++11中的auto。就是我不知这到底是什么类型,但是你传给我就行了。
我们在定义泛型类的时候可以对T进行限定。同样,我们使用通配符的时候也可以进行限定,而且还可以进行超类型限定。
下面先说extends限定:
Pair<Manager> managerBuddies = new Pair<>(ceo,cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies;
wildcardBuddies.setFirst(lowlyEmployee);//compile-time error
Pair<? extends Employee> 表面这个变量可以接受Employee或其子类的引用,所以第二句不会报错。但是第三句就会报错。因为这个? extends Employee的范围太过广泛,例如Employee有多个子类,它们之间就不能相互转换,所以一旦确定了?是其中一个子类就不能再设定为其他的子类了,所以这就直接规定为语法层面上不行。
但是,使用getFirst就没有问题,因为都可以讲返回值赋值给一个Employee引用。
接着讲一下super限定,也就是超类型限定,这是通配符类型独有的限定。
语法格式为: Pair<? super Manager>。
这就把实际类型的范围限定为Manager或其的超类型。这时,我们倒是可以使用setFirst方法了,因为可以用Manager对象或其子类对象调用,因为Pair中必定是它们的祖先类引用,所以转换没有问题。
但是,这时又到getFirst有问题了。因为Manager的超类型也太广泛了,每个祖先类并不能相互转换。因为只能统一的转换为 Object。因此,getFirst这时的实际用途不大。
注意,super和extends两种不能共存的。
最后说下无限定通配符,也就是只有一个?,什么限定都没有。这种常用在与实际参数类型关系不大的泛型类中,例如Class<?>。
对于Pair<?>,getFirst的返回值只能赋给Object。setFirst方法不能调用,甚至不能用Object调用。(可以调用setFirst(null))
Pair<?>和Pair的本质不同在于:可以用任意Object对象来调用原始的Pair类的setObject方法。
14.最后这点说下反射与泛型。
首先,Class类是泛型的。
接着,Java泛型的卓越特性之一是在虚拟机中泛型类型的擦除。但是,擦除的类仍然保留了一些泛型祖先的微弱记忆。例如,原始的Pair类知道源于泛型类Pair<T>,即使一个Pair类型对象无法区分是由Pair<String>构造的还是由Pair<Employee>构造的。
注意,包含在类文件中的让泛型反射可用的类型信息与旧的虚拟机不兼容。
可以通过Type接口、TypeVariable接口、WildcardType接口、ParameterizedType接口和GenericArrayType接口来实现对泛型类的反射,从而得到泛型类型的完整声明。既包括<T>等的声明。
423

被折叠的 条评论
为什么被折叠?



