泛型出现以前,泛型程序设计主要通过继承实现,例如ArrayList
中维护一个Object数组,即可接受所有类型的对象。
这种设计存在两个问题:
- 当从
ArrayList
中取出一个元素时,需要进行强制类型转换才能给子类对象。 - 当实例化一个
ArrayList
对象后,可以向其中加入任意类的对象,没有错误检查。
泛型的出现解决了以上两个问题,通过类型参数来指定元素的类型。
定义一个String链表ArrayList<String> list = new ArrayList<String>();
后一个String可以省略,编译器能够通过前一个String推断出泛型类型。
当使用ArrayList.get()
方法时,不需要再类型转换,直接赋值给String对象
类型参数的魅力在于:使得程序具有更好的可读性和安全性
一个简单的泛型类Pair
|
|
泛型类中,类型变量用<>括起来放在类型名的后面。在Java库中,使用E表示集合的元素类型,K、V分别表示Key、Value,T表示任意类型。
泛型方法
可以将方法声明为泛型方法,类型变量放在修饰符之后,返回类型之前。
|
|
调用
|
|
<String>
可以省略,编译器能够自动识别类型为String,但是参数列表需要都为String,否则会报错。
限定类型变量
当我尝试在泛型类中调用String.length()
方法时,会出现编译错误,这是显然的,因为目前这个类是未知类型,未来可能赋给它的是任意类型,无法保证这个属性中有哪些方法(但是一定包含Object的方法)。
为了解决这个问题,可以对类型变量加以约束,使用这样的语法<T extends BoundingType>
表示T应该是绑定类型的子类型,这个类型可以是类也可以是接口。
如果想要限定为 继承了某个类、实现了某些接口的话,需要使用&
分隔多个限定例如:T extends Comparable & Serializable
如果限定中有类,那么类名必须是列表中的第一个。
修改Pair类,编译通过。
|
|
泛型代码和虚拟机
其实泛型类只是在编码阶段优化了传统的基于继承的泛型程序设计,在编译之后,虚拟机中,是不存在泛型类型对象的,所有的对象都属于普通类。
类型擦除
对任意的泛型类型,都自动提供了一个相应的原始类型,就是泛型类型去掉类型参数之后的泛型类型名,对尖括号和其中的类型参数,直接删除,对其他的类型参数,用其限定类型代替,没有限定类型就用Object
如上面的Pair<T extends String>
泛型类型的原始类型是:
|
|
这样显然是合理的,这个泛型类型中,类型参数被限定为String
的子类,在编译前编译器借助泛型保证了传入的任何对象都可以赋给String对象的引用,所以在编译后不会出现问题。
思考一个问题,假如有多个类型限定,在泛型类中都使用了这多个类型的方法,那么类型擦除机制会选用那种类型呢?
比如T extends ArrayAlg & Comparable & ActionListener
,在泛型类中同时使用了:
|
|
首先,限定中的类一定写在第一个,否则编译不通过:
然后,编译器一定选择第一个类名或者接口名,这里是ArrayAlg
。
之后,T类型参数被擦除成为ArrayAlg
,但是因为其中有Comparable & ActionListener 接口的方法,所以报错:
对此,编译器会进行必要的类型转换:
|
|
这样是合理的,在这个泛型类型中,类型参数被限定为ArrayAlg
的子类,并且实现了Comparable & ActionListener 接口,这也就是说,赋值给Pair对象中first、second属性的对象一定有以下方法:
- test() –来自
ArrayAlg
- compareTo(T) –来自
Comparable
- actionPerformed(ActionEvent) –来自
ActionListener
所以即使编译后类型Pair中的类型参数被擦除为ArrayAlg
,Pair
对象中这个ArrayAlg
类对象的引用指向的对象一定包含上述三个方法域。所以,对这个ArrayAlg
对象进行强制类型转换,使其变成Comparable & ActionListener 接口,是没有问题的。
翻译泛型表达式
在擦除返回类型后,编译器会自动插入强制类型转换。
在使用泛型类型,如:
|
|
这里编译器根据ArrayList<String>
识别出类型参数是String类型,当删除这个类型参数后,编译器就会使用原始类型,于是出现类型不兼容:
加上类型转换即可解决:
|
|
所以说,泛型只存在于编码阶段,编译完成之前,泛型的意义就是增加了代码的可读性、安全性,以及减少了一部分代码量。
翻译泛型方法
类型擦除对泛型方法同样适用。看下面这个例子:
看似右边的方法是重写了父类的方法,但是在类型擦除后,父类方法其实变成了:public void setSecond(Object second)
这个方法理所应当的继承到了子类中,
而上面个这个子类方法是:public void setSecond(LocalDate localDate)
参数类型不一样,所以我们写的方法不是真正的重写,但是为了保持setSecond的多态性和代码的可读性,编译器会在子类中加上一个桥方法
:
|
|
第一个方法才是真正重写的方法,同时也是父子类的桥方法。在使用子类对象去调用如dateInterval.setSecond(date)
时,依据多态特性,调用的是桥方法,编译器给我们套了一层,让我们看起来是直接调用了我们写的方法。
对Setter来说,出现了返回类型相同,参数不同的方法,而对Getter来说,则是出现了返回类型不同,参数类型相同,在写Java代码时,这是不允许的。
但是在JVM中,用参数类型和返回类型确定一个方法,所以编译器能够正确处理这一情况。
Getter桥方法实现了,先调用我们”重写”的getter,然后在这个里面调用子类对象中从父类对象继承的Getter,获得Object对象,然后进行类型转换为DateInterval
返回。
约束与局限性
不能使用基本类型实例化类型参数
例如,没有Pair<double>
,只有Pair<Double>
,当然其原因是类型擦除,Pair类中包含Object类型的域,而Object不能存储double值。
运行时类型查询仅适用于原始类型
|
|
不能创建参数化类型的数组
这样创建后,pairs的类型是Pair[],可以将其转换为Object[]
数组会记住他的元素类型,如果向其添加一个String,就会报错。
对数组来说,它存储的类型就是Pair,这也就意味着,向其中放入一个别的参数类型的Pair对象如Pair<Employee>
那么就会通过数组的参数类型检查,因为类型擦除后类型都是Pair,但是在编译时会造成类型转换错误,因为如果这个对象中使用了Employee类中的方法,那么编译器会尝试使用类型转换