泛型类
java泛型类的定义中,在尖括号中把类型变量依次列举出来,按照惯例,在JDK中,使用E表示集合的元素类型,K与V分别表示键与值。T用来表示“任意类型”。一个简单的泛型类如下:
public class Generic<T,U>{
private T a;
private U b;
public Generic(T a,U b){
this.a=a;
this.b=b;
}
}
泛型方法
泛型方法可以定义在普通类中,与泛型类的定义比较相似的一点是,在定义中需要添加类型变量。需要注意的一点是:类型变量所放的位置在修饰符与返回类型之间。
public class Original{
public <T> void hello(T message){
System.out.println(message);
}
}
类型变量的限定
对于类型变量有时候需要对其做一些约束,可以通过如下的一个场景引出:
public static <T> T findMin(T[] vals){
T minVal=vals[0];
for(int i=1;i<vals.length;i++){
if(minVal.compareTo(vals[i])>0)//默认了T类型有该方法
minVal=vals[i];
}
return minVal;
}
为了保证元素比较中一定可以使用compareTo
方法,则必须对类型变量T
加以约束:
public static <T extends Comparable> T findMin(T[] vals){
...
}
在泛型中,通过extends
关键字,使用语法<T extends BoundingType>
来约束变量类型。一个类型变量或通配符可以有多个绑定类型,如:<T extends Comparable & Serializable>
。
类型擦除
对于JVM而言,其中所有的对象都是普通对象,并没有泛型类型对象。这里面一个衔接源代码中的泛型代码与JVM中普通字节码文件的一个重要机制就是类型擦除。
对于任意一个泛型类型,都对应一个相应的普通类型(raw type)。普通类型即泛型类型去掉类型变量。擦除是指将泛型类型中的类型变量替换为限定类型/绑定类型(DoundingType),默认为Object
。
泛型表达式的擦除
对于刚才说的Generic
类,使用我之前java 反射博客中提到的ReflectionTest
测试,在控制台输入Generic,会得到如下结果:
public class Generic
{
public Generic(java.lang.Object, java.lang.Object);
private java.lang.Object a;
private java.lang.Object b;
}
如果将之前的Generic
修改为:
public class Generic<T extends Comparable,U extends Serializable>{
private T a;
private U b;
public Generic(T a,U b){
this.a=a;
this.b=b;
}
}
那么对应的结果变成了:
public class Generic
{
public Generic(java.lang.Comparable, java.io.Serializable);
private java.lang.Comparable a;
private java.io.Serializable b;
}
这样就验证了擦除机制。这里面还有一个细节问题就是,如果类型变量的限定类型不止一个,那么在擦除之后,泛型类型变成了第一个限定类型。再次修改Generic类为:
public class Generic<T extends Comparable & Serializable,U>{
private T a;
private U b;
public Generic(T a,U b){
this.a=a;
this.b=b;
}
}
ReflectionTest
程序执行结果:
public class Generic
{
public Generic(java.lang.Comparable, java.lang.Object);
private java.lang.Comparable a;
private java.lang.Object b;
}
调整Comparable,Serializable
的位置之后,结果为:
public class Generic
{
public Generic(java.io.Serializable, java.lang.Object);
private java.io.Serializable a;
private java.lang.Object b;
}
为了提高效率,应该将标记接口放在列表的末尾(如这里面的标记接口为Serializable
)。
由于泛型擦除,那么编译器对泛型源代码编译的过程中,会通过插入一些强制类型转化的代码保持原来的语义。对于之前的Generic
,稍作修改,如下:
public class Generic<T,U>{
private T a;
private U b;
public Generic(T a,U b){
this.a=a;
this.b=b;
}
public T getA() {
return a;
}
public static void main(String[] args){
Generic<Integer,String> obj=new Generic(1,"hello");
Integer a=obj.getA();//插入强制类型转化字节码
}
}
使用jdk自带的javap
反汇编程序分析main
函数,控制台执行javap -v Generic
有如下结果:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=3, args_size=1
0: new #4 // class Generic
3: dup
4: iconst_1
5: invokestatic #5 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
8: ldc #6 // String hello
10: invokespecial #7 // Method "<init>":(Ljava/lang/Object;Ljava/lang/Object;)V
13: astore_1
14: aload_1
15: invokevirtual #8 // Method getA:()Ljava/lang/Object;
18: checkcast #9 // class java/lang/Integer
21: astore_2
22: return
LineNumberTable:
line 12: 0
line 13: 14
line 14: 22
第18行的字节码checkcast
就验证了之前的结论。
泛型方法的擦除
由于java中的实例方法会涉及到动态绑定,而动态绑定需要知道运行时对象的实际类型,这样就和擦除机制相互矛盾了。下面举例说明:
class Pair<T extends Comparable> {
private T start;
private T end;
public Pair(){}
public Pair(T start,T end){
this.start=start;
this.end=end;
}
public T getStart() {
return start;
}
public void setStart(T start) {
this.start = start;
}
public T getEnd() {
return end;
}
public void setEnd(T end) {
this.end = end;
}
}
public class TimePair extends Pair<Long>{
public void setEnd(Long end){
if(this.getStart().compareTo(end)<=0)
setEnd(end);
}
public static void main(String[] args){
TimePair timePair=new TimePair();
Pair<Long> pair=timePair;//此处发生了类型擦除
pair.setEnd(1L);//此处又需要记住实际的元素类型为Long
}
}
通过执行javap TimePair
,输出如下:
class TimePair extends Pair<java.lang.Long> {
TimePair();
public void setEnd(java.lang.Long);
public static void main(java.lang.String[]);
public void setEnd(java.lang.Object);//该方法就是为了多态性而添加的【桥方法】
}
这里面的逻辑是这样的:父类Pair
类型擦除之后,setEnd()
方法的参数类型为Object
,而在子类TimePair
中,并没有对该方法进行重写(override),而因为public void setEnd(java.lang.Long)
发生了覆盖,这样就无法进行动态绑定了,通过添加桥方法即解决了这个问题。
对之前的内容做个简单的总结如下:
- 泛型对JVM透明,JVM中只有普通的类和方法;
- 类型参数在编译过程中都会被它们的限定类型替换;
- 桥方法用来保持多态性;
泛型擦除的局限性
类型检查与原始类型
由于发生了类型擦除,无论是通过instanceof
方式,该是通过getClass()
方法来检查一个对象的实际类型,其实际类型只适合于原始类型。
Pair<Long> longPair=new Pair();
Pair<String> strPair=new Pair();
System.out.println(longPair.getClass()==strPair.getClass());//true
System.out.println(longPair instanceof Pair<Long>);//error
System.out.println(longPair instanceof Pair);//true
带类型参数的数组
无法实例化带类型参数的数组,这是由于多态与类型擦除的矛盾造成的。
Pair<Integer>[] intPairs=new Pair<>[10];
Object[] objs=intPairs;//Pair[] 自动转化为 Object[],Object[] 能记住元素类型为Pair,但不是Pair<Integer>
Pair<String> strPair=new Pair();//类型擦除之后为Pair
objs[0]=strPair;//不会触发ArrayStoreException,但是存在类型不一致错误
为了防止上述情况的发生,不允许(人为)创建带类型参数的数组。在一些特殊的情况下,这种数组可由虚拟机构造,如下:
public class GenericArray{
static <T> void addAll(Collection<T> c,T... vals){
for(T val:vals)
c.add(val);
}
public static void main(String[] args){
addAll(new ArrayList<Pair<Integer>>(),new Pair<Integer>());
}
}
调用addAll()
方法,JVM就必须对vals建立一个Pair[]
数组,不过由于这不是显示编写的,编译器会警告,而不会报错。执行javac GenericArray
,会看到如下结果:
Note: TimePair.java uses unchecked or unsafe operations.
类型变量的实例化
不能使用类似new T(); new T[…],T.class
的语句/表达式。由于类型擦除new T()
转化为new Object()
,这是和原意不符的,而T.class
也会转化为Object.class
,会导致相同的问题。
解决该问题可以通过下面两种方法:
使用lambda表达式,需要提供一个接受lambda表达式的方法。对于上面的Pair
类,可以这么写:
public static <T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get(),constr.get());
}
Supplier<T>
是一种函数式接口,表示无参数且返回类型为T的函数。
使用反射机制解决,实现方式如下:
public static <T> Pair<T> makePair(Class<T> cls){
try{
return new Pair<>(cls.newInstance(),cls.newInstance())
}
catch(Exception e)[return null;}
}
由于Class
类本身就是泛型的,Integer.class,String.class
分别是Class<Integer>,Class<String>
的子类的唯一实例对象。makePair
方法自然就能推断出pair的类型。
泛型数组
由于类型擦除,无法创建泛型数组,举例如下:
public <T extends Comparable> void createGenericArray(){
T[] genericArray=new T[2];//类型擦除导致出现 new Comparable[2];
}
泛型类型的继承规则
即使类型变量之间具有继承关系,但是这和对应的泛型类型无关,泛型类型之间依然是相互独立的。
class A{}
class B extends A{}
Pair<B> bPair=new Pair<>();
Pair<A> aPair=bPair;//error
通配符类型
使用通配符类型,可以使得变量类型更加灵活,如
Pair<? extends Number>
表示类型变量为Number
子类型的Pair
泛型类型。通过这种方法,就可以很好地解决上一节出现的问题:
class A{}
class B extends A{}
Pair<? extends A> aPair=new Pair<A>();
Pair<? extends A> bPair=new Pair<B>();
aPair=bPair;//right
bPair=aPair;//right
这里的逻辑关系是:Pair<? extends A>
是Pair<A>,Pair<B>
的公共父类。不过由于Pair<? extends A>
在类型擦除之后并不知到存储的对象start,end
的实际类型,如果调用setXXX()
赋值,就无法保证类型一致。而对于getXXX()
方法,没有这个问题,因为类型擦除后,类型变量会替换为限定类型,此处即父类A
,而父类引用可以引用子类对象。
另外一个通配符使用super
关键字,如<? super B>
。这种情况下,无法调用getXXX()
方法,因为编译器并不知道返回的究竟是那个父类。对于setXXX()
,可以把限定类型对应的对象作为实参传入,也就是:
class A{}
class B extends A{}
Pair<? super B> pair=new Pair<>();
pair.setStart(new B());//right
pair.setEnd(new A());//error
总结下来也就是,对于<? super BoundType>
,可以向泛型对象写入,对于<? extends BoundType>
,可以从泛型对象读取。
最后一种是无限定通配符?
。使用无限定通配符构成的泛型类型与原始类型是有区别的:
Pair<?> pair=new Pair<A>();
pair.setStart(new A());//error
Pair rawPair=new Pair();
rawPair.setStart(new A());//right
Pair<?>
与Pair
本质区别在于:可以使用任意的Object
对象调用原始Pair
的setXXX()
。