什么是桥接方法?

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

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

在字节码文件中,桥接方法会被标记为ACC_BRIDGEACC_SYNTHETIC,其中ACC_BRIDGE用于表示该方法是由编译器产生的桥接方法,ACC_SYNTHETIC用于表示该方法是由编译器自动生成。

文章持续更新,微信搜索「万猫学社第一时间阅读,关注后回复「电子书」,免费获取12本Java必读技术书籍。

什么时候生成桥接方法?

为了实现哪些Java语言特性会生成桥接方法?最常见的两种情况就是协变返回值类型类型擦除,因为它们导致了父类方法的参数和实际调用的方法参数类型不一致。下面我们通过两个例子更好地理解一下。

文章持续更新,微信搜索「万猫学社第一时间阅读,关注后回复「电子书」,免费获取12本Java必读技术书籍。

协变返回类型

协变返回类型是指子类方法的返回值类型不必严格等同于父类中被重写的方法的返回值类型,而可以是更 "具体" 的类型。

在Java 1.5添加了对协变返回类型的支持,即子类重写父类方法时,返回的类型可以是子类方法返回类型的子类。下面看一个例子:

public class Parent {
Number get() {
return 1;
}
}
public class Child extends Parent {

    @Override
Integer get() {
return 1;
}
}

Child类重写其父类Parent的get方法,Parent的get方法返回类型为Number,而Child类中get方法返回类型为Integer。

将这段代码进行编译,再反编译:

javac Child.java
javap -v -c Child.class

文章持续更新,微信搜索「万猫学社第一时间阅读,关注后回复「电子书」,免费获取12本Java必读技术书籍。

结果如下:

public class Child extends Parent
......省略部分结果......
java.lang.Integer get();
descriptor: ()Ljava/lang/Integer;
flags:
Code:
stack=1, locals=1, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: areturn
LineNumberTable:
line 5: 0 java.lang.Number get();
descriptor: ()Ljava/lang/Number;
flags: ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #3 // Method get:()Ljava/lang/Integer;
4: areturn
LineNumberTable:
line 1: 0

从上面的结果可以看到,有一个方法java.lang.Number get(), 在源码中是没有出现过的,是由编译器自动生成的,该方法被标记为ACC_BRIDGEACC_SYNTHETIC,就是我们前面所说的桥接方法。

这个方法就起了一个桥接的作用,它所做的就是把对自身的调用通过invokevirtual指令再调用方法java.lang.Integer get()

编译器这么做的原因是什么呢?因为在JVM方法中,返回类型也是方法签名的一部分,而桥接方法的签名和其父类的方法签名一致,以此就实现了协变返回值类型。

文章持续更新,微信搜索「万猫学社第一时间阅读,关注后回复「电子书」,免费获取12本Java必读技术书籍。

类型擦除

泛型是Java 1.5才引进的概念,在这之前是没有泛型的概念的,但泛型代码能够很好地和之前版本的代码很好地兼容,这是为什么呢?

这是因为,在编译期间Java编译器会将类型参数替换为其上界(类型参数中extends子句的类型),如果上界没有定义,则默认为Object,这就叫做类型擦除。

当一个子类在继承(或实现)一个父类(或接口)的泛型方法时,在子类中明确指定了泛型类型,那么在编译时编译器会自动生成桥接方法,例如:

public class Parent<T> {

    void set(T t) {
}
}
public class Child extends Parent<String> {

    @Override
void set(String str) {
}
}

文章持续更新,微信搜索「万猫学社第一时间阅读,关注后回复「电子书」,免费获取12本Java必读技术书籍。

Child类在继承其父类Parent的泛型方法时,明确指定了泛型类型为String,将这段代码进行编译,再反编译:

public class Child extends Parent<java.lang.String>
......省略部分结果......
void set(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags:
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 5: 0 void set(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #2 // class java/lang/String
5: invokevirtual #3 // Method set:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 1: 0

从上面的结果可以看到,有一个方法void set(java.lang.Object), 在源码中是没有出现过的,是由编译器自动生成的,该方法被标记为ACC_BRIDGEACC_SYNTHETIC,就是我们前面所说的桥接方法。

这个方法就起了一个桥接的作用,它所做的就是把对自身的调用通过invokevirtual指令再调用方法void set(java.lang.String)

编译器这么做的原因是什么呢?因为Parent类在类型擦除之后,变成这样:

public class Parent<Object> {

    void set(Object t) {
}
}

编译器为了让子类有一个与父类的方法签名一致的方法,就在子类自动生成一个与父类的方法签名一致的桥接方法。

文章持续更新,微信搜索「万猫学社第一时间阅读,关注后回复「电子书」,免费获取12本Java必读技术书籍。

如何查找桥接方法的实际方法

在Spring Framework中已经实现了查找桥接方法的实际方法的功能,就在spring-core模块中的BridgeMethodResolver类中,像这样直接使用就行了:

method = BridgeMethodResolver.findBridgedMethod(method);

findBridgedMethod方法是怎么实现的呢?我们来分析一下源码(spring-core的版本为5.2.8.RELEASE):

public static Method findBridgedMethod(Method bridgeMethod) {
// 如果不是桥连方法,就直接返回原方法。
if (!bridgeMethod.isBridge()) {
return bridgeMethod;
}
// 先从本地缓存读取,缓存中有则直接返回。
Method bridgedMethod = cache.get(bridgeMethod);
if (bridgedMethod == null) {
List<Method> candidateMethods = new ArrayList<>();
// 以方法名称和入参个数相等为筛选条件。
MethodFilter filter = candidateMethod ->
isBridgedCandidateFor(candidateMethod, bridgeMethod);
// 递归该类及其所有父类上的所有方法,符合筛选条件就添加进来。
ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass()
, candidateMethods::add, filter);
if (!candidateMethods.isEmpty()) {
// 如果符合筛选条件的方法个数为1,则直接采用;
// 否则,调用searchCandidates方法再次筛选。
bridgedMethod = candidateMethods.size() == 1 ?
candidateMethods.get(0) :
searchCandidates(candidateMethods, bridgeMethod);
}
// 如果找不到实际方法,则返回原来的桥连方法。
if (bridgedMethod == null) {
// A bridge method was passed in but we couldn't find the bridged method.
// Let's proceed with the passed-in method and hope for the best...
bridgedMethod = bridgeMethod;
}
// 把查找的结果放入内存缓存。
cache.put(bridgeMethod, bridgedMethod);
}
return bridgedMethod;
}

文章持续更新,微信搜索「万猫学社第一时间阅读,关注后回复「电子书」,免费获取12本Java必读技术书籍。

我们再看一下再次筛选的searchCandidates方法是如何实现的:

private static Method searchCandidates(List<Method> candidateMethods, Method bridgeMethod) {
if (candidateMethods.isEmpty()) {
return null;
}
Method previousMethod = null;
boolean sameSig = true;
// 遍历候选方法的列表
for (Method candidateMethod : candidateMethods) {
// 对比桥接方法的泛型类型参数和候选方法是否匹配,如果匹配则直接返回该候选方法。
if (isBridgeMethodFor(bridgeMethod, candidateMethod, bridgeMethod.getDeclaringClass())) {
return candidateMethod;
}
else if (previousMethod != null) {
// 如果不匹配,则判断所有候选方法的参数列表是否相等。
sameSig = sameSig && Arrays.equals(candidateMethod.getGenericParameterTypes()
, previousMethod.getGenericParameterTypes());
}
previousMethod = candidateMethod;
}
// 如果所有候选方法的参数列表全相等,则返回第一个候选方法。
return (sameSig ? candidateMethods.get(0) : null);
}

总结以上源码就是,通过判断方法名、参数的个数以及泛型类型参数来获取桥接方法的实际方法。

微信公众号:万猫学社

微信扫描二维码

关注后回复「电子书」

获取12本Java必读技术书籍

3分钟快速搞懂Java的桥接方法的更多相关文章

  1. 一文彻底搞懂Java中的环境变量

    一文搞懂Java环境变量 记得刚接触Java,第一件事就是配环境变量,作为一个初学者,只知道环境变量怎样配,在加上各种IDE使我们能方便的开发,而忽略了其本质的东西,只知其然不知其所以然,随着不断的深 ...

  2. 【Java】15分钟快速体验阿里Java诊断工具Arthas

    [墙裂推荐]15分钟快速体验阿里Java诊断工具Arthas : https://alibaba.github.io/arthas/arthas-tutorials?language=cn&i ...

  3. [转帖]五分钟彻底搞懂你一直没明白的Linux内存管理

    五分钟彻底搞懂你一直没明白的Linux内存管理 https://cloud.tencent.com/developer/article/1462476 现在的服务器大部分都是运行在Linux上面的,所 ...

  4. 一文教你快速搞懂速度曲线规划之S形曲线(超详细+图文+推导+附件代码)

    本文介绍了运动控制终的S曲线,通过matlab和C语言实现并进行仿真:本文篇幅较长,请自备茶水: 请帮忙点个赞

  5. 一文搞懂Java引用拷贝、浅拷贝、深拷贝

    微信搜一搜 「bigsai」 专注于Java和数据结构与算法的铁铁 文章收录在github/bigsai-algorithm 在开发.刷题.面试中,我们可能会遇到将一个对象的属性赋值到另一个对象的情况 ...

  6. 一文搞懂Java引用拷贝、深拷贝、浅拷贝

    刷题.面试中,我们可能会遇到将一个对象的属性赋值到另一个对象的情况,这种情况就叫做拷贝.拷贝与Java内存结构息息相关,搞懂Java深浅拷贝是很必要的! 在对象的拷贝中,很多初学者可能搞不清到底是拷贝 ...

  7. 轻松搞懂Java中的自旋锁

    前言 在之前的文章<一文彻底搞懂面试中常问的各种“锁”>中介绍了Java中的各种“锁”,可能对于不是很了解这些概念的同学来说会觉得有点绕,所以我决定拆分出来,逐步详细的介绍一下这些锁的来龙 ...

  8. [转]快速搞懂Gson的用法

    原文地址:http://coladesign.cn/fast-understand-the-usage-of-gson/ 谷歌gson这个Java类库可以把Java对象转换成JSON,也可以把JSON ...

  9. JavaScript彻底搞懂apply和call方法

    彻底搞懂JavaScript中的apply和call方法 call和apply都是为了改变某个函数运行的context上下文而存在的,即为了改变函数体内部this的指向.因为JavaScript的函数 ...

随机推荐

  1. Android开发-AlertDialog,Progress,ProgressDialog,自定义layout

    AlertDialog 默认样式 单选样式 多选样式 自定义样式 效果图   AlertDialog效果图 class OnClick implements View.OnClickListener ...

  2. Pinpoint 编译环境搭建(Pinpoint系列一)

    本文基于 Pinpoint 2.1.0 版本 目录 一.2.1.0 版本特性 二.编译环境准备 三.编译注意事项 四.编译目录 五.注意事项 新版本的内容参考官方文档, Pinpoint的整个搭建是历 ...

  3. kali 更新msf

    用leafpad打开,方便复制粘贴 leafpad /etc/apt/sources.list 然后复制下面的源覆盖原本的 deb http://mirrors.ustc.edu.cn/kali ka ...

  4. [web安全原理]PHP命令执行漏洞基础

    前言 PHP命令执行漏洞 应用程序的某些功能功能需要调用可以执行系统命令的函数,如果这些函数或者函数的参数被用户控制,就有可能通过命令连接符将恶意命令拼接到正常的函数中,从而随意执行系统命令,这就是命 ...

  5. 加密PDF文件,提高文件安全性

    PDF文件的一大优点是可以设置文件的安全性,不仅可以通过证书加密的形式加密文件,还可以通过pdfFactory来设置密码的形式加密文件. 我们可以通过两种方式开启"PDF加密"来为 ...

  6. 「LOJ 539」「LibreOJ NOIP Round #1」旅游路线

    description 题面较长,这里给出题目链接 solution 考虑预处理出\(f[i][j]\)表示在第\(i\)个点加满油后,从第\(i\)个点出发,至多消耗\(j\)元钱走过的最大路程,那 ...

  7. Math对象-JavaScript

    Math 是一个内置对象,它拥有一些数学常数属性和数学函数方法.Math 不是一个函数对象. Math 用于 Number 类型. Math 的所有属性与方法都是静态的. 属性 Math.E 欧拉常数 ...

  8. python3基础3

    # 匿名函数: bbb = lambda a, b: a + b print(bbb(1,1)) # 函数 def add(a=None, b=None): """ 接收 ...

  9. 多元Huffman编码变形—回溯法

    一.问题描述 描述 在一个操场的四周摆放着n堆石子.现要将石子有次序地合并成一堆.规定在合并过程中最多可以有m(k)次选k堆石子合并成新的一堆,2≤k≤n,合并的费用为新的一堆的石子数.试设计一个算法 ...

  10. CodeBlocks相关配置

    因为我平时CodeBlocks的使用频率不高,但考试时需要用到,担心忘记相关配置在哪里调整,在此记录下. 打开调试模式 首先一定是创建项目. 项目创建完成后,配置调试器\(GDB\)路径 打开调试窗口 ...