阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Java桥接方法

80次阅读
没有评论

共计 5813 个字符,预计需要花费 15 分钟才能阅读完成。

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

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

问题描述

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

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

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

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

/**
* 用户信息算子
* @author renzhiqiang
* @date 2022/2/20 18:30
*/
public class UserInfoOperator implements Operator {
@Override
public 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 方法。惊不惊奇,意不意外!

Java 桥接方法

产生原因
编译器生成 bridge 方法

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

Java 桥接方法
Java 桥接方法
Java 桥接方法

通过 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 类型
     * @param object
     */
    void process(Object object);
}

想象一下,如果没有编译器自动生成的桥接方法,那么在编译层面是不能通过的:因为接口 Operator 中的 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 和 List,编译之后都会变成 List。我们再考虑一种常见的情形:Java 类库中比较器的用法。我们自定义比较器的时候,可以通过实现 Comparator 接口,实现比较逻辑。示例代码如下:

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

这种情况下,编译器同样会产生一个桥接方法。方法签名为 intcompare(Object a, Object b)。

Java 桥接方法

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

public class MyComparator implements Comparator {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 max(List list, Comparator 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{
        @Override
        public void foo() {}
    }
}

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

Java 桥接方法

Java 桥接方法

深入分析,并根据上述分析的经验,我们猜测,编译器生成桥接方法,一定是在某种情况下需要一个方法,来满足 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 {
    @Override
    public String test(String s) {return s;}
}

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

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

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

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

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-25发表,共计5813字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中