本文参考节选自 机工社《数据结构与算法分析 Java语言描述》
一. 简单的泛型类和接口
首先看以下代码中的GenericMemoryCell泛型类:
public class GenericMemoryCell
{
public AnyType Read(){return storedValue;}
pubic void Write(Anytype x){storedValue = x;}
private AnyType storedValue;
}
当声明一个泛型类时,类的声明则包含一个或者多个类型参数,这些参数被放在类名后面的一对的尖括号内。代码中的第一行指出了GenericMemoryCell拥有一个类型参数。在这个例子中对类型参数没有明显的限制,所以用户可以创建像GenericMemoryCell<String>和GenericMemoryCell<Integer>这样的类型,但是不能创建GenericMemoryCell
public interface Comparable
{
public int compareTo(AnyType other);
}
现在String类实现了Comparable
二. 自动装箱和拆箱
首先看以下代码:
public Class WrapperDemo
{
public static void main(String []args)
{
MemoryCell m = new MemoryCell();
m.write(new Integer(37));
Integer wrapperVal = (Integer)m.read();
int val = wrapperVal.intvalue();
System.out.println("Contents are:"+val);
}
}
上面的代码写的很麻烦,因为使用包装类需要在调用write之前创建Integer对象,然后才能使用inValue方法从Integer中提取int值。在Java5之前这是需要的,因为一个int类型的量被放到需要Integer类型的地方,那么编译将会产生一个错误信息,而如果将一个Integer对象的结果赋给一个int型的量,则编译也将产生一个错误信息。所以说,上面的代码准确的反映了基本类型和引用类型之间的区别,但是还没有清楚地表示出程序员把那些int存入集合(collection)的意图。
而Java5 矫正了这种情况,如果一个int类型被传递到一个需要Integer类型的地方,那么编译器将在幕后插入一个对Integer 构造方法的调用。这就是自动装箱。而如果一个Integer被放到一个需要int类型的地方,则编译器将在幕后插入一个对intValue方法的调用,这就叫自动拆箱。对于其他的7种基本类型,同样会发生类似的情形。下面的代码描述了自动装箱和自动拆箱的使用。注意:在GenericMemoryCell中引用的那些实体依旧是Integer对象;在GenericMemoryCell实例化过程中int不能够替代Integer。
Class BoxingDemo
{
public static void main(String []args)
{
GenericMemoryCell m = new GenericMemoryCell();
m.write(37);
int val = m.read();
System.out.println("Contents are:"+val);
}
}
三. 带有限制的通配符
首先看以下代码:
public static double totalArea(Shape []arr)
{
double total =0;
for(shape s: arr)
if(s! = null)
total += s.area();
return total;
}
这是一个static方法,该方法计算了一个shape数组的总面积(假设Shape是含有area方法的类;而Circle和Square是继承Shape的类)。假设我们要重写这个计算总面积的方法,使得该方法能够使用Collection
public static double totalArea(Collection arr)
{
double total =0;
for(shape s: arr)
if(s! = null)
total += s.area();
return total;
}
如果传递一个Collection
因为使用泛型的全部原因就在于为了产生编译错误而不是产生运行不匹配的运行时异常,所以泛型集合不是协变的。因此我们不能把Collection
于是现在我们又遇到了新的问题,泛型(及泛型集合)由于不是协变的,而数组是协变的。若没有附加的语法,那么用户就会避免使用集合(Collection),因为失去协变性让代码缺少灵活性。
Java5使用通配符来弥补了这个不足。通配符用来表示参数类型的子类或者超类,以下代码描述了带有限制的通配符的使用:
public static double totalArea(Collection extends Shape> arr)
{
double total =0;
for(shape s: arr)
if(s! = null)
total += s.area();
return total;
}
其中编写一个将Collection
四. 泛型的static方法
继续首先看下面的代码:
public static double totalArea(Collection extends Shape> arr)
{
double total =0;
for(shape s: arr)
if(s! = null)
total += s.area();
return total;
}
从某种意义上来说,上面的代码中的totalArea方法是一个泛型方法,因为它能够接受不同类型的参数。但是这里没有特定类型的参数表,正如在GenericMemoryCell类的声明中所做的那样,有时候特定类型很重要,这或许是由于以下的原因:
如果是在这样,那么必须要声明一种带有若干参数的的显式泛型方法。
例如下列代码就描述了一种泛型的static方法,该方法对x在数组中进行一系列的查找。通过使用一种泛型方法,代替使用Object对象作为参数的非泛型方法,当在Shape对象的数组中查找Apple对象时我们得到编译时错误。
public static boolean contains(AnyType []arr,AnyType x)
{
for(AnyType val:arr)
if(x.equals(val))
return true;
return false;
}
泛型方法特别像泛型类,因为类型参数表使用相同的语法,在泛型方法中类型参数表位于返回类型之前。
五. 类型界限
假设我们想要编写一个findMax方法。先看以下的代码:
public static AnyType findMax(AnyType[] arr)
{
int maxIndex = 0;
for(int i=1;i0)
maxIndex = i;
return arr[maxIndex];
}
由于在上面的代码第6行编译器不能保证对于compareTo的调用时合法的,因此程序不能正常运行;只有在AnyType是Comparable的情况下才能保证compareTo的存在。不过我们可以使用类型限界来解决这个问题。类型限界在简括号内进行指定,它指定参数类型必须具有的特性。所以一种自然而然的想法是把性质改写成:
public static AnyType findMax(AnyType []arr)
{...}
我们知道如今在Java5下Comparable接口如今是泛型的,所以这种做法非常自然而然,不过更好的方法应该如下:
public static > AnyType findMax(AnyType []arr)
{...}
然而这个方法仍然不能让人满意。为了看清这个问题,按以下思路再思考这个问题:
首先假设Shape类实现接口Comparable
应该说,AnyType IS-a Comparable
public static > AnyType findMax(AnyType []arr)
{...}
以下的代码为findMax方法的实现:
public static > AnyType findMax(AnyType []arr)
{
int maxIndex = 0;
for(int i=1;i0)
maxIndex = i;
return arr[maxIndex];
}
对于以上的代码,编译器接受了类型T的数组,只是使得T实现Compare接口,其中T IS-A S。
六. 类型擦除
泛型很大程度上是Java 语言中的成分而不是虚拟机中的结构。泛型类可以由所谓的类型擦除(type erasure)过程而转变成非泛型类。这样编译器就生成一种和泛型类同名的原始类(raw class),但是类型参数都被删去了。类型变量由它们的类型限界来代替,当一个具有擦除返回类型的泛型方法被调用时候,一些特性被自动的插入。如果使用一个泛型类而不带类型参数,那么使用的是原始类。
类型擦除的一个很重要的推论是:所生成的代码和程序员在泛型之前所写的代码并没有太大的不同,而且事实上运行的也并不快。不过其显著的优点在于,程序员不必把一些类型转化放到代码当中,编译器将进行重要的类型检验。
七. 对于泛型的限制
对于泛型类型有许多的限制。由于类型擦除的原因,这里列出的每一个限制都是必须要遵守的。
(1)基本类型
基本类型不能用作类型参数
(2)instanceof检测
instanceof 的类型检测和转换工作只针对原始类型进行。例如以下的代码:
GenericMemoryCell cell1= new GenericMemoryCell();
cell1.write(4);
Object cell = cell1;
GenericMemoryCell cell2= new GenericMemoryCell();
String s = cell2.read();
这里的运行转换在运行时是成功的,因为所有的类型都是GenericMemoryCell。但是在最后一行,由于对read的调用企图返回一个String对象从而产生一个运行时异常。结果类型转换将产生一个警告,而对应的instanceof 检测是非法的。
(3)static的语境
在一个泛型类中,static类和static域都不可引用类的类型变量,因为在类型擦除后类型变量就不复存在了。另外由于只存在一个原始的类,因此static域在该类型的诸泛型实例之间是共享的。
(4)泛型类型的实例化
不能创建一个泛型类型的实例。如果T是一个类型变量,那么语句:
T obj = new T();
是非法的。T由它的限界代替,这可能是Object类甚至是抽象类,因此对new的调用没有意义。
(5)泛型数组对象
也不能创建一个泛型数组,如果T是一个类型变量,则语句:
T[] arr = new T[10];
是非法的。T将有它的限界代替,这很有可能是Object T,于是(有擦除类型产生的)对T[ ] 的类型转换将无法进行,因为Object IS-NOT-A T[ ]。由于我们不能创建泛型对象的数组,一般来说我们必须创建一个擦除类型的数组,然后使用类型转换。这种类型转换将产生一个为检验的类型转换的编译警告。
(6)参数化类型数组
参数化类型的数组的实例化是非法的。考虑一下代码:
GenericMemoryCell []arr1 = new GenericMemoryCell[10];
GenericMemoryCell cell = new GenericMemoryCell();cell.write(4.5);
Object []arr2 = arr1;
arr2[0] = cell;
String s arr1[0].read();
正常情况下,我们认为i第4行的赋值会生成一个ArrayStoreException,这是因为赋值类型有错。可是,在擦除类型后,数组类型为GenericMemoryCell[ ],而加到数组中的对象也是GenericMemoryCell,因此不存在ArrayStoreException异常。于是代码没有类型转换,它最终将在第5行代码产生一个ClassCastException异常,这正是泛型应该避免的情况。