——每天的寥寥几笔,坚持下去,将会是一份沉甸甸的积累。
看懂了上一篇文章,大概就会用泛型,但你不理解其内部原理,这边文章来和大家探讨写泛型核心的难点——擦除。
其实,java虚拟机是没有泛型类型对象的,在虚拟机看来所有类都是普通类,这样的好处,就是不用改动虚拟机,同时也能兼容早期没有泛型时的程序,毕竟虚拟机没改动。
但是,问题来了,你写的代码里面明明有泛型,怎么到了虚拟机那里就都一样了呢?其实,这就是编译器做的好事,它把泛型改写了一下,改写成了和没有泛型前一样的通用版本代码,这样虚拟机也能认识了。这就是传说中的擦除(erased)——删掉类型参数并用限定类型(无限定类型时用Object)替换类型变量,擦除的类型也被称为原始类型。
这样的话,上一篇中的Pair<T>,就成了
public class Pair{//<T>类型参数被删掉了
private Object first;//T都被替换为Object
public Object second;
public Pair(Object first, Object second){
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
}
如果Pair<T>改成Pair<T extends Number>那么就不用Object替换T,而是用Number替换T
如果Pair<T>改成Pair<T extends Number&Comparable>那么就不用Object替换T,而是用多限定的第一个限定Number替换T
2.擦出过后又是如何找回原有类型?让虚拟机不是Object也不是Number而是某一特定的子类的呢?
举例:设上面的取Pair<T extends Number>
public static void main(String[] args) {
Pair<Integer> pair = new Pair(1,2);
Integer obj = pair.getFirst();//安全通过
}
由于擦除,那么必然调用pair.getFirst()会返回Number类型,可又是如何不用强转直接赋给Integer的obj引用的呢?
原因是:编译器帮忙执行了两条指令,其一就是调用getFirst()方法返回Number类型,但由于擦除前,有对Pair<Integer> pair中的Integer类型做了记录,因此,编译器会帮忙将Number类型转成Integer类型,因此,我们最后调用pair.getFirst(),得到的就是Integer类型,所以上面的代码安全通过了编译和运行。
3.上面是编译器翻译泛型表达式,然后在翻译泛型方法时就有些麻烦了。
举例:假设上面的取Pair<T>,然后ConcretePair继承Pair<String>
public class ConcretePair extends Pair<String> {
public ConcretePair(String first, String second) {
super(first, second);
}
@Override
public String getFirst() {
return super.getFirst();
}
@Override
public void setFirst(String first) {
super.setFirst(first);
}
@Override
public String getSecond() {
return super.getSecond();
}
@Override
public void setSecond(String second) {
super.setSecond(second);
}
}
根据擦除原理,Pair<T>类变成Pair类,同时用Object替换所有T。而对于Concrete类则变成了继承Pair——<String>被擦除了,但是Concrete类里面的String还是保留了下来。
这样就出现了一个问题,拿getFirst()方法来说,它是覆写了父类的方法,@override标记可以作证,但是由于擦除,父类成了Pair,而Pair类显然没有String getFirst(),那又怎么能说ConcretePair里的String getFirst()覆写了父类方法呢?出现矛盾了。那又是怎么解决的呢?
的确,这里的getFirst()方法由于返回类型是String,已经称不上是父类Pair中Object getFirst()的覆写了,@override注解也是形同虚设,不起作用了,也就是说这个getFirst()方法成了ConcretePair子类自己新添的方法,如果要调用必须把Pair<String>的父类型向下转型成ConcretePair,这样原来覆写的多态性被打破了。
即:
Pair<String> pair = new ConcretePair("sd","w");
pair.getFirst();//出错:本来由于多态,可以调用,但现在该方法成了ConcretePair自己新建的类而无法像这样调用。
((Concrete)pair).getFirst();//这样虽然可以调用,但不是我们的本意
那这种情况怎么解决呢?
编译器的解决方案是给你在ConcretePair类里生成你所想要的覆写方法Object getFirst()等,这些编译器自己加的方法被称为桥方法。也就是如下的情况:
public class ConcretePair extends Pair<String> {
public ConcretePair(String first, String second) {
super(first, second);
}
@Override
public String getFirst() {
return super.getFirst();
}
@Override
public void setFirst(String first) {
super.setFirst(first);
}
@Override
public String getSecond() {
return super.getSecond();
}
@Override
public void setSecond(String second) {
super.setSecond(second);
}
public Object getFirst() {//重名报错
return getFirst();
}
public void setFirst(Object first) {
setFirst((String)first);
}
public Object getSecond() {
return getSecond();
}
public void setSecond(Object second) {
setSecond((String)second);
}
}
当然,生成的四个方法我们是看不到的,也不会像上面那样(重名等各种报错)。但我们可以通过反编译字节码来看到,javap -c ConcretePair.class 【篇幅有限,我就贴多出来的四个方法了】。
public void setSecond(java.lang.String);
Code:
0: aload_0
1: aload_1
2: invokespecial #6 // Method Pair.setSecond:(Ljava/lang/Object;)V
5: return
public void setSecond(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/String
5: invokevirtual #7 // Method setSecond:(Ljava/lang/String;)V
8: return
public java.lang.Object getSecond();
Code:
0: aload_0
1: invokevirtual #8 // Method getSecond:()Ljava/lang/String;
4: areturn
public void setFirst(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #3 // class java/lang/String
5: invokevirtual #9 // Method setFirst:(Ljava/lang/String;)V
8: return
public java.lang.Object getFirst();
Code:
0: aload_0
1: invokevirtual #10 // Method getFirst:()Ljava/lang/String;
4: areturn
}
很清楚,的确多出来了四个覆写的方法,这样的话多态性就不会被打破,
Pair<String> pair = new ConcretePair("sd","w");
pair.getFirst();//这里调用的实际是编译器为我们生成的方法,然后该方法内部再调用我们所谓新建的String getFirst()方法,最后的返回类型就成了Object
(编译生成的方法),但类型转换不会出错,上面讲过了
ok,一切清楚。
4.延生(边边角角)
(1)实现Cloneable方法时,这里也用到了桥方法
public class Employee implements Cloneable{
public Employee clone(){...}
//实际上
//Employee clone() defined above
//Object clone() bridge method,overrides Object.clone,因为所有类都是Object的子类
}
(2)协变覆写(上面cloneable的例子):允许子类覆写函数的返回类型是父类被覆写函数返回类型的子类。即覆写了父类Object中的clone方法,而且返回类型Employee是Object的子类。
世界晚安,,西安晚安。。。。。