前言
Java在JDK1.5版本中加入了泛型,加入泛型机制的主要原因是为了满足在1999年制定的最早的Java规范需求之一(JSR14),专家组花了5年时间来定义规范和测试实现。
使用泛型编写的程序要比那些杂乱使用Object
变量,再进行强制类型转换的代码有着更好的安全性和可读性,泛型尤其对集合类有用,例如ArrayList
类。Java泛型看起来很像C++中的模板概念。
为什么使用泛型
泛型程序意味着编写的代码可以被很多不同类型的对象重用,Java在增加泛型类之前,是通过继承来实现泛型程序设计的。以ArrayList
为例,其内部维护了一个Object
数组:
|
|
这样实现有两个问题,一是获取对象的时候必须进行强制类型转换:
|
|
此外,没有错误检查,因此可以向ArrayList
中添加任何类的对象。
|
|
对于这个添加过程,编译和运行都不会出错,但当我们调用get()
方法进行强制类型转换时,便会出现错误。
因此,泛型类为这类问题提供了更好的解决方法,类型参数(type parameters),ArrayList
类有一个类型参数用来指示元素的类型:
|
|
这样的代码具有更好的可读性,我们一看就知道ArrayList
内部存储的是String
对象。这样对于get()
方法和add()
方法,编译器都可以更好的工作,调用get()
时不再需要强制类型转换,编译器知道返回值的类型是String
,而add()
方法编译器则知道其有一个String
类型的参数,编译器可以检查,避免插入错误类型的对象。
泛型使用
泛型的使用基本包括泛型类,泛型接口,泛型方法三个方面,下面一一展开。
泛型类
首先定义一个简单的泛型类,代码如下:
|
|
GenericClass
类通过一个类型变量T
,用尖括号(<>
)括起来,并放在类名后面,泛型类可以同时使用多个类型变量,如下:
|
|
当我们使用泛型类时,可以将一个引用类型传给类型变量(不能用基本数据类型),实例化泛型类对象:
|
|
那么下面代码:
|
|
输出为:
|
|
泛型接口
定义泛型接口和泛型类基本差不多,代码如下:
|
|
泛型接口的使用分为两种,一是实现接口的类继续声明泛型类型:
|
|
另一种是在实现泛型接口时指明参数类型,如下:
泛型方法
定义一个泛型方法如下:
|
|
这个方法是在普通类中定义的,不是在泛型类中定义的,这里的类型变量放在修饰符(public static)的后面,返回类型的前面。因此定义格式如下:
|
|
泛型方法可以定义在普通类中,也可以定义在泛型类中,调用泛型方法如下,在方法名前的尖括号中放入具体的类型:
|
|
类型变量的限定
有时候,类或者方法需要对类型变量加以约束,下面给出一个例子,我们要计算数组中的最小元素:
上述代码有个问题,我们定义了一个smallest
变量,他可以是任何一个类的对象,而且我们还使用了compareTo()
方法,但是我们无法保证所有类中都有compareTo()
方法。因此我们可以对类型变量T
做出一些限制,将T限制为实现了Comparable
(只包含一个compareTo()
方法)接口的类,如下所示:
|
|
现在泛型方法min
只能被实现了Comparable
接口的类(如String,Date)的数组调用,其他类调用则会产生编译错误。
一个类型变量可以有多个限定,比如:
|
|
限定中可以有多个接口类型,但是只能有一个类,而且类必须是限定列表中第一个。
继承规则
使用泛型类时,需要了解一下有关继承和子类型的准则,考虑一个类及其子类,如Employee
和Manager
,GenericClass<Manager>
是GenericClass<Empolyee>
的子类吗?答案是”不是”,就是说无论S和T有什么联系,通常GenericClass<S>
和GenericClass<T>
没有什么关系。
泛型类可以扩展或实现其他的泛型类,例如ArrayList<Manager>
类实现List<Manager>
接口,一个ArrayList<Manager>
可以被转换为一个List<Manager>
。
通配符
固定的泛型类型使用起来会很麻烦,通配符类型:
表示任何泛型GenericClass
类型,类型参数是Employee
的子类,如GenericClass<Manager>
,GenericClass<Manager>
是GenericClass<? extends Employee>
的子类型。GenericClass<? extends Employee>
的set
和get
方法分别如下:
当我们调用setVar()
方法时,编译器只知道需要某个Employee
的子类型,但不知道具体是什么类型,因此会出现错误。但是调用getVar()
方法就不存在这个问题,将getVar()
的返回值赋给一个Employee
的引用完全没问题。这个便是引入有限定的通配符的关键之处。
超类型限定
通配符限定与类型变量限定十分类似,但是还可以指定一个超类型限定,如下所示:
这个通配符限制为Manager
的所有超类型限定,带有超类型限定的通配符可以为方法提供参数,但不能使用返回值,GenericClass<? super Manager>
有方法:
编译器不知道setVar()
方法的确切类型,但是可以用任意Manager
对象(或子类型)调用它,而不能用Employee
对象调用。如果调用getVar()
方法,返回的对象类型不会得到保证,只能把它赋给一个Object
。
综合前面子类型限定通配符,我们得到以下结论:
带有超类型限定的通配符可以向泛型写入,带有子类型限定的通配符可以从泛型对象读取。
无限定通配符
还可以使用无限定的通配符,比如:GenericClass<?>
,类型GenericClass<?>
的方法如下:
setVar()
方法不能被调用,甚至不能被Object
调用,而getVar()
的返回值只能返回给一个Object
,类型GenericClass<?>
和类型类型GenericClass
的本质区别在于:可以用任意Object
对象调用原始的GenericClass
类的setObject
方法。
无限定通配符对于一些简单的操作非常有用,下面的方法是测试一个GenericClass
是否包含一个null
引用:
泛型与虚拟机
类型擦除
在虚拟机中并没有泛型类型对象,所有对象都属于普通类。无论何时定义一个泛型类型,都自动提供了一个相应的原始类型,原始类型的名字就是删除类型参数后的泛型类型名,然后擦除类型变量,用限定类型替换,如果没有限定类型则用Object
替换。比如,上文中的GenericClass<T>
的原始类型如下:
因为T
是一个无限定的类型变量,所以用Object
替换。当有多个限定类型变量时,用第一个限定的类型变量替换。所有的GenericClass
的泛型类型实质是同一个类。如下:
|
|
运行结果为:
翻译泛型表达式
当程序调用泛型类中的方法时,如果擦除返回类型,编译器插入强制类型转换,下面两条语句中擦除getVar()
方法的返回类型后将返回Object
类型,编译器自动插入Date
的强制类型转换。
编译器把这个方法调用翻译成两条虚拟机指令:
- 对原始方法
GenericClass.getVar()
调用 - 对返回的
Object
类型强制转换为Date
类型
当存取泛型类中的域时也会插入强制类型转换,假设GenericClass
中的var
变量是公有的(public),那么表达式:
也会在结果字节码中插入强制类型转换。
桥方法
类型擦除带来了一些复杂问题,看下面示例:
类MyDate
继承了GenericClass<Date>
,并覆盖了setVar()
方法,这个类擦除后变成:
但是问题来了,前面我们说过GenericClass<Date>
类型擦除后,存在一个setVar(Object)
方法,和setVar(Date)
显然是两个方法,因为他们有不同的类型参数。思考以下程序:
这里希望对setVar()
的调用具有多态性,并调用最合适的方法,由于date
引用mydate
对象,所以应该调用MyDate.setVar()
。问题在于类型擦除与多态发生了冲突,为了解决这个问题,编译器在MyDate
中生成了一个桥方法:
虚拟机用date
引用的对象调用setVar()
方法,这个对象是MyDate
类型的,因而会调用MyDate.setVar(Object)
方法,这个方法是新合成的桥方法。假设MyDate
也覆盖了getVar()
方法:
在类型擦除中,有两个getVar()
方法:
不能这样编写Java代码,因为具有相同参数类型的两个方法是不合法的,但是在虚拟机中,用参数类型和返回类型确定一个方法,因此编译器可能产生两个仅返回类型不同的方法字节码,虚拟机可以区分这两个方法。
Java泛型转换总结
- 虚拟机中没有泛型,只有普通类和方法
- 所有的类型参数都用它们的限定类型替换
- 桥方法被合成用来保持多态性
- 为保持类安全,必要时插入强制类型转换
约束与局限性
本节将阐述使用Java泛型时需要考虑的一些限制,大多数限制都是由类型擦除引起的。
类型判断只适用于原始类型
当我们在代码中进行判断某对象是否是特定类的实例时,如下:
上述代码仅仅测试a
是否是任意类型的一个GenericClass
。
强制类型转换时:
无论使用instanceof
或涉及泛型类型的强制转换表达式都会被编译器警告。
不能创建参数化类型数组
不能实例化参数化类型的数组,例如:
类型擦除后,strs
的类型是GenericClass[]
,可以把它转换为Object[]
:
|
|
数组会记住它的元素类型,如果试图存储其它元素类型,就会抛出一个ArrayStoreException
异常:
|
|
当我们在上述数组中存储泛型类型时,由于类型擦除会使数组这种机制失效,如下:
|
|
上面赋值语句经过类型擦除后,能够通过数组类型检查,不过仍会导致类型错误,基于以上原因,不允许创建参数化类型数组。
不能实例化类型变量
对于类型变量T,不能使用如下表达式:new T(...); new T[...]; T.class
。例如,下面的GenericClass<T>
构造器就是非法的:
但是,可以通过反射调用Class.newInstance
方法来构造泛型对象,如下:
使用该方法示例如下:
注意,Class
类本身是泛型,String.Class
是一个Class<String>
的实例。
泛型类中的静态域或静态方法中引用类型变量无效
不能在静态域或方法中引入类型变量,以下代码会报错。
实测,给出的错误是”无法从静态上下文中引用非静态类型变量T
“。 类型擦除后,由于静态域是类级别的,因此所有泛型类只包含一个single
域,而一个single
域无法安放多个类型属性。对于静态方法来说也是如此。
注意擦除后的冲突
|
|
实测,给出的错误是:”Error:(18, 20) java: 名称冲突: GenericClass中的equals(T)和java.lang.Object中的equals(java.lang.Object)具有相同疑符, 但两者均不覆盖对方
“。equals()
方法被擦除类型后变为public boolean equals(Object value)
,与类Object
的public boolean equals(Object value)
一样,而两者均不能覆盖对方,导致两者方法冲突。