点击上方“JavaEdge”,关注公众号

设为“星标”,好文章不错过!

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他

1 当反射遇见方法重载

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_02

重载grade方法,入参分别为int、Integer。
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_03
若不通过反射这种高级编程方式,选用哪个重载方法自然很清晰,比如传666走int参数重载方法,传入Integer.valueOf(“666”)走Integer重载。

但你若墨守成规认为反射调用方法也是根据入参类型确定方法重载,那就掉坑了。
使用getDeclaredMethod获取 grade方法,然后传入Integer.valueOf(“36”)
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_04
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_05

因为通过反射进行方法调用首先是

通过方法签名来确定方法

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_06

本例的getDeclaredMethod传入的参数类型Integer.TYPE其实一直代表int。
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_07

所以实际执行方法时传包装类型、基本类型,最终都是调用int入参的grade方法。

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_08

修正方案

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_09

Integer.TYPE改为Integer.class,实际执行的参数类型就是Integer了。且无论传包装类型/基本类型,最终都会调用Integer为入参的grade方法。

所以反射调用方法,是以反射获取方法时传入的方法名和参数类型来确定调用的方法。

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他

2 当泛型因类型擦除遇见桥接方法

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_02

泛型作为一种编程范式,使得开发者可以使用类型参数替代精确类型,实例化时再指明具体类型。也利于代码重用,将一套代码应用到多种数据类型

泛型的类型检测,可以在编译时暴露大多数泛型编码错误。但由于历史兼容性而妥协的泛型类型擦除,在运行时才会暴露很多坑。

案例

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_06

期望在类字段内容变动时记录日志,于是开发同学就想到定义一个泛型父类,并在父类中定义一个统一的日志记录方法,子类可继承该方法。上线后总出现日志重复记录问题。

父类
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_13

子类Child1 未提供父类泛型参数且定义了一个参数为String而非TsetValue。期望覆盖父类的setValue实现。
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_14

子类方法的调用是通过反射。
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_15

虽Parent的value字段正确设置JavaEdge,但父类setValue调用了两次,计数器而显示2
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_16
两次Parent的setValue方法调用,是因为getMethods找到了两个setValue的,分属于父类/子类。

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_08

子类重写父类方法失败原因

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_09

  • 子类未指定String泛型参数,父类的泛型方法setValue(T value)泛型擦除后是setValue(Object value),于是子类入参String的setValue被当作了新方法

  • 子类的setValue方法未加@Override注解,编译器未能检测到重写失败。

重写子类方法时,务必使用@Override注解。

但有人认为问题是反射API使用不当而未意识到重写失败。查文档后才发现

  • getMethods能获得当前类和父类的所有public方法

  • getDeclaredMethods仅获得当前类所有的public、protected、package和private方法

于是用getDeclaredMethods替换getMethods
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_19

这虽能解决重复记录日志,但未解决子类重写父类方法失败,日志:
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_20

当其他人使用Child1时还是会发现有俩setValue,让人困惑。

重新实现Child2,继承Parent时String作为泛型T类型,并使用@Override注解setValue,实现有效的方法重写
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_21

还是出现重复日志
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_22

Child2的setValue调了两次。难道是JDK的反射出Bug了!
通过getDeclaredMethods查找到的方法肯定来自Child2本身;而且Child2类中看起来也只有一个setValue,怎么可能还重复?

调试发现,Child2类其实有俩setValue:入参分别是String/Object。
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_23拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_24

这就是泛型类型擦除导致。

解密反射下的泛型擦除天坑

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_06

Java泛型类型在编译后被擦除为Object。子类虽指定父类泛型T类型是String,但编译后T会被擦除成为Object,所以父类setValue入参是Object,value也是Object。
若Child2 setValue想覆盖父类,那入参也须为Object。所以,编译器会为我们生成一个桥接方法

Child2类的class字节码:

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_26

若编译器未帮我们实现该桥接方法,那Child2重写的是父类泛型类型擦除后、入参是Object的setValue。这俩方法参数,一个String一个Object,明显不符合Java语义:

class Parent {

    AtomicInteger updateCount = new AtomicInteger();
    private Object value;
    public void setValue(Object value) {
        System.out.println("调用 Parent 的 setValue");
        this.value = value;
        updateCount.incrementAndGet();
    }
}

class Child2 extends Parent {
    @Override
    public void setValue(String value) {
        System.out.println("调用 Child2 的 setValue");
        super.setValue(value);
    }
}

验证:使用jclasslib打开Child2,可看到入参为Object的桥接方法上标记public synthetic bridge。synthetic代表由编译器生成的不可见代码,bridge代表这是泛型类型擦除后生成的桥接代码
拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_27

修正方案

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_06

使用method的isBridge方法,来判断方法是不是桥接方法:

  • 通过getDeclaredMethods方法获取到所有方法后,必须同时根据方法名setValue和非isBridge两个条件过滤,才能实现唯一过滤

  • 使用Stream时,如果希望只匹配0或1项的话,可以考虑配合ifPresent来使用findFirst方法。

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_29

使用反射查询类方法清单时:

  • getMethods和getDeclaredMethods是有区别的,前者可以查询到父类方法,后者只能查询到当前类

  • 反射进行方法调用要注意过滤桥接方法。

往期推荐

大厂如何解决数值精度/舍入/溢出问题

大厂数据库事务实践-事务生效就能保证正确回滚?

线上问题事迹(一)数据库事务居然都没生效?

硬核干货:HTTP超时、重复请求必见坑点及解决方案

给大忙人们看的Java NIO教程之Channel

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_30

目前交流群已有 800+人,旨在促进技术交流,可关注公众号添加笔者微信邀请进群

拼夕夕三轮面经:被问到反射和泛型的bug,你踏空了吗?_其他_31

喜欢文章,点个“在看、点赞、分享”素质三连支持一下~