继承关系在编译期做了什么?桥接方法泛型擦除

Posted 爱叨叨的程序狗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了继承关系在编译期做了什么?桥接方法泛型擦除相关的知识,希望对你有一定的参考价值。

什么是桥接方法?

Java桥接方法(Bridge Method)是一种为了实现某些Java语言特性而由编译器自动生成的方法。

可以通过Method类的isBridge方法来判断一个方法是否为桥接方法。

在字节码文件中,桥接方法会被标记为ACC_BRIDGEACC_SYNTHETIC

ACC_BRIDGE用于表示该方法是由编译器产生的桥接方法,ACC_SYNTHETIC用于表示该方法是由编译器自动生成。

什么时候生成桥接方法?

最常见的两种情况是:

  • 协变返回值类型
  • 类型擦除

这两种情况会导致父类方法的参数和实际调用的方法参数类型不一致。

概念补充

协变返回类型:子类方法的返回值类型不必阉割等同于父类中被重写的方法的返回值类型,而是以更”具体“

的类型。

类型擦除:在JDK 1.5之后引入泛型的概念,泛型能够和之前版本代码很好的兼容,就是因为在编译期间Java编译器会将类型参数替换为其上界(类型参数中extends字句类型),如果上界没有定义,则默认为Object。

talk is cheaper,show me your code.

父类:

public class Parent<T> {
    //用于记录value更新的次数,模拟日志记录的逻辑
    AtomicInteger updateCount = new AtomicInteger();
    private T value;

    //重写toString,输出值和值更新次数

    @Override
    public String toString() {
        return String.format("value: %s updateCount: %d", value, updateCount.get());
    }

    //设置值,调用一次该方法,原子性value+1
    public void setValue(T value) {
        this.value = value;
        updateCount.incrementAndGet();
    }
}

子类:

public class Child2 extends Parent<String> {
    @Override
    public void setValue(String value) {
        System.out.println("Child2.setValue called");
        super.setValue(value);
    }
}

测试并运行:

public class TestChild {
    public static void main(String[] args) {
        Child2 child1 = new Child2();
        //获取Child所有的方法,通过反射调用Child的setValue,"test"是入参
        Arrays.stream(child1.getClass().getMethods())
                .filter(method -> method.getName().equals("setValue")).forEach(method -> {
                    try {
                        method.invoke(child1, "test");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
        System.out.println(child1.toString());
    }
}

很显然,setValue被执行了两次。

实践是检验真理的唯一标准:

在反射反代的位置打上断点,可以看到有两个setValue不同入参类型的方法,那么通过method.getName()获取方法名字叫setValue理所应当会获取到两个,所以在通过反射调用时,便会执行两次,所以更新值是2。

使用jclasslib工具或javap -c命令查看反编译后的代码:

发现有两个setValue方法,一个是本身的Chlid本身的setValue,另一个是编译期生成的桥接方法,可以桥接方法生成的方法入参是Object类型,这便是在编译期间寻找了String类型的上界,上界没有定义默认为Object。

正确使用,避免泛型擦除的坑

方案一:在通过反射获取父类方法时,过滤掉编译器生成的Bridge方法即可。

public class TestChild {
    public static void main(String[] args) {
        Child2 child1 = new Child2();
        //获取Child所有的方法,通过反射调用Child的setValue,"test"是入参
        Arrays.stream(child1.getClass().getMethods())
                .filter(method -> method.getName().equals("setValue") && !method.isBridge()).findFirst().ifPresent(method -> {
                    try {
                        method.invoke(child1, "test");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
        System.out.println(child1.toString());
    }
}

方案二:使用Spring中BridgeMethodResolver的findBridgedMethod方法

    public static void main(String[] args) throws NoSuchMethodException {
        Child2 child1 = new Child2();
        //获取Child所有的方法,通过反射调用Child的setValue,"test"是入参
        Method setValue = BridgeMethodResolver.findBridgedMethod(child1.getClass().getMethod("setValue", String.class));
        try {
            setValue.invoke(child1,"test");
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(child1.toString());
    }
}

本文参考:

以上是关于继承关系在编译期做了什么?桥接方法泛型擦除的主要内容,如果未能解决你的问题,请参考以下文章

Java的桥接方法和BridgeMethodResolver使用

Java的桥接方法和BridgeMethodResolver使用

Java的桥接方法和BridgeMethodResolver使用

27.Android架构-泛型擦除机制

Java泛型擦除

泛型深入--java泛型的继承和实现泛型擦除