12.1 泛型基本概念
引入泛型的主要动机是让开发人员更安全地使用Java标准库中的集合类,尽早地发现一些代码中包含的潜在错误。Java的集合类框架在JDK 1.2中被添加到Java标准库中,其中包含了常用的java.util.List、java.util.Map和java.util.Set等接口及其实现类。在J2SE 5.0引入泛型之前,Java中的集合类对象实际上是异构类型对象的集合。为了能够存放任何类型的对象,集合类中的元素的类型统一为Object类。在存放元素时,不论对象的实际类型如何,都可以将其保存到集合中。在读取元素时,需要对得到的对象进行强制类型转换,转换成对象的实际类型。使用强制类型转换的问题在于,运行时才可能发现类型不兼容的情况,由java.lang.ClassCastException异常来表示。运行时才发现的错误的处理代价比编译时发现的错误的处理代价要高得多,应该尽可能早地发现这些错误。
比如,在创建了一个List接口的实现类ArrayList的对象之后,可以向该对象中添加任何类型的对象。该ArrayList类的对象中可以同时包含String类和Number类的对象,这通常不是程序中所需要的行为。集合中通常包含的是同构类型的对象,但是Java语言并没有提供相应的机制来阻止向一个集合类的对象中添加不正确类型的对象,开发人员也不能在代码中表明集合类的对象中应该包含的对象类型。向一个集合类的对象中添加不兼容类型的对象,在编译时并不会出错。在运行时,当程序使用不正确类型的对象时,会在进行强制类型转换时抛出ClassCastException异常。
为了实现类型安全的集合类,J2SE 5.0引入了泛型的语言特性。泛型中包含的具体内容比较多,主要包括泛型类型和泛型方法的声明和实例化。泛型的引入也对Java标准库中的很多API造成了影响。泛型类型与一般类型的区别在于,泛型类型有形式类型参数(type parameter),可以在泛型类型被实例化时替换成实际的具体类型(type argument)。先从一个具体的示例说起。代码清单12-1中的ObjectHolder类是一个简单的泛型类,用来保存特定类型的对象引用。与一般的类型不同,泛型类型声明中的“<T>”用来表示形式类型参数T。形式类型参数T可以用在泛型类型实现的代码中。
代码清单12-1 声明泛型类型的示例
public class ObjectHolder<T>{
private T obj;
public T getObject(){
return obj;
}
public void setObject(T obj){
this.obj=obj;
}
}
泛型类的使用与一般的Java类并没有太大的区别,只是需要为其中声明的形式类型参数指定实际的类型。代码清单12-2给出了泛型类ObjectHolder的基本使用方式。形式类型参数T在实例化泛型类型时被替换成实际类型String。
代码清单12-2 使用泛型类型的示例
ObjectHolder<String>holder=new ObjectHolder<String>();
holder.setObject("Hello");
String str=holder.getObject();
在创建出泛型类的对象之后,该对象在使用时的类型是受限的,比如代码清单12-2中的holder对象,在调用setObject方法时,参数的类型只能是String类型;getObject方法的返回值也是String类型。如果不使用泛型来实现类似的功能,那么setObject方法的参数声明只能是Object类。同一对象的某个使用者可能在调用setObject方法时传入一个String类的对象,而另外一个使用者可能错误地认为其中包含的是Number类的对象,在调用getObject方法之后进行强制类型转换。这样在运行时会出现ClassCastException异常。如果希望只保存String类的对象,在不使用泛型的情况下,需要把setObject方法的参数和getObject方法的返回值都声明为String类型。这样在保存其他类型的对象时,需要创建对应类型的新的Java类。这些Java类的内部逻辑是相同的,造成了不必要的代码重复。通过使用泛型,只需要一个Java类就可以表示不同类型的对象。
结合代码清单12-1和代码清单12-2中对泛型类的声明和使用方式,对泛型相关的基本概念进行具体说明。如果在一个类型中使用了形式类型参数,则称该类型为泛型类型(generic type)。代码清单12-1中的ObjectHolder类是一个泛型类。泛型类型可以被实例化。在实例化之后,泛型类型声明中的形式类型参数被替换成实际的类型。实例化之后的泛型类型被称为参数化类型(parameterized type)。代码清单12-2中的ObjectHolder<String>是一种参数化类型。对于同一个泛型类型来说,可能的参数化类型的数量非常多。根据使用的实际类型,参数化类型分为两类:一类是不带通配符的类型,另外一类是带通配符的类型。通配符(wildcard)“?”的作用是表示一组类型的集合,可以匹配特定范围内的类型。在使用通配符时可以指定其上界或下界。通过添加上界或下界可以限制通配符表示的具体类型的范围。不包含上界或下界的通配符被称为无界通配符(unbounded wildcard)。例如,参数化类型ObjectHolder<?>表示其中包含的对象的具体类型是不确定的,可以是任何类型。在声明形式类型参数时也可以指定上界,用来限制实例化时可用的实际类型的范围。
在Java中,除了枚举类型、匿名内部类型和异常类型之外,其他类型都可以添加形式类型参数,成为泛型类型。形式类型参数的名称可以是Java中任何合法的标识符。一般使用单个大写字母作为形式类型参数的名称,以区别于一般的标识符。形式类型参数可以有多个,如“MyClass<S, U,V>”中声明了3个形式类型参数。不同的形式类型参数在代码中表示不同的含义。以集合类框架为例,List接口只包含一个形式类型参数,表示列表中包含的元素的类型;而Map接口则包含两个形式类型参数,分别表示映射表中条目的键和值的类型。
形式类型参数类似于一般的类型,但是两者存在一些差别。两者的共同之处在于都可以作为类型使用在某些场合,包括作为方法的参数和返回值类型、作为域和局部变量的类型声明、进行强制类型转换及作为泛型类型和泛型方法的实际类型参数。但是形式类型参数在某些情况下是不能使用的,包括不能用来创建对象和数组、不能作为父类型、不能使用在instanceof表达式中、不能使用其类型字面量、不能出现在异常处理中,以及不能出现在静态上下文中。这就意味着,如果T是形式类型参数,类似“new T()”、“new T[]”、“class MyClass extends T”、“instanceof T”、“T.class”、“catch(T)”和“static T”等都是无法通过编译的错误用法。这些限制源于Java中泛型类型的实现机制,即类型擦除(type erasure)。
为了兼容J2SE 5.0之前的遗留代码,泛型类型在使用时可以不指定实际类型。如果不指定实际类型而直接使用类型声明,所得到的类型被称为原始类型(raw type)。如果在代码中直接使用ObjectHolder进行声明,则使用的是泛型类ObjectHolder的原始类型。原始类型的作用是与无法使用泛型的遗留代码进行交互。除此之外,原始类型不应该用在其他地方,否则引入泛型就变得毫无意义。使用原始类型是不安全的操作,编译器会给出相关的警告信息。
在构造方法或一般方法的声明中也可以使用形式类型参数。包含形式类型参数的方法被称为泛型方法。泛型方法与泛型类型并没有直接的关系。在一个非泛型类型中同样可以包含泛型方法。泛型类型中的泛型方法可以使用在类型中定义的形式类型参数,也可以使用自己的形式类型参数。在调用泛型方法时,通常不需要显式指定所用的实际类型。编译器可以根据方法调用时的实际参数类型和上下文信息进行类型推断。代码清单12-1中的ObjectHolder类中的setObject和getObject都是泛型方法。
在使用泛型的情况下,编译器会对代码进行详细的类型检查。对于可以确定为错误的类型使用的地方,编译器会给出错误信息;对于无法判断是否正确的情况,编译器会给出警告信息,这意味着代码中存在可能的类型安全问题,开发人员需要谨慎处理这些警告。如果确定没有问题,那么可以使用“@SuppressWarnings("unchecked")”注解来抑制警告信息的输出。通常只有使用原始类型与遗留代码交互时所产生的警告信息才是可以忽略的。如果代码在编译过程中没有出现任何与类型安全相关的警告信息,那么代码在运行时肯定不会出现未预期的ClassCastException异常。