Bridge Methods¶
A bridge method is a synthetic method generated by the compiler for the Java programming language to bridge two things together, for the purpose of allowing certain syntax and semantics in Java to work as intended while keeping to the requirements of the Java Virtual Machine.
There are two main reasons that the compiler generates a bridge method:
- To allow inner classes to access private members of classes within the same
compilation unit as the inner class.
- This will be written about in the future. This kind of bridge method has disappeared since Java 11 and the introduction of access control nests.
- To bounce calls of a generics type-erased overriden method to the correct
method in the class, preserving the semantics of method overrides and generics.
- In the Minecraft Forge community, this is often referred to as a bouncer.
Bouncers¶
Let us take the following example:
public interface Consumer<T> {
void apply(T object);
}
public class CountConsumer implements Consumer<Integer> {
@Override
public void apply(Integer object) {
// ...
}
}
In the code snippet, the class implements the generic interface Consumer<T> and
specifies the type parameter as Integer. Accordingly, the class implements the
method void apply(Integer) as specified by the interface's contract. This is normal
Java syntax: to implement an interface, a class must either be abstract or implement
all its non-default methods.
Type Erasure¶
However, the problem that arises is of generics type erasure. During compilation, generics
are type-erased to the widest type that satisfies all the restrictions on the generics. For
example, the type parameter T would be erased to the type Object, while the more
constrained type parameter T extends Number would be erased to the type Number.
Generics signatures in the class file
Although the generics types are erased during compilation, the JVM class file format
provides for the Signature attribute, which allows the compiler to save the information
about the generics into the class file. However, the JVM does not require the presence of
this attribute, which means that class files can be modified to remove the information but
leave intact the semantics of the JVM (as done by some obfuscators).
Therefore, the above example could be compiled to the following due to type-erasure (showing the original type in the comments):
This is only a rendering of the source after compilation and type erasure.
As described below, this snippet as-is would have a compilation error.
public interface Consumer/* <T> */ {
void accept(/* T */ Object object);
}
public class CountConsumer implements Consumer/* <T> */ {
/* @Override */ // the Override annotation is source-level
public void accept(Integer object) {
// ...
}
}
This presents a problem: the type erasure of the interface made it so the method is now
void accept(Object), but no method of that name and descriptor is present in the
implementing class. First off, if this were the actual source, it would cause a compilation
error as CountConsumer no longer implements the methods as specified by Consumer's
contract.
Method Calls¶
Second, consider the following snippet which makes use of the above class and interface:
// Normal usage
Consumer<Integer> consumerInt = new CountConsumer();
consumerInt.accept(Integer.valueOf(42));
// Compilable but errorneous usage
// This will cause an unchecked cast warning
Consumer consumerObj = new CountConsumer();
consumerObj.accept(Integer.valueOf(42)); // Still works
consumerObj.accept(new Object()); // No such method in CountConsumer!
The first part shows normal usage of the interface and class. The second part is a bit
trickier: by leaving out the generic on the type, we can store a CountConsumer into what is
effectively a Consumer<Object> (though without the generics specified, which would've
caused a compilation error). This allows the Object to be passed into the second method
call, which does compile (though with a warning as noted, but will cause a
ClassCastException on runtime because an Object cannot be cast to an Integer.
If the second snippet above were truly representative of the compiled code as used in the
third snippet, then the third method call would cause a NoSuchErrorMethod as the concrete
class in the variable does not have a void accept(Object) method.
Adding the Bouncer¶
To fix this, the compiler inserts a bouncer method into the class, resulting in the following code:
This is only a rendering of the source after compilation and type erasure, with the bouncer method included.
public interface Consumer/* <T> */ {
void accept(/* T */ Object object);
}
public class CountConsumer implements Consumer/* <T> */ {
/* @Override */ // the Override annotation is source-level
public void accept(Integer object) {
// ...
}
// Compiler-generated
public /* synthetic bridge */ void accept(Object object) {
this.accept((Integer) object);
}
}
The newly-added bouncer method does two things:
- Acts as the method which overrides the
void accept(Object)method fromConsumer, preserving the semantics of method overrides for the JVM in finding the actual method to be called during runtime. - Does the necessary casting when called through the type erased method from Consumer (such
as when stored and called through a
Consumervariable), to ensure the JVM can find a method to call and for aClassCastExceptionto be thrown at runtime if an invalid type is passed.
This solves the problems shown by the first compiled code snippet above, in a way that preserves the semantics of method overriding and method calls on type erased methods for generic typed classes/interfaces.
Bridges affect static analysis tools
Static analysis tools have to account for bridge methods when tracing what methods override a given method, because they introduce a layer of indirection which is compiler-generated and not documented in both specifications for the Java Language and Java Virtual Machine, except for one mention in JLS 16, Example §15.12.4.5-1.