3 minutes to quickly understand the bridge method example of Java

  • 2021-08-21 20:19:39
  • OfStack

What is the bridging method?

The bridging method in Java (Bridge Method) is a method automatically generated by the compiler in order to realize some features of Java language.

We can judge whether a method is a bridge method by isBridge method of Method class.

In the ByteCode file, the bridging method is marked as ACC_BRIDGE and ACC_SYNTHETIC, where ACC_BRIDGE is used to indicate that the method is a bridging method generated by the compiler and ACC_SYNTHETIC is used to indicate that the method is automatically generated by the compiler.

When is the bridging method generated?

Which Java language features generate bridging methods for implementation? The two most common cases are covariant return value types and type erasure, because they cause the parameters of the parent class method to be of different types from the parameters of the actually called method. Let's better understand 1 through two examples.

Covariant return type

Covariant return type means that the return value type of a subclass method does not have to be strictly equivalent to the return value type of an overridden method in the parent class, but can be a more "concrete" type.

Support for covariant return types was added in Java 1.5, that is, when a subclass overrides a parent class method, the returned type can be a subclass of the return type of the subclass method. Let's look at an example:


public class Parent {
  Number get() {
    return 1;
  }
}

public class Child extends Parent {

  @Override
  Integer get() {
    return 1;
  }
}

The Child class overrides the get method of its parent class Parent, the get method of Parent returns Number, and the get method of the Child class returns Integer.

Compile this code and decompile it:


javac Child.java
javap -v -c Child.class

The results are as follows:

public class Child extends Parent
...... omit some results......
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

From the above results, we can see that there is a method java. lang. Number get (), which did not appear in the source code and was automatically generated by the compiler. This method is marked as ACC_BRIDGE and ACC_SYNTHETIC, which is the bridging method we mentioned earlier.

This method acts as a bridge, and all it does is call itself through the invokevirtual instruction and then call the method java. lang. Integer get ().

** What is the reason for the compiler to do this? ** Because in the JVM method, the return type is also part 1 of the method signature, and the signature of the bridging method and the method signature of its parent class are 1, covariant return value types are implemented.

Type erase

Generics was introduced in Java 1.5. There was no concept of generics before that, but generic code is compatible with previous versions of code. Why?

This is because during compilation, the Java compiler replaces the type parameter with its upper bound (the type of the extends clause in the type parameter). If the upper bound is not defined, it defaults to Object, which is called type erasure.

When a subclass inherits (or implements) a generic method of a parent class (or interface) and a generic type is explicitly specified in the subclass, the compiler automatically generates bridging methods at compile time, such as:


public class Parent<T> {

  void set(T t) {
  }
}

public class Child extends Parent<String> {

  @Override
  void set(String str) {
  }
}

When the Child class inherits the generic method of its parent class Parent, it explicitly specifies the generic type as String, compiles this code, and then decompiles it:

public class Child extends Parent < java.lang.String >
...... omit some results......
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

From the above results, we can see that there is a method void set (java. lang. Object), which did not appear in the source code and was automatically generated by the compiler. This method is marked as ACC_BRIDGE and ACC_SYNTHETIC, which is the bridging method we mentioned earlier.

This method acts as a bridge by calling itself through the invokevirtual instruction and then calling the method void set (java. lang. String).

** What is the reason for the compiler to do this? ** Because the Parent class becomes this after type erasure:


public class Parent<Object> {

  void set(Object t) {
  }
}

In order to make a subclass have a method with a method signature of 1 to the parent class, the compiler automatically generates a bridge method with a method signature of 1 to the parent class in the subclass.

How to get the actual method of bridging method

The ability to get the actual method of the bridging method is already implemented in Spring Framework, in the BridgeMethodResolver class in the spring-core module, just use it directly like this:


method = BridgeMethodResolver.findBridgedMethod(method);

How is the findBridgedMethod method implemented? Let's analyze the source code under 1 (spring-core version 5.2. 8. RELEASE):


public static Method findBridgedMethod(Method bridgeMethod) {
  //  If it is not a bridge method, it returns directly to the original method. 
 if (!bridgeMethod.isBridge()) {
 return bridgeMethod;
 }
  //  Read it from the local cache first, and return it directly if it is in the cache. 
 Method bridgedMethod = cache.get(bridgeMethod);
 if (bridgedMethod == null) {
 List<Method> candidateMethods = new ArrayList<>();
    //  The filter condition is that the method name and the number of parameters are equal. 
 MethodFilter filter = candidateMethod ->
  isBridgedCandidateFor(candidateMethod, bridgeMethod);
    //  Recursively all methods on this class and all its parent classes, and add them if they meet the filter criteria. 
 ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass()
      , candidateMethods::add, filter);
 if (!candidateMethods.isEmpty()) {
      //  If the number of methods that match the filter is 1 , then directly adopted; 
      //  Otherwise, call the searchCandidates Method is filtered again. 
  bridgedMethod = candidateMethods.size() == 1 ?
   candidateMethods.get(0) :
   searchCandidates(candidateMethods, bridgeMethod);
 }
    //  If the actual method cannot be found, the original bridging method is returned. 
 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;
 }
    //  Put the results of the search into the memory cache. 
 cache.put(bridgeMethod, bridgedMethod);
 }
 return bridgedMethod;
}

Let's look at how the searchCandidates method filtered again under 1 is implemented:


private static Method searchCandidates(List<Method> candidateMethods, Method bridgeMethod) {
 if (candidateMethods.isEmpty()) {
 return null;
 }
 Method previousMethod = null;
 boolean sameSig = true;
  //  Traverse the list of candidate methods 
 for (Method candidateMethod : candidateMethods) {
    //  Compare the generic type parameters of the bridging method with the candidate method, and if so, return the candidate method directly. 
 if (isBridgeMethodFor(bridgeMethod, candidateMethod, bridgeMethod.getDeclaringClass())) {
  return candidateMethod;
 }
 else if (previousMethod != null) {
      //  If there is no match, it is judged whether the parameter lists of all candidate methods are equal. 
  sameSig = sameSig && Arrays.equals(candidateMethod.getGenericParameterTypes()
        , previousMethod.getGenericParameterTypes());
 }
 previousMethod = candidateMethod;
 }
  //  If the parameter lists of all candidate methods are equal, the 1 Candidate methods. 
 return (sameSig ? candidateMethods.get(0) : null);
}

To sum up the above source code is to get the actual method of bridging method by judging the method name, the number of parameters and generic type parameters.


Related articles: