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

上一篇 / 下一篇  2022-03-03 14:07:05

  笔者在最近的日常工作中,因业务需要,研究 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 接口,进行业务逻辑处理。那么我们来创建一个具体的算子,并实现Operator 接口,重写 process(T t) 方法。如下:
  /**
   * 用户信息算子
   * @author renzhiqiang
   * @date 2022/2/20 18:30
   */
  public class UserInfoOperator implements Operator<String> {
      @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 方法。惊不惊奇,意不意外!
图 debug 发现 UserInfoOperator 类的两个 process 方法

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


TAG: 软件开发 Java

 

评分:0

我来说两句

Open Toolbar