Erasure and Bridge Methods in Java
How Java compilers generate bridge methods to preserve polymorphic behavior after type erasure with practical examples.
How Java compilers generate bridge methods to preserve polymorphic behavior after type erasure with practical examples.
Erasure and Bridge Methods in Java
Bridge methods are compiler-generated methods that preserve polymorphic behavior when generics interact with inheritance after type erasure. When a generic method in a supertype is overridden in a subtype, erasure can cause the method signatures to collide — the compiler resolves this by inserting bridge methods that redirect calls from the erased signature to the concrete typed implementation.
Introduction
Type erasure is the mechanism Java uses to implement generics at compile time — generic type information is removed, and type parameters become their erasure (typically Object or their bound type). This creates a fundamental mismatch between source-level method signatures and runtime method signatures. When a subtype overrides a generic method with a more specific return type, the compiler must generate a synthetic bridge method to maintain the override relationship at runtime.
Bridge methods are invisible in source code but visible in bytecode and reflection. They are the reason why a method declared as String get() in a subclass actually appears as two methods in bytecode — the real String get() and a synthetic Object get() bridge. Understanding bridge methods is essential for anyone working with generic hierarchies, as they explain the behavior of reflection, serialization, and debugging stack traces involving generic types.
This guide explains why bridge methods exist, how to detect them in bytecode, the subtle bugs that arise from accidentally overriding a bridge instead of the real method, and the security and observability implications of synthetic method generation.
The Core Problem
Consider a generic supertype and a concrete subtype:
public class Node<T> {
public T data;
public void set(T data) { this.data = data; }
public T get() { return data; }
}
At erasure, Node<T> becomes:
public class Node {
public Object data;
public void set(Object data) { this.data = data; }
public Object get() { return data; }
}
Now if you extend this with a concrete type:
public class StringNode extends Node<String> {
@Override
public String get() { return "Hello"; }
}
Erasure of StringNode.get() is String get() and erasure of Node.get() is Object get(). The signatures do not match — this would not normally be a valid override. Java solves this with bridge methods.
What Is a Bridge Method
A bridge method is a synthetic method the compiler generates when:
-
A subtype method has a more specific return type than the erased supertype method
-
The method signatures differ only in generic vs raw type
public class StringNode extends Node<String> {
// The user-defined method
public String get() { return "Hello"; }
// Compiler-generated bridge method (synthetic)
public Object get() {
return this.get(); // delegates to the real String get()
}
}
The bridge Object get() delegates to the user-defined String get().
Code Example: Seeing Bridge Methods
public abstract class Pair<K, V> {
K key;
V value;
public abstract K getKey();
public abstract V getValue();
}
public class StringIntPair extends Pair<String, Integer> {
@Override
public String getKey() { return "count"; }
@Override
public Integer getValue() { return 42; }
}
After erasure, Pair methods are Object getKey() / Object getValue(). The concrete methods have return types String and Integer. The compiler generates bridge methods:
// Synthetic bridges generated in StringIntPair
public Object getKey() { return this.getKey(); } // bridge → String getKey()
public Object getValue() { return this.getValue(); } // bridge → Integer getValue()
Use javap -c to observe them:
javap -c StringIntPair
# You will see:
# public java.lang.Object getKey();
# invokedynamic #...
# public java.lang.String getKey();
# aload_0; aload_0; ...
# public java.lang.Object getValue();
# invokedynamic #...
# public java.lang.Integer getValue();
Mermaid Diagram: Bridge Method Delegation
classDiagram
class Pair {
<<abstract>>
+getKey()
+getValue()
}
class PairErased {
+getKey()
+getValue()
}
class StringIntPair {
+getKey()
+getValue()
}
PairErased <|-- StringIntPair
StringIntPair : bridge delegates to typed method
Code Example: Covariant Return Types with Bridge Methods
public class Numeric {
public Number value() { return 0; }
}
public class IntegerBox extends Numeric {
@Override
public Integer value() { return Integer.valueOf(42); }
// Bridge generated:
// public Number value() { return this.value(); }
}
This also works when the bridge method’s return type is a subtype of the erased return type — a feature called covariant return types.
Failure Scenarios
1. Bridge Methods Causing Infinite Recursion (Accidental)
The infinite recursion bug from accidentally overriding a bridge method is one of the more insidious erasure-related bugs because it is invisible in source code. The method signature Object get() looks perfectly legal — Object is a valid return type, get is a valid method name. There is no compiler warning telling you that you have chosen the wrong signature. The mistake only becomes apparent at runtime when the program hangs or throws a StackOverflowError.
public class BadNode extends Node<String> {
@Override
public Object get() { // accidentally overrides the bridge, not the real method
return super.get(); // infinite recursion! super.get() calls the bridge
}
}
If you accidentally declare the bridge signature (Object get()) instead of the intended String get(), your method calls super.get() which calls the bridge, which calls your method — infinite recursion.
The root cause is that after erasure, Node.get() becomes Object get(). The compiler generates a bridge Object get() in StringNode that delegates to String get(). But if you write public Object get() in StringNode, you are not overriding Node.get() — you are overriding the bridge method. Your public Object get() becomes the bridge itself, replacing the compiler-generated one. When your Object get() calls super.get(), it calls the erased Node.get() which is Object get() on the supertype. That resolves to your own Object get() (since you overrode the bridge), creating a self-call loop: this.get() → super.get() (which resolves to this.get()) → infinite recursion.
The key diagnostic is understanding that bridge methods are not normal overrides — they are synthetic delegation points. When you provide a method with the bridge’s erased signature, you are replacing the delegation mechanism, not adding a second override. The fix is to use the correct signature: public String get() overrides the actual generic method, and the compiler-generated bridge delegates to it. If you are unsure which signature to use, the rule is: the method that calls super.get() should return the generic type (String), not the erased type (Object). Using @Override on the method helps catch this mistake — the compiler will reject public Object get() with @Override because it does not actually override anything in the supertype after erasure.
2. Collision After Erasure
public class Base<T> {
public void set(T data) { } // erased: set(Object)
}
public class Sub extends Base<String> {
public void set(String data) { } // override with more specific signature
// Bridge: set(Object) → set(String) — fine, no conflict
}
BUT if you also had:
public class Base<T> {
public void set(String s) { } // set(String) — different from set(Object)
}
// After erasure, Sub's set(String) and set(Object) both exist — compile error
// "method set(String) clashes with set(Object) after erasure"
3. Bridge Method Visibility Issues
Bridge methods inherit the visibility of the original method they bridge to. If the original method is public, the generated bridge is also public. A public method in a superclass produces a public bridge in the subclass, even if the subclass method that triggered the bridge generation is package-private. The bridge’s visibility comes from the supertype declaration, not the subtype implementation.
This gets tricky across package boundaries. Take packagea.Node<T> with a public T compute() method. packageb.StringNode extends Node<String> overrides compute() with String compute(). The compiler generates a public bridge Object compute() in StringNode that delegates to String compute(). Callers in other packages who obtain a StringNode reference via the supertype Node<?> can resolve the bridge and call it successfully.
The problem: if the supertype method is public but the subtype’s bridge cannot be accessed from the caller’s package, the caller gets a NoSuchMethodError at runtime even though the method “exists” in the type hierarchy. The code compiles fine, the bridge is generated, but the link step fails when a class loader tries to resolve the method through a class it cannot see. That disconnect between compile-time success and runtime failure is what makes this so easy to miss.
Another wrinkle: if a subclass is package-private and its overriding method has more restrictive visibility than the supertype method, the bridge still inherits the supertype’s visibility. A package-private class can expose a public bridge if the superclass method is public. Callers in other packages can invoke the bridge even though they cannot instantiate the package-private subclass. Usually this is harmless. In security-sensitive contexts where you rely on package boundaries to enforce visibility, it can be surprising.
The fix is straightforward: keep generic override methods in subclasses at least as visible as the supertype method, and test generic inheritance hierarchies across package boundaries to catch these link errors before they reach production.
public class Outer<T> {
T compute() { return null; }
}
Trade-Off Table
| Scenario | Without Bridge Methods | With Bridge Methods |
|---|---|---|
| Override generic method with specific return | Would not compile | Compiles, polymorphism works |
| Binary compatibility | Breaks | Preserved |
| Bytecode size | Smaller | Slightly larger (synthetic methods) |
| Reflection | Sees bridge methods | Sees synthetic bridge methods |
| Debugging | Simpler | More complex — synthetic methods in stack traces |
Observability Checklist
- Use
javap -c -vto inspect synthetic bridge methods in compiled bytecode - In stack traces, bridge methods can appear — know how to filter them
- Static analysis tools sometimes flag bridge methods as redundant — suppress only if certain the behavior is correct
- Ensure test coverage exercises generic override paths to catch incorrect bridge delegation
- For framework authors, test that subclassing with generic overrides works across package boundaries
Security Notes
- Synthetic methods are hidden by default: Bridge methods are marked
ACCSyntheticin the class file. Security managers may treat synthetic code differently — verify your security policy handles synthetic bytecode. - Bridge method substitution: If a malicious subclass overrides a bridge method differently than the original generic method, the caller’s expectations (expecting
Object) may be violated. Do not rely on bridge method delegation for security-sensitive logic. - No access control on bridges: Bridge methods inherit the visibility of the original method. A package-private bridge called from another package may not be accessible, potentially breaking expected polymorphic behavior.
Pitfalls
-
Confusing bridge for real method: When debugging with reflection, you may see both the generic-bridge
Object get()and the realString get()— always checkisSynthetic()to identify bridges. -
equals() / hashCode() / toString(): These methods are not subject to erasure bridge generation — they are already declared in
Objectwith the exact signatures used at runtime. -
Generic interface implementation: The same bridge mechanism applies when a generic interface is implemented. Implementing
Comparator<String>withint compare(String a, String b)generates a bridgeint compare(Object a, Object b). -
Clash detection: If two methods would have the same erasure signature, the compiler emits “name clash” errors — this catches cases where your overloaded methods become ambiguous after erasure.
Quick Recap
- Bridge methods are synthetic methods generated by the compiler to maintain override compatibility after erasure
- They appear when a generic method is overridden with a more specific return type or signature
- The bridge delegates to the real method:
Object get() { return this.get(); } - Use
javap -cto see them in bytecode;javap -vshows theACCSyntheticflag - Bridge methods are invisible to normal source code but visible in reflection (
Method.isSynthetic()) - They are a direct consequence of type erasure and are required for the generics + inheritance pattern to work
Interview Questions
Further Reading
- Type Erasure — how generics are erased at compile time
- Generic Classes — defining classes with type parameters
- Generic Methods — methods with type parameters
- Wildcards —
? extends Tand? super T - Type Bounds — upper and lower bounds on type parameters
- Oracle: Bridge Methods — official documentation on compiler-generated bridge methods
- OpenJDK: Bridge Method Generation — reflection API for observing bridge methods at runtime
Conclusion
Bridge methods exist because of a fundamental mismatch between the source-level override relationship and the runtime signature of erased methods. At runtime, Node.get() is Object get() — not String get(). If StringNode only declared String get(), it would not actually override Node.get() after erasure. The bridge bridges this gap by providing the erased signature that delegates to the typed implementation.
The key thing to understand is that bridge methods are synthetic — they are not written by hand, they are generated by the compiler and marked with the ACC Synthetic flag in the class file. You will not see them in source code, but they appear in bytecode and in reflection via Method.isSynthetic(). Stack traces can include them, which sometimes makes debugging confusing.
The dangerous pitfall is accidentally overriding the bridge instead of the real method. If you declare public Object get() in StringNode instead of public String get(), you override the bridge. Calling super.get() from within that method calls the bridge, which calls this.get() — infinite recursion. The signature difference between Object get() and String get() is invisible in source but critical at runtime.
Because bridge methods are generated for generic override scenarios, they only appear when you combine generics with inheritance and override methods with more specific return types. Plain generic classes without inheritance do not generate bridges.
For the full picture of what generics look like at runtime, see Type Erasure in Java Generics — bridge methods are the most visible symptom of the erasure process.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.