前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >继承关系在编译期做了什么?桥接方法、泛型擦除

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

作者头像
关忆北.
发布2021-12-07 16:53:41
3900
发布2021-12-07 16:53:41
举报
文章被收录于专栏:关忆北.关忆北.关忆北.
什么是桥接方法?

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());
    }
}
image-20210730135112424
image-20210730135112424

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

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

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

image-20210730141140326
image-20210730141140326

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

image-20210730135317102
image-20210730135317102
image-20210730135349552
image-20210730135349552

发现有两个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方法

image-20210730144744432
image-20210730144744432
    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());
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-07-30 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是桥接方法?
  • 什么时候生成桥接方法?
  • 概念补充
  • talk is cheaper,show me your code.
  • 实践是检验真理的唯一标准:
  • 正确使用,避免泛型擦除的坑
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档