热修复Java探索之泛型编译

为什么需要泛型

Java5之前是没有泛型的,在Java5之前要想实现泛型的功能需要借助Object+类型强转实现,而类型强转的问题在于编译器是检查不到语法错误的,很多ClassCastException就会在运行期出现。

Java5才引入的泛型,此时由于扩展虚拟机指令推动厂商升级JVM显然是无法接手的,因此Java的泛型基本完全是在编译器层面实现的,生成字节码之前会被完全擦出,从而虚拟机完全无感知,这种技术被称为泛型擦除(erasure)

擦除

生成的字节码文件是不包含泛型信息的,我们看一下的例子。

public class GenericFoo<T> {
    private T  foo;
    public void setFoo(T foo){
        this.foo=foo;
    }
    public T getFoo(){
        return foo;
    }

    //public void setFoo(Object foo){
    //    this.foo= (T) foo;
    //}
}

反编译得到smali:

# instance fields
.field private foo:Ljava/lang/Object;

# virtual methods
.method public getFoo()Ljava/lang/Object;
.method public setFoo(Ljava/lang/Object;)V

可以很清楚的看到,编译后字节码泛型T 都直接被Object替换了,如果我们把上面注释的方法打开则会出现下面的提示:
'setFoo(T)' clashed with 'setFoo(Object)';both method has same erasure
就因为泛型擦除完实际就是’setFoo(Object)’,所以想在定义一个’setFoo(Object)’行不通的。

限定泛型

我们把上面的例子改一改:

public class GenericFoo<T extends Number> {
    private T  foo;
    public void setFoo(T foo){
        this.foo=foo;
    }
    public T getFoo(){
        return foo;
    }
}

得到字节码:

# instance fields
.field private foo:Ljava/lang/Number;
# virtual methods
.method public getFoo()Ljava/lang/Number;
.method public setFoo(Ljava/lang/Number;)V

可以看到泛型被定义的上界所替换。

泛型与多态

public class A<T> {
    private T t;

    public T get() {
        return t;
    }

    public void set(T t) {
        this.t = t;
    }
}

class B extends A<Number> {
    private Number n;

    @Override //类型擦除后的返回值应该是Object,为什么这里是重写父类get方法
    public Number get() {
        return n;
    }

    @Override//类型擦除后的参数类型应该是Object,为什么这里是重写父类get方法
    public void set(Number n) {
        this.n = n;
    }
}

class C extends A {
    private Number n;

    @Override //类型擦除后的返回值应该是Object,为什么这里是重写父类get方法
    public Number get() {
        super.get();
        return n;
    }

    //@Override 会提示没有对应方法重写,因为参数类型不是Object,这个可以理解
    public void set(Number o) {
        this.n = n;
    }
}

可以看到B类的set 和 get方法返回值和参数值并不是Object,但是却可以被@Override修饰,这代表了我们重写了A类的set和get方法,这和我们认知不一致,我们从编译的角度看一下:

# instance fields
.field private n:Ljava/lang/Number;
# virtual methods
.method public get()Ljava/lang/Number;
.method public bridge synthetic get()Ljava/lang/Object;
    invoke-virtual {p0}, Lcom/stv/test/B;->get()Ljava/lang/Number;
    move-result-object v0
    return-object v0
.end method

.method public set(Ljava/lang/Number;)V

.method public bridge synthetic set(Ljava/lang/Object;)V
check-cast p1, Ljava/lang/Number;

    invoke-virtual {p0, p1}, Lcom/stv/test/B;->set(Ljava/lang/Number;)V

    return-void
.end method

可以看到虽然我们并没有直接重写set、get方法,但是编译器帮我们做了,并且用bridge进行标识,而且内部逻辑也是简单调用我们自己的set、get方法。实际干的事情就像下面一样:

class B extends A<Number> {
    private Number n;

    public Number get() {
        return n;
    }
    @Override 
    public (bridge) Object get() {
        return get();//调用我们自己的方法
    }

    public void set(Number n) {
        this.n = n;
    }

    @Override
    public (bridge) void set(Object n) {
        Number n1 = (Number)n;
        set(n1);//调用我们自己的方法
    }
}

所以可以看出@Override只不过是个假象,实际是编译器重写了真正的父类方法然后再调用我们的方法而已。

扫描二维码关注公众号,回复: 526956 查看本文章

如果我们自己写get()Ljava/lang/Object;和get()Ljava/lang/Number;是不允许同时出现的,是没法通过代码检查的,方法的重载是以参数而不是以返回值作为区分标准的。

上面的C类原理也一样,编译器为get方法提供了桥接方法。

热部署解决方案
如果出现B extends A<Number>这种并且重写父类方法的话,编译器可能会为B添加桥接方法,此时出现了方法新增,就不能走热部署方案了。

猜你喜欢

转载自blog.csdn.net/momo_ibeike/article/details/80267272
今日推荐