当前位置: 首页 > 科技观察

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

时间:2023-03-12 23:01:30 科技观察

在最近的日常工作中,由于业务需要,笔者对Java字节码层面的知识进行了研究。具体需要根据类字节码获取具体方法名的方法入参,而源码中只有一个方法名。但是在实际使用中发现,当一个类实现了泛型接口时,在字节码层面,该类有两个同名的方法,导致无法判断哪个方法是我们需要的方法。经过研究发现,其中一个方法是编译器在编译时自动生成的桥接方法,可以通过特定的标识符来区分这两个方法。注意:这里的桥接方式与设计模式中的桥接方式不是一个概念。问题描述为了说明问题,作者将实际业务场景的具体案例模糊化,用一个稍微简单一点的能说明问题的例子来分析编译器自动生成的bridge方法。我们知道Java泛型是JDK5引入的新特性,被广泛使用。比如我们有一个运算符泛型接口Operator,接口中有一个process(Tt)方法,用于对输入参数T进行逻辑处理。示例代码如下:/***@authorrenzhiqiang*@date2022/2/2018:30*/publicinterfaceOperator{/***process方法*@paramt*/voidprocess(Tt);}在实际业务场景中,我们会有不同的Operator来实现Operator接口进行业务逻辑处理。那我们就创建一个具体的operator,实现Operator接口,重写process(Tt)方法。如下:/***用户信息运算符*@authorrenzhiqiang*@date2022/2/2018:30*/publicclassUserInfoOperatorimplementsOperator{@Overridepublicvoidprocess(Strings){//dosomething其中,泛型接口中的入参类型T被实现类中实际需要的类型java.lang.String代替。此时,我们已经准备好代码示例。那么,我们的目标是什么?就是获取UserInfoOperator#process(Strings)方法的参数类型java.lang.String。读到这里,读者可能会想:这不是很简单吗?通过反射,根据Class#getDeclaredMethods(),得到UserInfoOperator的所有方法,找到方法名为process的方法,然后得到参数列表。能否获取参数类型java.lang.String?如果正在阅读本文的您也这么认为,那么请继续阅读。根据Java反射方法Class#getDeclaredMethods()的描述:返回一个Method对象数组,包括public、protected、默认(包)访问、私有方法,但不包括继承方法。翻译过来就是:返回方法对象数组,包括公共方法、接受方法、受保护方法、默认(包)访问方法、私有方法,但不包括继承方法。根据我们的例子,如果我们通过反射使用Class#getDeclaredMethods()方法,在我们期望的返回方法数组中,应该只有一个名称为process的方法,但是这里有两个process方法。不意外,不意外!图debug发现UserInfoOperator类的两个process方法是因为编译器生成了bridge方法。我们知道,Java源代码需要经过编译器编译生成相应的.class文件,才能被JVM使用。在源码中,我们只定义了一个名为process的方法。然后我们考虑编译器在编译源代码的过程中是否会进行一些特殊的处理。为了更直观的查看编译后的字节码文件,在Idea中安装jclasslib插件,通过jclasslib查看UserInfoOperator和Operator的字节码。如下:图jclasslib查看UserInfoOperator类的字节码(第一种处理方法)图jclasslib查看UserInfoOperator类的字节码(第二种处理方法)图jclasslib查看Operator类的字节码通过jclasslib和.class文件查看发现,UserInfoOperator类中确实有两个流程方法:一个方法入参是java.lang.String,另一个方法入参是java.lang.Object。在Operator字节码中,只有一个process方法,方法的入参是java.lang.Object。同时,我们注意到在UserInfoOperator类的字节码中,[AccessFlag]项,方法的访问标志之一是[publicsyntheticbridge]。其中public很好理解,但是【合成桥】是从哪里来的呢?查阅相关资料后发现,标识符synthetic表示该方法是否由编译器自动生成;标识符bridge表示该方法是否为编译器生成的Bridging方法。图方法访问标志(来源:深入理解Java虚拟机(第三版))至此,可以确定其中一个过程方法是编译器自动生成的桥接方法。那么为什么编译器会生成桥接方法呢?而在什么情况下,会产生桥接方法呢?而如何判断一个方法是否是桥接方法呢?我们继续分析。为什么生成的bridge方法在源码中编译正确,Operator类的process方法的参数定义为process(Tt),参数类型为T。在字节码层面,我们可以看到process之后方法被编译时,编译器将输入参数类型更改为java.lang.Object。伪代码提示,大概是这样的:publicinterfaceOperator{/***方法参数变成Object类型*@paramobject*/voidprocess(Objectobject);}想象一下如果没有编译器自动生成的bridge方法,则无法在编译级别传递:因为接口Operator中的process方法,编译后参数类型变为java.lang.Object类型,而实现类UserInfoOperator中process方法的参数为java.lang.Object类型。lang.String类型,两者的方法参数不一致,导致UserInfoOperator没有重写接口中的process方法,所以编译失败。在这种情况下,编译器自动生成了一个桥接方法voidprocess(Objectobj)方法,可以编译通过,这似乎是理所当然的事情。自动生成的流程方法,方法签名为:voidprocess(Objectobject)。伪代码,大概是这样的://自动生成的流程方法publicvoidprocess(Objectobject){process((String)object);}类型擦除我们知道Java中泛型在编译时会进行泛型信息擦除。比如代码定义了List和List,编译后就会变成List。让我们考虑另一种常见情况:Java类库中比较器的使用。我们在自定义比较器的时候,可以通过实现Comparator接口来实现比较逻辑。示例代码如下:publicclassMyComparatorimplementsComparator{publicintcompare(Integera,Integerb){//比较逻辑}}这种情况下,编译器也会生成桥接方法。方法签名是intcompare(Objecta,Objectb)。MyComparator类的两个比较方法的伪代码如图所示,大致是这样的:publicclassMyComparatorimplementsComparator{publicintcompare(Integera,Integerb){//比较逻辑}//桥接方法(桥接方法)publicintcompare(Objecta,Objectb){returncompare((Integer)a,(Integer)b);因此,当我们使用下面的方法进行比较时,就可以编译得到我们预期的结果:Objecta=5;Objectb=6;ComparatorrawComp=newMyComparator();//可以编译通过,因为bridge方法compare(Objecta,Objectb)intcomp=rawComp.compare(a,b)自动生成;另外,我们知道泛型编译之后,类型信息会被抹掉。如果我们有这样一个比较方法://comparisonmethodpublicTmax(Listlist,Comparatorcomparator){TbiggestSoFar=list.get(0);for(Tt:list){if(comparator.compare(t,biggestSoFar)>0){biggestSoFar=t;}}returnbiggestSoFar;}编译后,泛型被擦除,伪代码表示如下:publicObjectmax(Listlist,Comparatorcomparator){ObjectbiggestSoFar=list.get(0);for(Objectt:list){if(comparator.compare(t,biggestSoFar)>0){//比较逻辑biggestSoFar=t;}}returnbiggestSoFar;}我们将MyComparator的参数之一传递给max()方法。如果没有桥接方法,第四行的比较逻辑将无法正确编译,因为MyComparator类中没有两个参数类型为Object类型的比较方法,只有参数类型为Integer类型的比较方法。读者可自行测试。解决方案通过上面的案例描述,我们知道在实现泛型接口的场景下,编译器会自动生成一个桥接方法来保证编译能够通过。那么在这种情况下,我们只需要识别出哪些是桥接方法,哪些不是桥接方法,就可以解决我们最初的问题了。自然地,既然编译器自动生成了一个桥接方法,那么我们应该有一些方法来判断一个方法是否是一个桥接方法。果然,我们继续研究,发现Method类中提供了Method#isBridge()方法。查看源码中对该方法的描述:Method#isBridge():如果该方法是桥接方法则返回true;否则返回假。至此,我们通过反射获取到UserInfoOperator类中的两个process方法,然后调用Method#isBridge()方法锁定需要的方法,从而进一步获取到方法参数java.lang.String。经过深入的分析,可以说在业务需求上,我们找到了一个完美的解决方案。但事后不禁想:除了上面的例子,还有哪些情况下编译器会自动生成桥接方法呢?让我们继续深入挖掘。类继承通过查阅相关资料,我们考虑以下情况:/***会不会产生下面的桥接方法?*@author任志强*@date2022/2/2018:33*/publicclassBridgeMethodSample{staticclassA{publicvoidfoo(){}}publicstaticclassCextendsA{}publicstaticclassDextendsA{@Overridepublicvoidfoo(){}}}在上面的代码示例中,我们定义了三个静态内部类:ACD,其中CD分别继承自A。通过jclasslib编译查看BridgeMethodSample字节码后,我们还发现编译器在C类中为它生成了一个桥接方法voidfoo(),而在D类中没有。GraphC类生成了一个桥接方法。图类D不生成桥接方法。深入分析,并且根据上面分析的经验,我们猜测编译器在生成桥接方法的时候,一定是需要某个方法在某种情况下满足Java编程规范,或者说需要保证正确性的程序运行。从字节码可以看出A类没有public修饰,包范围外的程序无法访问A类,更不用说A类中的方法了。但是C类有public修饰,C类中的方法,包括继承的方法,可以被包外的程序访问。因此,编译器需要生成一个桥接方法来保证foo()方法可以被访问到,以满足程序的正确运行。但是类D也继承了A,只是没有生成桥接方法。根本原因是D类重写了父类A中的foo()方法,即不需要生成桥接方法。方法重写我们再来看另一种情况,方法重写。在Java中,方法覆盖(Override)是子类重写父类允许访问的方法的实现过程。重写需要满足一定的规则:1、方法必须和父类中的方法同名。2.方法必须与父类中的参数相同。3、必须存在IS-A关系(继承)。JDK5以后,重写的方法的返回类型可以和父类方法的返回类型相同,也可以不同,但??必须是父类方法返回类型的子类。让我们考虑以下代码示例://定义一个父类,包括一个test()方法publicclassFather{publicObjecttest(Strings){returns;}}//定义一个子类,继承父类publicclassChildextendsFather{@OverridepublicStringtest(Strings){returns;}}上面,在Child子类中,我们重写了test()方法,但是返回值的类型,我们把java.lang.Object改成了java.lang.String的子类。编译完成后,我们同样使用jclasslib插件查看两个类的字节码,如下图:图子类字节码test()方法(1)子类字节码test()方法(2)图父类字节码测试()方法根据上图,我们发现我们重写了Child类中的test()方法,但是在字节码层面,我们发现有两个test()方法,其中一个的访问标志为[publicsyntheticbridge],说明这个方法是编译器为我们生成的。而当我们不改变Child#test()方法的返回类型时,编译器不会为我们生成桥接方法,读者可以自行试验。也就是说,当子类方法重写父类方法,返回类型不一致时,编译器也会为我们生成一个桥接方法。上面,作者列举了编译器自动为我们生成桥接方法的几种情况。那么是否还有其他场景编译器也会生成桥接方法呢?如果你也研究过或使用过桥接方法,欢迎交流讨论。同时给出桥接方法的非官方定义,希望能给读者一些启发:桥接方法:这些是在源函数和目标函数之间创建中间层的方法。它通常用作类型擦除过程的一部分。这意味着桥接方法需要作为类型安全的接口。由于作者水平有限,难免会有不准确、不充分的理解。欢迎交流讨论!参考https://stackoverflow.com/questions/5007357/java-generics-bridge-methodhttps://stackoverflow.com/questions/14144888/find-generic-method-with-actual-types-from-getdeclaredmethodshttps://www.geeksforgeeks.org/method-class-isbridge-method-in-java/

猜你喜欢