Introduction:

In my recent daily work, the author studied the knowledge of Java bytecode level due to business needs. In particular, you need to get a method entry parameter for a particular method name, which is only one in the source code, based on the class bytecode. In practice, however, we find that in the case of classes implementing generic interfaces, at the bytecode level, the class has two methods with the same name, making it impossible to determine which method is the one we need. After research, it is found that one of the methods is the bridge method automatically generated by the compiler in the process of compilation. The two methods can be distinguished by a specific identifier.

Note: The bridging method here is not the same as the bridging pattern in the design pattern.

Problem description

To illustrate the problem, I obscure the actual business scenario and use a slightly simpler illustrative example to analyze the bridge method automatically generated by the compiler.

As we know, Java generics are a new feature introduced in JDK 5 and widely used. For example, we have an Operator generic interface that has a process(T T) method that performs logical processing on the input parameter T. Example code is as follows:

/ * *

* @author renzhiqiang

* @date 2022/2/20 18:30

* /

public interface Operator {

/ * *

* process method

* @param t

* /

void process(T t);

}

In actual service scenarios, there are different operators to implement the Operator interface for service logic processing. So let’s create a concrete Operator and implement the Operator interface, overriding the process(T T) method. As follows:

/ * *

* User information operator

* @author renzhiqiang

* @date 2022/2/20 18:30

* /

public class UserInfoOperator implements Operator {

@Override

public void process(String s) {

// do something

}

}

Where, the input parameter type T in the generic interface is replaced in the implementation class with the actual required type java.lang.string. At this point, we have code samples ready.

So what is our goal? UserInfoOperator#process(String s) : java.lang.String At this point, the reader may be thinking: Get all of the UserInfoOperator methods from Class#getDeclaredMethods(), find the method named process, and get the argument list. You can retrieve the parameter type java.lang.String.

If that’s what you think as you read, keep reading.

According to Java reflection method Class#getDeclaredMethods

(a)

Description:

Returns an array of Method objectsincluding public, protected, default (package) access, and private methods, butexcludes inherited methods.

Returns an array of method objects, including public methods, protected methods, default (package) access methods, and private methods, but not inherited methods.

According to our example, if we use the Class#getDeclaredMethods() method via reflection, we would expect only one method name to be process in the returned method array, but there are two process methods. No surprise, no surprise!

Figure Debug finds two process methods of the UserInfoOperator class

The reasons causing

The compiler generates the Bridge method

As we know, Java source code needs to be compiled by the compiler to generate the corresponding.class file for use by the JVM. In the source code, we define only one method called process. So let’s consider whether the compiler does some special processing when compiling the source code. To view the compiled bytecode file directly, install the Jclasslib plug-in in Idea and view the bytecode of UserInfoOperator and Operator through jclasslib. As follows:

Figure jclasslib viewing the bytecode of the UserInfoOperator class (the first process method)

Figure jclasslib viewing the bytecode of the UserInfoOperator class (second process method)

Figure jClasslib looks at the bytecode of the Operator class

A look at the.class file in jclasslib shows that there are indeed two process methods in the UserInfoOperator class: One of the methods has an input parameter of java.lang.string and the other has an input parameter of java.lang.object. In Operator bytecode, there is only one process method whose input parameter is java.lang.object. We also notice that in the bytecode of the UserInfoOperator class, the [Access token] entry, where the access token of one of the methods is [Public Synthetic Bridge]. Synthetic bridge is the synthetic bridge of synthetic bridge.

After consulting relevant materials, it is found that the identifier synthetic, indicating whether this method is automatically generated by the compiler; Identifier bridge, indicating whether this method is a bridge method generated by the compiler.

Graph method access flags (Source: Understanding the Java Virtual Machine in Depth (3rd edition))

At this point, it is clear that one of the process methods is a bridge method that is automatically generated by the compiler. So why does the compiler create bridge methods? And under what circumstances do bridge methods occur? And how to tell if a method is a bridge method? So let’s move on.

Why are bridge methods generated

Compile and

In the source code, the Operator class process method is defined as process(T T), and the parameter type is T. At the bytecode level, we see that after the process method is compiled, the compiler changes the input parameter type to java.lang.object. The pseudocode schematic looks something like this:

public interface Operator {

/ * *

* The method argument becomes Object

* @param object

* /

void process(Object object);

}

Imagine that it would be impossible to pass at the compile level without the bridge method automatically generated by the compiler: Because the process method in the interface Operator, after being compiled, changes the parameter type to java.lang.Object. The process method of the implementation class UserInfoOperator takes the type java.lang.String as the parameter of the two methods. As a result, UserInfoOperator does not overwrite the process method of the interface, so the compilation fails.

In this case, it seems natural that the compiler automatically generates a bridge method called void Process (Object obj) that will compile. An automatically generated process method with a signature of void Process (Object Object). The pseudocode schematic looks something like this:

// Automatically generated process methods

public void process(Object object) {

process((String) object);

}

Type erasure

We know that generics in Java erase generics information at compile time. If the code defines a List and a List, it will become a List when compiled. Let’s consider another common case: the use of comparators in Java class libraries. When we customize a Comparator, we can implement the comparison logic by implementing the Comparator interface. Example code is as follows:

public class MyComparator implements Comparator {

public int compare(Integer a,Integer b) {

// Make sure you have the right answer

}

}

In this case, the compiler also generates a bridge method. Intcompare (Object A, Object b).

Figure two compare methods of the MyComparator class

The pseudocode schematic looks something like this:

public class MyComparator implements Comparator {

public int compare(Integer a,Integer b) {

// Make sure you have the right answer

}

// Bridge method

public int compare(Object a,Object b) {

return compare((Integer)a,(Integer)b);

}

}

Therefore, we can compile and get the result we expect when we compare using the following method:

Object a = 5;

Object b = 6;

Comparator rawComp = new MyComparator();

Compare (Object a, Object b);

int comp = rawComp.compare(a, b);

In addition, we know that after generics are compiled, the type information is erased. If we have a comparison like this:

// Compare methods

public T max(List list, Comparator comparator){

T biggestSoFar = list.get(0);

for ( T t : list ) {

if (comparator.compare(t,biggestSoFar) > 0) {

biggestSoFar = t;

}

}

return biggestSoFar;

}

After compilation, the generics are erased and the pseudocode says something like this:

public Object max(List list, Comparator comparator) {

Object biggestSoFar =list.get(0);

for ( Object t : list ) {

If (comparator.compare(t,biggestSoFar) > 0) {//

biggestSoFar = t;

}

}

return biggestSoFar;

}

We pass one of the MyComparator arguments to the Max () method. Without the bridge method, the comparison logic in line 4 would not compile correctly because the MyComparator class has no two comparison methods of type Object and only comparison methods of type Integer. Readers can test for themselves.

The solution

From the case description above, we know that in the case of implementing a generic interface, the compiler will automatically generate a bridge method to ensure that the compilation will pass. So in this case, we can solve our original problem by simply identifying which is the bridge method and which is not the bridge method. Naturally, since the compiler automatically generates a bridge method, there should be some way to tell if a method is a bridge method.

Sure enough, we continue to discover that the Method class provides the Method#isBridge() Method. Method#isBridge() : Returns true if this method is a bridge method; Returns false otherwise.

At this point, we get the two process methods in the UserInfoOperator class by reflection, and then call Method#isBridge() to lock the required method, thus further obtaining the method parameter java.lang.string.

In-depth analysis

At this point, in terms of business requirements, we have found a perfect solution. But after that, you can’t help but wonder: In addition to the examples above, in what other cases does the compiler automatically generate bridge methods? We went further.

Class inheritance

Through consulting relevant information, we consider the following situations:

/ * *

* Does the following result in a bridge method?

* @author renzhiqiang

* @date 2022/2/20 18:33

* /

public class BridgeMethodSample {

static class A {

public void foo() {

}

}

public static class C extends A{

}

public static class D extends A{

@Override

public void foo() {

}

}

}

In the code example above, we define three static inner classes: A, C and D, where C and D inherit from A. When we compile the BridgeMethodSample bytecode through jclasslib, we also see that the compiler generates the bridge method void foo() for class C, but not for class D.

Diagram class C generates bridge methods

Graph class D does not generate bridge methods

Further analysis, and based on the experience of the above analysis, led us to guess that the compiler must generate a bridge method at some point in order to satisfy the Java programming specification or to ensure that the program runs correctly. As you can see from the bytecode, class A has no public modifier, and programs outside the package scope do not have access to class A, let alone the methods in class A.

But class C has the public modifier, and methods in class C, including inherited methods, can be accessed by programs outside the package. Therefore, the compiler needs to generate a bridge method so that foo() can be accessed and the program can run correctly. However, class D also inherits from A, but does not generate A bridge method. The root reason is that the foo() method of the parent class A is overridden in class D, that is, there is no need to generate A bridge method.

Methods to rewrite

Let’s do one more case, method rewrite.

In Java, method Override is the process by which a subclass rewrites the implementation of an accessible method from its parent class. Rewriting needs to satisfy certain rules:

1. The method must have the same name as in the parentclass.

2. The method must have the same parameter as in theparent class.

3. There must be an IS-A relationship (inheritance).

After JDK 5, overriding methods can have the same or different return types as the parent method, but they must be subclasses of the parent method return type. Let’s consider the following code example:

// Define a parent class that contains a test() method

public class Father {

public Object test(String s) {

return s;

}

}

// Define a subclass that inherits from the parent class

public class Child extends Father {

@Override

public String test(String s) {

return s;

}

}

Above, in the Child subclass, we override the test() method, but change the type of the return value from java.lang.Object to its subclass java.lang.string. After compiling, we also use the Jclasslib plug-in to look at the bytecodes for both classes, as shown below:

Child bytecode test() (1)

Child bytecode test()

Figure Father class bytecode test() method

According to the above figure, we find that the test() method is overridden in the Child class, but at the bytecode level, there are two test() methods, one of which is marked as [public synthetic Bridge], indicating that the method is generated for us by the compiler. While we don’t change the return type of the Child#test() method, the compiler doesn’t generate the bridge method for us, so the reader can experiment.

That is, in cases where a subclass method overrides a parent method and returns an inconsistent type, the compiler also generates a bridge method for us.

Above, I’ve listed several cases where the compiler automatically generates bridge methods for us. Are there any other scenarios in which the compiler generates bridge methods? If you have studied or used the Bridge approach, you are welcome to discuss it.

At the same time, an unofficial definition of bridge method is given, hoping to give readers some inspiration:

Bridge Method: These are methods that create an intermediate layerbetween the source and the target functions. It is usually used as part of thetype erasure process. It means that the bridge method is required as a typesafe interface.

Limited to the author’s limited level, it is hard to avoid inaccurate understanding, not in place. Welcome to exchange and discuss!

reference

Stackoverflow.com/questions/5…

Stackoverflow.com/questions/1…

www.geeksforgeeks.org/method-clas…