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

e69fca4b8b710882196ebf7672a28fbf.gif

作者 | 小志

来源 | 程序员小灰

导语

笔者在最近的日常工作中,因业务需要,研究 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 方法。惊不惊奇,意不意外!

456e3d30c3bcdb0f4e41bb7b4054045d.png

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

产生原因

编译器生成 bridge 方法

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

776d40363b5ff71963b38de47f64413c.png

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

35d10b5f5601d9ffcf74fbf4ae2405cd.png

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

14c5e15716e96fab8c5e758f162b3298.png

图 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,表示此方法是否是由编译器产生的桥接方法。

3fa1120d32105e26378000b72344fedd.png

图 方法访问标志(来源:深入理解 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) 。

514c7a882e6664c428612d28e0274b12.png

图 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 中却没有。

8e5fa3506222e76d8d5570c74cdc3f0f.png

图 类C 生成桥接方法

ac4f23a42393d6aeb53e0bce3c722afe.png

图 类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 插件,查看两个类的字节码,如下所示:

e4a78978ea6c7630f3f79cd9bab86a9a.png

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

270bbf1c47441ac6ae8a890164696404.png

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

7907b6811739bac2446e78bbfa5dbb96.png

图 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/

139f6af741fe8e2574450e52d109e0c8.gif

24e2e17e630d33ab3b08d0b314f01b5f.png

往期推荐

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

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

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

OpenStack 如何跨版本升级

d0b8ebc8854a45cd9ee16872bb03c3de.gif

点分享

b2c1fb9dc402fd97eb0a6b709aaa883b.gif

点收藏

7794ca42c198a7021d32812da609e5a5.gif

点点赞

f70455e004655267b82de37af32a3ea9.gif

点在看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/512210.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ACMMM2021|在多模态训练中融入“知识+图谱”:方法及电商应用实践

简介&#xff1a; 随着人工智能技术的不断发展&#xff0c;知识图谱作为人工智能领域的知识支柱&#xff0c;以其强大的知识表示和推理能力受到学术界和产业界的广泛关注。近年来&#xff0c;知识图谱在语义搜索、问答、知识管理等领域得到了广泛的应用。 作者 | 朱渝珊 来源 |…

带你体验云原生场景下 Serverless 应用编程模型

简介&#xff1a; 阿里云 Knative 基于 ASK 之上&#xff0c;在完全兼容社区 Knaitve 的同时对 FC、ECI 工作负载进行统一应用编排&#xff0c;支持事件驱动、自动弹性&#xff0c;为您提供统一的 Serverless 应用编程模型。 背景 阿里云 Serverless Kubernetes&#xff08;A…

CSO全球网络安全大会来了,权威奖项征集中

全球网络安全顶级峰会——IDC 2022 全球CSO网络安全大会&#xff08;以下简称大会&#xff09;将于2022年6月首次落地中国。本届大会以“聚力数据安全 赋能企业现代化”为主题&#xff0c;由Foundry&#xff08;IDG&#xff09;/IDC联合上海市信息安全行业协会共同举办&#xf…

stream of java_Java 8 新特性-Stream更优雅的处理集合入门

Java 8 新特性之——Stream一. 简单介绍Stream是Java 8提出了的一种新的对集合对象功能的增强。它集合Lambda表达式&#xff0c;对集合提供了一些非常便利&#xff0c;高效的操作&#xff0c;使得代码具有非常高的可读性&#xff0c;优雅性&#xff01;&#xff01;举个例子来说…

MySQL深潜|剖析Performance Schema内存管理

简介&#xff1a; 本文主要是通过对PFS引擎的内存管理源码的阅读&#xff0c;解读PFS内存分配及释放原理&#xff0c;深入剖析其中存在的一些问题&#xff0c;以及一些改进思路。 一 引言 MySQL Performance schema(PFS)是MySQL提供的强大的性能监控诊断工具&#xff0c;提供…

敲地鼠java_Java实现的打地鼠小游戏完整示例【附源码下载】

本文实例讲述了Java实现的打地鼠小游戏。分享给大家供大家参考&#xff0c;具体如下&#xff1a;这里涉及到java线程和GUI的相关知识&#xff0c;一个简单的java小游戏打地鼠&#xff0c;有兴趣的朋友可以优化一下。先来看看运行效果&#xff1a;具体代码&#xff1a;Mouse.jav…

深入理解 Docker 网络原理

作者 | 渡、来源 | CSDN博客Docker网络原理容器是相对独立的环境&#xff0c;相当于一个小型的Linux系统&#xff0c;外界无法直接访问&#xff0c;那他是怎么做的呢&#xff0c;这里我们先了解下Linux veth pair。1. Linux veth pairveth pair是成对出现的一种虚拟网络设备接口…

全网首发|阿里资深技术专家数仓调优经验分享(上)

简介&#xff1a; 本篇文章总结了AnalyticDB表的设计的最佳经验、数据写入的最佳经验、高效查询的最佳实践&#xff0c;以及一些常见的问题。 随着云原生数据仓库AnalyticDB for MySQL&#xff08;下文统一简称&#xff1a;AnalyticDB&#xff09;在阿里集团各个业务线、社会上…

重大技术突破首次发布!十问解密云栖大会!

简介&#xff1a; 2021年10月19日&#xff0c;杭州&#xff0c;云栖大会正式开幕。大会上&#xff0c;阿里巴巴正式发布自研云芯片倚天710&#xff0c;这是阿里云推进“一云多芯”策略的重要一步&#xff0c;也是阿里第一颗为云而生的CPU芯片&#xff0c;将在阿里云数据中心部署…

Redis 缓存击穿(失效)、缓存穿透、缓存雪崩怎么解决?

作者 | 码哥来源 | 码哥字节原始数据存储在 DB 中&#xff08;如 MySQL、Hbase 等&#xff09;&#xff0c;但 DB 的读写性能低、延迟高。比如 MySQL 在 4 核 8G 上的 TPS 5000&#xff0c;QPS 10000 左右&#xff0c;读写平均耗时 10~100 ms。用 Redis 作为缓存系统正好可以…

在 Kubernetes 集群中使用 MetalLB 作为 Load Balancer(上)

作者 | Addo Zhang来源 | 云原生指北TL&#xff1b;DR网络方面的知识又多又杂&#xff0c;很多又是系统内核的部分。原本自己不是做网络方面的&#xff0c;系统内核知识也薄弱。但恰恰是这些陌生的内容满满的诱惑&#xff0c;加上现在的工作跟网络关联更多了&#xff0c;逮住机…

Java程序员情人节_盘点程序员情人节的表白,前端程序员最浪漫,后端不服来战...

今天是 2 月 14 日情人节&#xff0c;我看公众号后台有好多人在回复关键字&#xff1a;情人节表白代码。我想&#xff0c;我作为大家的校长&#xff0c;必须给大家送一波福利代码啊!我是真没想到&#xff0c;竟然很多程序员都想用代码表白&#xff0c;还整的挺浪漫的&#xff0…

GRPC: 如何实现分布式日志跟踪?

简介&#xff1a; 本文将介绍如何在 gRPC 分布式场景中&#xff0c;实现 API 的日志跟踪。 介绍 本文将介绍如何在 gRPC 分布式场景中&#xff0c;实现 API 的日志追踪。 什么是 API 日志追踪&#xff1f; 一个 API 请求会跨多个微服务&#xff0c;我们希望通过一个唯一的 ID…

一文搞懂物联网Modbus通讯协议

简介&#xff1a; 一般来说&#xff0c;常见的物联网通讯协议众多&#xff0c;如蓝牙、Zigbee、WiFi、ModBus、PROFINET、EtherCAT、蜂窝等。而在众多的物联网通讯协议中&#xff0c;Modbus是当前非常流行的一种通讯协议。它一种串行通信协议&#xff0c;是Modicon公司于1979年…

快看世界技术VP安尝思:从漫画到视频,如何用技术赋能创作

供稿 | 快看世界 2月25日&#xff0c;在火山引擎举办的视频云科技原力峰会上&#xff0c;快看世界技术VP安尝思受邀参加&#xff0c;并发表主题为《如何搭建轻量高效的视频技术体系》的演讲。 安尝思表示&#xff0c;快看视频内容的革命性变化来自于漫剧的推出&#xff0c;在过…

2021云栖大会丨阿里云发布第四代神龙架构,提供业界首个大规模弹性RDMA加速能力

简介&#xff1a; 10月20日&#xff0c;2021年杭州栖大云会上&#xff0c;阿里云发布第四代神龙架构&#xff0c;升级至全新的eRMDA网络架构&#xff0c;是业界首个大规模弹性RDMA加速能力。 10月20日&#xff0c;2021年杭州栖大云会上&#xff0c;阿里云发布第四代神龙架构。…

Rambus推出面向下一代数据中心的PCIe 6.0控制器

新闻摘要&#xff1a; 将先进的人工智能/机器学习、存储和网络应用的性能提升至64 GT/s优化了功耗、面积和延迟&#xff0c;全方位实现PCIe 6.0特性集成IDE引擎提供最先进的数据安全Rambus PCIe 6.0控制器框图 作为业界领先的芯片和IP核供应商&#xff0c;致力于使数据传输更…

阿里云混合云Apsara Stack 2.0发布 加速政企数智创新

简介&#xff1a; 2021年10月21日&#xff0c;杭州 – 今日&#xff0c;阿里云于云栖大会正式发布Apsara Stack 2.0&#xff0c;从面向单一私有云场景&#xff0c;升级为服务大型集团云&行业云场景。新一代Apsara Stack不仅可以为政企定制稳定、安全、开放、智能的数字底座…

ArrayList源码浅析

简介&#xff1a; ArrayList作为我们开发中最常用的集合&#xff0c;作为极高频次使用的类&#xff0c;我们不妨阅读源码一谈究竟。 前言 ArrayList作为我们开发中最常用的集合&#xff0c;作为极高频次使用的类&#xff0c;我们不妨阅读源码一谈究竟。 介绍 ArrayList继承…

阿里云边缘云ENS再升级 四大场景应用加速产业数字化落地

简介&#xff1a; 云栖大会 | 于10月21日上午举办的边缘云应用升级与技术创新论坛中&#xff0c;阿里云边缘云ENS产品全面升级&#xff0c;从边缘云产品、技术、行业应用等维度全面阐述阿里云在边缘计算领域的技术积累、产品&解决方案沉淀、商业实践。 一年一度科技圈盛事…