作者 | 小志

来源 | 程序员小灰

导语

笔者在最近的日常工作中,因业务需要,研究 Java 字节码层面的知识。具体是,需要根据类字节码,获取特定方法名的方法入参,此方法名在源码中只有一个。但是在实际使用中发现:在类实现泛型接口的情况下,在字节码层面,类却有两个同名方法,导致无法确定哪个方法才是我们需要的方法。经过研究发现,其中一个方法是编译器在编译的过程中,自动生成的桥接方法(bridge method),两个方法可通过特定标识区分。

注:此处的桥接方法,跟设计模式中的桥接模式,不是一个概念。

问题描述

为了能够说明问题,笔者模糊了实际业务场景的具体案例,用一个稍微简单,能够说明问题的示例,来分析编译器自动生成的桥接方法(bridge method)。

我们知道,Java 泛型是JDK 5 中引入的一个新特性,应用广泛。比如,我们有一个操作算子泛型接口 Operator<T>,接口中有一个 process(T t) 方法,其作用是对入参 T 进行逻辑处理。示例代码如下:

/*** @author renzhiqiang* @date 2022/2/20 18:30*/public interface Operator<T> {/*** process method* @param t*/void process(T t);}

在实际业务场景中,我们会有不同的操作算子,实现Operator<T> 接口,进行业务逻辑处理。那么我们来创建一个具体的算子,并实现Operator<T> 接口,重写 process(T t) 方法。如下:

/*** 用户信息算子* @author renzhiqiang* @date 2022/2/20 18:30*/public class UserInfoOperator implements Operator<String> {@Overridepublic void process(String s) {// do something}}

其中,泛型接口中的入参类型 T,在实现类中替换成了实际需要的类型 java.lang.String。到这里,我们就准备好了代码样例。

那么,我们的目标是什么呢?就是要获取UserInfoOperator#process(String s) 方法的参数类型java.lang.String。读到这里,读者可能会想:这不很简单么,通过反射,根据Class#getDeclaredMethods(),获取到 UserInfoOperator 的所有方法,再找到方法名是 process 的方法,然后再获取到参数列表,不就可以获取参数类型java.lang.String 了么。

如果正在阅读文章的你也这么想的话,那请继续往下看。

根据 Java 反射方法Class#getDeclaredMethods() 的描述:

Returns an array of Method objectsincluding public, protected, default (package) access, and private methods, butexcludes inherited methods.

翻译过来就是:返回方法对象数组,包括公共方法、受保护方法、默认(包)访问方法和私有方法,但不包括继承方法。

根据我们的示例,如果我们通过反射,利用Class#getDeclaredMethods() 方法,我们预期的返回方法数组中,应该只有一个方法名是process 才对,但是这里却有两个 process 方法。惊不惊奇,意不意外!

图 debug 发现 UserInfoOperator 类的两个 process 方法

产生原因

编译器生成 bridge 方法

我们知道,Java 源码需要经过编译器编译,生成对应的 .class 文件,才能给 JVM 使用。在源码中,我们只定义了一个名为 process 的方法。那么我们考虑,编译器在编译源码的过程中,是否会进行一些特的处理。为了更加直观的查看编译后的字节码文件,在 Idea 安装 jclasslib 插件,通过 jclasslib 查看 UserInfoOperator 和 Operator<T> 的字节码。如下:

图 jclasslib 查看 UserInfoOperator 类的字节码(第一个 process 方法)

图 jclasslib 查看 UserInfoOperator 类的字节码 (第二个 process 方法)

图 jclasslib 查看 Operator<T> 类的字节码

通过 jclasslib 查看 .class 文件发现,在 UserInfoOperator 类中确实存在两个 process 方法:其中一个方法入参是 java.lang.String,另一个方法的入参是 java.lang.Object。而在 Operator 字节码中,只有一个 process 方法,方法的入参是 java.lang.Object。同时我们注意到,在 UserInfoOperator 类的字节码中, [访问标志]项,其中一个方法的访问标志是 [public synthetic bridge]。其中 public 很好理解,但是其中的 [synthetic bridge] 是怎么来的呢?

查阅相关资料后发现,标识符 synthetic ,表示此方法是否是由编译器自动产生的;标识符 bridge,表示此方法是否是由编译器产生的桥接方法。

图 方法访问标志(来源:深入理解 Java 虚拟机(第三版))

到此,可以确定的是,其中一个process 方法,是编译器自动产生的桥接方法。那么为什么编译器会产生桥接方法呢?以及在什么情况下,会产生桥接方法?以及如何判断一个方法是不是桥接方法?我们继续往下分析。

为何生成 bridge 方法

正确编译

在源码中,Operator 类的 process 方法的参数定义是 process(T t),参数类型是 T。而在字节码层面我们看到,process 方法在编译之后,编译器将入参类型变成了 java.lang.Object。伪代码示意,大概是这样:

public interface Operator<Object> {/*** 方法参数变成 Object 类型* @param object*/void process(Object object);}

想象一下,如果没有编译器自动生成的桥接方法,那么在编译层面是不能通过的:因为接口 Operator<T> 中的 process 方法,,经过编译之后,参数类型变成了 java.lang.Object 类型,而实现类 UserInfoOperator 中的 process 方法的参数是 java.lang.String 类型,两者的方法参数不一致,导致UserInfoOperator 并没有重写接口中的 process 方法,因此编译无法通过。

这种情况下,编译器自动生成一个桥接方法 void process(Object obj) 方法,则可以编译通过,似乎是理所当然的事情。自动生成的 process方法,方法签名为:void process(Object object)。伪代码示意,大概是这样:

// 自动生成的process 方法public void process(Object object) {process((String) object);}

类型擦除

我们知道,Java 中的泛型在编译期间会将泛型信息擦除。如代码定义 List<String> 和 List<Integer>,编译之后都会变成 List。我们再考虑一种常见的情形:Java 类库中比较器的用法。我们自定义比较器的时候,可以通过实现 Comparator 接口,实现比较逻辑。示例代码如下:

public class MyComparator implements Comparator<Integer> {public int compare(Integer a,Integer b) {// 比较逻辑  }}

这种情况下,编译器同样会产生一个桥接方法。

方法签名为 intcompare(Object a, Object b) 。

图 MyComparator 类的两个 compare 方法

伪代码示意,大概是这样:

public class MyComparator implements Comparator<Integer> {public int compare(Integer a,Integer b) {// 比较逻辑}// 桥接方法 (bridge method)public int compare(Object a,Object b) {return compare((Integer)a,(Integer)b);}}

因此,当我们使用如下方式进行比较的时候,能够通过编译并得到我们预期的结果:

Object a = 5;Object b = 6;Comparator rawComp = new MyComparator();// 可以通过编译,因为自动生成了桥接方法compare(Object a, Object b)int comp = rawComp.compare(a, b);

另外,我们知道,泛型编译之后,类型信息会被擦除。如果我们有这样一个比较方法:

// 比较方法public <T> T max(List<T> list, Comparator<T> comparator){T biggestSoFar = list.get(0);for ( T t : list ) {if (comparator.compare(t,biggestSoFar) > 0) {biggestSoFar = t;}}return biggestSoFar;}

编译之后,泛型被擦除掉,伪代码表示,大概是这样:

public Object max(List list, Comparator comparator) {Object biggestSoFar =list.get(0);for ( Object  t : list ) {if (comparator.compare(t,biggestSoFar) > 0) {  //比较逻辑biggestSoFar = t;}}return biggestSoFar;}

我们将 MyComparator 其中一个参数传入 max() 方法。如果没有桥接方法的话,那么第四行的比较逻辑,将无法正确编译,因为MyComparator 类中没有两个参数是 Object 类型的比较方法,只有参数类型是 Integer 类型的比较方法。读者可自行测试。

解决方案

通过以上的案例描述,我们知道,在实现泛型接口的场景下,编译器会自动生成桥接方法,保证编译能够通过。那么在这种情况下,我们只要识别哪一个是桥接方法,哪一个不是桥接方法,就可以解决我们一开始的问题。很自然的,既然编译器自动产生了一个桥接方法,那么应该会有某种方式,可以让我们判断一个方法是否是桥接方法。

果然,我们继续研究发现,Method 类中提供了 Method#isBridge() 方法。查看源码中对方法的描述:Method#isBridge():Returns true if this method is a bridge method;returns false otherwise。

到此,我们通过反射,获取到 UserInfoOperator 类中的两个process 方法,再调用 Method#isBridge() 方法,即可锁定需要的方法,因而进一步获取方法参数 java.lang.String。

深入分析

至此可以说,就业务需求来说,我们完美的找到了解决方案。但在此之后,不禁会想:除了上述示例,还有哪些情况下,编译器也会自动生成桥接方法呢?我们继续深入研究。

类继承

通过查阅相关资料,我们考虑如下一种情况:

/*** 如下会产生桥接方法吗?* @author renzhiqiang* @date 2022/2/20 18:33*/public class BridgeMethodSample {static class A {public void foo() {}}public static class C extends A{}public static class D extends A{@Overridepublic void foo() {}}}

上述代码示例中,我们定义了三个静态内部类:A C D,其中 C D 分别继承 A。经过编译,通过jclasslib 查看 BridgeMethodSample 字节码,我们也发现:类 C 中编译器为其生成了桥接方法 void foo(),而类 D 中却没有。

图 类C 生成桥接方法

图 类D 没有生成桥接方法

深入分析,并根据上述分析的经验,我们猜测,编译器生成桥接方法,一定是在某种情况下需要一个方法,来满足 Java 编程规范,或者需要保证程序运行的正确性。通过字节码可以看出,类 A 没有 public 修饰,包范围以外的程序是没有访问类 A 的权限的,更不用说类 A 中的方法。

但是类 C 是有public 修饰,C 类中的方法,包括继承来的方法,是可以被包外的程序访问的。因此,编译器需要生成一个桥接方法,以保证能够访问 foo() 方法,满足程序的正确运行。但是,类 D 同样继承 A,却没有生成桥接方法,根本原因是类 D 中重写了父类 A 中的 foo() 方法,即没有必要生成桥接方法。

方法重写

我们再看一种情况,方法重写。

Java 中,方法重写(Override),是子类对父类的允许访问的方法的实现过程进行重新编写的过程。重写需要满足一定的规则:

  • 1. The method must have the same name as in the parentclass.

  • 2. The method must have the same parameter as in theparent class.

  • 3. There must be an IS-A relationship (inheritance).

JDK 5 之后,重写方法的返回类型,可以与父类方法返回类型相同,也可以不相同,但必须是父类方法返回类型的子类。我们考虑如下代码示例:

// 定义一个父类,包含一个 test() 方法public class Father {public Object test(String s) {return s;}}// 定义一个子类,继承父类public class Child extends Father {@Overridepublic String test(String s) {return s;}}

以上,在 Child 子类中,我们重写了 test() 方法,但是返回值的类型,我们将 java.lang.Object 改变为它的子类 java.lang.String。编译之后,我们同样使用 jclasslib 插件,查看两个类的字节码,如下所示:

图 Child 类字节码test() 方法(1)

图 Child 类字节码test() 方法(2)

图 Father类字节码test() 方法

根据上图我们发现,Child 类中我们重写了 test() 方法,但是在字节码层面,发现有两个 test() 方法,其中一个方法的访问标志为 [public synthetic bridge], 表示这个方法是编译器为我们生成的。而当我们不改变 Child#test() 方法的返回类型时,编译器并没有为我们生成桥接方法,读者可自行试验。

也就是说,在子类方法重写父类方法,返回类型不一致的情况下,编译器也为我们生成了桥接方法。

以上,笔者罗列了几种编译器为我们自动生成桥接方法的情况。那么是否还有其他场景下,编译器也会生成桥接方法呢?如果您也曾研究过或者使用过 bridge 方法,欢迎交流讨论。

同时,给出一个 bridge 方法的非官方定义,希望能够给读者一些启发:

Bridge Method: These are methods that create an intermediate layerbetween the source and the target functions. It is usually used as part of thetype erasure process. It means that the bridge method is required as a typesafe interface.

限于笔者水平有限,难免有理解不准确、不到位的地方。欢迎交流讨论!

参考链接:

https://stackoverflow.com/questions/5007357/java-generics-bridge-method

https://stackoverflow.com/questions/14144888/find-generic-method-with-actual-types-from-getdeclaredmethods

https://www.geeksforgeeks.org/method-class-isbridge-method-in-java/

往期推荐

为什么大家都在抵制用定时任务实现「关闭超时订单」功能?

如果被问到分布式锁,应该怎样回答?

别再用 Redis List 实现消息队列了,Stream 专为队列而生

OpenStack 如何跨版本升级

点分享

点收藏

点点赞

点在看

Java 底层知识:什么是 “桥接方法” ?相关推荐

  1. Java反射中method.isBridge() 桥接方法

    Java反射中method.isBridge() 桥接方法 桥接方法是 JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法.我们可 ...

  2. java底层知识(6)--CPU、内存

    本文转载自:http://www.cnblogs.com/xkfz007/archive/2012/10/08/2715163.html 尊重原创 CPU的等待有多久? 原文标题:What Your ...

  3. java底层知识(3)--CPU 高速缓存

    本文转载自: http://blog.jobbole.com/36263/ 尊重原创 3.CPU的高速缓存 现在的CPU比25年前要精密得多了.在那个年代,CPU的频率与内存总线的频率基本在同一层面上 ...

  4. 3分钟快速搞懂Java的桥接方法

    什么是桥接方法? Java中的桥接方法(Bridge Method)是一种为了实现某些Java语言特性而由编译器自动生成的方法. 我们可以通过Method类的isBridge方法来判断一个方法是否是桥 ...

  5. 一网打尽:Java 程序员必须了解的计算机底层知识!

    公众号后台回复"面试",获取精品学习资料 扫描下方海报了解专栏详情 本文来自公众号读者cxuan的投稿 我们每个程序员或许都有一个梦,那就是成为大牛,我们或许都沉浸在各种框架中,以 ...

  6. Java系列:关于Java中的桥接方法

    这两天在看<Java核心技术 卷1>的泛型相关章节,其中说到了在泛型子类中override父类的泛型方法时,编译器会自动生成一个桥接方法,这块有点看不明白. 书上的例子代码如下: publ ...

  7. java程序中单方法接口通常是,Java基础知识整理

    面向对象和面向过程的区别 面向过程 优点: 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机.嵌入式开发.Linux/Unix等一般采用面向过程开发,性能是最重要的因素. ...

  8. Java 桥接方法(Bridge Method)

    目录 重写方法的返回类型是其父类返回类型的子类型 重写泛型方法生成桥接 什么是「桥接方法」,下面来从两个例子中体会一下. 重写方法的返回类型是其父类返回类型的子类型 public class Merc ...

  9. [译] Java 桥接方法详解

    原文地址:Java bridge methods explained 原文作者:STAS 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- 译者:kezhenxu9 ...

最新文章

  1. 大数据分布式集群搭建(6)
  2. 如何搭建并使用便携式 4G/LTE 伪基站研究移动安全
  3. 开发板_Hi3516DV300核心板/开发板;Hi3516EV100+4G+AUDIO RTMP开发板;海思系列开发板/核心板定制开发...
  4. Deep Learning(深度学习) 资料库
  5. 看YYModel源码的一些收获
  6. Installation error: INSTALL_FAILED_NO_MATCHING_ABIS
  7. 单位阶跃信号是周期信号吗_直流散热风扇的信号你知道如何控制吗?
  8. socat命令如何监听Linux串口设备通讯报文
  9. jvm-内存结构--分类索引
  10. Nantian PR-2/K10打印机的安装及配置方法
  11. Vue3+Vite快速搭建vue项目
  12. (每日一练c语言)商品优惠计算器
  13. 「Python 网络自动化」Nornir—— Inventory(主机清单)介绍
  14. 云脉相册管理,检索轻松便捷
  15. 计算机C语言好学吗?要是想自学应该怎么办?大学挂科赶紧恶补!
  16. 开放集识别之GPD and GEV Classi ers
  17. 推荐几个超有趣的公众号
  18. 使用 NumPy 来模拟随机游走(Random Walk)
  19. 电信3g在小米信号显示无服务器,关于小米手机电信3G信号问题的分析
  20. Axure9 最新授权码,持续更新中

热门文章

  1. java jui 正则表达式_常规正则表达式练习
  2. mysql 虚拟列索引_使用MySQL 5.7虚拟列提高查询效率
  3. linux中在工作空间编译cpp,linux tensorflow2.4.0 c++ 编译
  4. linux应用参数 冒号,Lua-面向对象中函数使用时冒号(:)和点(.)的区别
  5. linux有哪些实时同步工具,rsync文件同步工具常见模式有哪些?linux系统
  6. 德国院士:“工业4.0”概念升级了,包含人工智能和5G
  7. 致诺奖得主:低报酬、超工时,为什么我们要追求科学事业?
  8. 电影票上的字是怎么消失的?
  9. CGCKD2021大会报告整理(4)--风格迁移
  10. 睡眠音频分割及识别问题(六)--输入输出及方案讨论