Java中的桥接方法是什么
创始人
2024-12-26 18:02:35
0次

什么是桥接方法?

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

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

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

什么时候生成桥接方法?

为了实现哪些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

结果如下:

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_BRIDGE和ACC_SYNTHETIC,就是我们前面所说的桥接方法。

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

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

类型擦除

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

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

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

public class Parent {

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

  @Override
  void set(String str) {
  }
}

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

public class Child extends Parent
......省略部分结果......
 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_BRIDGE和ACC_SYNTHETIC,就是我们前面所说的桥接方法。

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

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

public class Parent {

  void set(Object t) {
  }
}

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

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

在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 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;
}

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

private static Method searchCandidates(List 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);
}

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

相关内容

热门资讯

空调连续工作多久需要停机休息 空调连续工作的时间取决于多个因素,包括空调的型号、功率、使用环境和维护情况等。一般来说,空调可以连续...
空调哪个符号是除湿的 空调除湿功能通常用一个水滴形状的符号表示。不同品牌和型号的空调遥控器可能会有所差异,但一般来说,除湿...
格力空调多长时间加一次氟利昂 1、一般情况下,空调对氟利昂的使用量是非常小的,所以在空调使用了四年左右的时候再考虑加也没问题。2、...
空调用多长时间需要加氟一次 一般情况下是五年。当出现以下这几种情况时,空调才需要加氟。1、空调的室内机和室外机各有两个接口,当接...
格调生活·C标思享荟:卡萨帝在... 9月12日的郑州龙湖金融岛,夜风轻拂,灯光璀璨。“C标时刻共传奇——卡萨帝思享荟暨20周年品牌盛典郑...
品牌价值破千亿、高端份额持续领... 9月12日,“C标时刻共传奇——卡萨帝思享荟暨20周年品牌盛典郑州站”在郑州龙湖金融岛举办。2026...
空调主板坏了修800还是换新的 空调主板坏了是修还是换新,需要考虑多个因素,以下是一些建议:1. 维修成本:如果维修费用较低,比如 ...
空调开多久需要休息多长时间 问题:空调开多久需要休息多长时间回答:1、在使用空调的过程中,一般空调开7~8个小时左右需要休息2~...
中央空调多长时间清洗一次 1、一般空调开机前清洗一次即可,空调开机中间时段再清洗一次,空调关机时清洗一次,这样比较合理。2、空...
中央空调氟利昂多久加一次 看空调制热和制冷程度,很多人的观点认为氟利昂必须一年放一次,其实这个观点是错误的,理解上面有点偏差,...