当前位置: 首页 > 后端技术 > Java

3分钟快速了解Java的桥接方法

时间:2023-04-01 15:28:11 Java

什么是桥接方法?Java中的BridgeMethod是编译器为了实现某些Java语言特性而自动生成的方法。我们可以通过Method类的isBridge方法来判断一个方法是否是桥接方法。在字节码文件中,桥接方法会被标记为ACC_BRIDGE和ACC_SYNTHETIC,其中ACC_BRIDGE表示该方法是编译器生成的桥接方法,ACC_SYNTHETIC表示该方法是编译器自动生成的。什么时候生成桥接方法?为实现哪些Java语言特性生成了桥接方法?最常见的两种情况是协变返回类型和类型擦除,因为它们导致超类方法的参数类型与实际调用的方法参数类型不一致。让我们通过两个例子更好地理解。协变返回类型协变返回类型是指子类方法的返回值类型不必严格等同于父类中重写方法的返回值类型,可以是更“具体”的类型。在Java1.5中,增加了对协变返回类型的支持,即子类重写父类方法时,返回类型可以是子类方法返回类型的子类。让我们看一个例子:publicclassParent{Numberget(){return1;}}publicclassChildextendsParent{@OverrideIntegerget(){return1;}}子类重写了父类Parent的get方法,Parent的get方法的返回类型是Number,而Child类中get方法的返回类型是Integer。编译反编译这段代码:javacChild.javajavap-v-cChild.class结果如下:publicclassChildextendsParent...省略部分结果...java.lang.Integerget();描述符:()Ljava/lang/Integer;标志:代码:stack=1,locals=1,args_size=10:iconst_11:invokestatic#2//方法java/lang/Integer.valueOf:(I)Ljava/lang/Integer;4:返回LineNumberTable:第5行:0java.lang.Numberget();描述符:()Ljava/lang/Number;标志:ACC_BRIDGE,ACC_SYNTHETIC代码:stack=1,locals=1,args_size=10:aload_01:invokevirtual#3//Methodget:()Ljava/lang/Integer;4:areturnLineNumberTable:line1:0从上面的结果可以看出,有一个方法java.lang.Numberget(),在源码中没有出现,是编译器自动生成的。这个方法被标记为ACC_BRIDGE和ACC_SYNTHETIC,也就是我们前面提到的桥接方法。这种方法起到了桥梁的作用。它所做的就是通过invokevirtual指令调用自身,然后调用方法java.lang.Integerget()。编译器这样做的原因是什么?因为在JVM方法中,返回类型也是方法签名的一部分,而桥接方法的签名与其父类的方法签名一致,从而实现了协变返回值类型。类型擦除泛型是Java1.5中引入的概念。在此之前,没有泛型的概念,但是泛型代码与以前版本的代码有很好的兼容性。为什么?这是因为Java编译器在编译时会将类型参数替换为其上界(类型参数中extends子句的类型)。如果没有定义上界,则默认为Object,称为类型擦除。当子类继承(或实现)父类(或接口)的泛型方法时,在子类中显式指定泛型类型,则编译器会在编译时自动生成一个桥接方法,例如:publicclassParent{voidset(Tt){}}publicclassChildextendsParent{@Overridevoidset(Stringstr){}}Child类继承父类Parent的泛型方法时,必须明确指定泛型类型为String,编译这段代码,然后反编译:publicclassChildextendsParent...省略部分结果...voidset(java.lang.String);描述符:(Ljava/lang/String;)V标志:代码:stack=0,locals=2,args_size=20:returnLineNumberTable:line5:0voidset(java.lang.Object);描述符:(Ljava/lang/Object;)Vflags:ACC_BRIDGE,ACC_SYNTHETICCode:stack=2,locals=2,args_size=20:aload_01:aload_12:checkcast#2//classjava/lang/String5:invokevirtual#3//Methodset:(Ljava/lang/String;)V8:returnLineNumberTable:line1:0从上面的结果可以看出,有一个方法voidset(java.lang.Object),在源码中没有出现,是由编译器自动生成的,这个方法被标记为ACC_BRIDGE和ACC_SYNTHETIC,也就是我们前面提到的桥接方法。这个方法作为一个桥梁,它所做的是通过invokevirtual指令将调用传递给自己,然后调用方法voidset(java.lang.String)。编译器这样做的原因是什么?因为父类在类型擦除之后变成了这样:publicclassParent{voidset(Objectt){}}为了让编译器让子类有一个和父类的方法签名一致的方法,它在子类中自动生成一个与父类的方法签名相匹配的桥接方法。如何获取桥接方法的实际方法获取桥接方法的实际方法的功能在SpringFramework中已经实现,就在spring-core模块中的BridgeMethodResolver类中,直接使用即可:method=BridgeMethodResolver.findBridgedMethod(方法);findBridgedMethod方法是如何实现的?下面分析一下源码(spring-core版本是5.2.8.RELEASE):publicstaticMethodfindBridgedMethod(MethodbridgeMethod){//如果不是桥接方法,直接返回原方法。如果(!bridgeMethod.isBridge()){返回bridgeMethod;}//先从本地缓存中读取,如果缓存中有则直接返回。方法bridgedMethod=cache.get(bridgeMethod);如果(bridgedMethod==null){ListcandidateMethods=newArrayList<>();//以方法名和入参个数作为过滤条件。MethodFilterfilter=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){//传入了桥接方法,但找不到桥接方法。//让我们继续传入的方法并希望获得最好的结果...bridgedMethod=bridgeMethod;}//将查找的结果放入内存缓存中。cache.put(bridgeMethod,bridgedMethod);}returnbridgedMethod;}我们来看看重筛的searchCandidates方法是如何实现的:}方法previousMethod=null;布尔值sameSig=true;//遍历候选方法列表for(MethodcandidateMethod:candidateMethods){//比较桥接方法的泛型类型参数是否匹配候选方法,匹配则直接返回候选方法。如果(isBridgeMethodFor(bridgeMethod,candidateMethod,bridgeMethod.getDeclaringClass())){返回candidateMethod;}elseif(previousMethod!=null){//如果不匹配,则判断所有候选方法的参数列表是否相等。sameSig=sameSig&&Arrays.equals(candidateMethod.getGenericParameterTypes(),previousMethod.getGenericParameterTypes());}previousMethod=candidateMethod;}//如果所有候选方法的参数列表都相等,则返回第一个候选方法。return(sameSig?candidateMethods.get(0):null);}总结以上源码就是通过判断方法名、参数个数和泛型类型参数获取桥接方法的实际方法。