Override a generic method with a more specific parameter type and something has to give. The subclass method has a different erased signature from the superclass method it is supposed to override, so by the JVM’s rules it does not override anything. The compiler solves this by writing a method you never asked for.
The idea
A bridge method is the compiler paying, in generated code, for a mismatch its own erasure created. The JVM dispatches on erased signatures, and erasure can turn an override into an unrelated overload. Rather than accept broken polymorphism or change the dispatch rules, the compiler emits a synthetic method with the superclass’s erased signature whose entire body is a cast plus a call to the real one. The interesting consequence is that dynamic dispatch in Java is not always a single vtable hop: for a generified hierarchy it is a hop into a generated trampoline that casts and hops again, and that cast is where a corrupted generic type finally fails.
The break
The tutorial’s example is the smallest one that shows the problem:
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}Read as source, MyNode.setData(Integer) overrides Node<Integer>.setData(T). Now apply erasure:
public class Node {
public Object data;
public void setData(Object data) { ... }
}
public class MyNode extends Node {
public void setData(Integer data) { ... }
}The tutorial states the failure exactly: “After type erasure, the method signatures do not match; the Node.setData(T) method becomes Node.setData(Object). As a result, the MyNode.setData(Integer) method does not override the Node.setData(Object) method.”
Two methods named setData with different descriptors are an overload, not an override. Call setData through a Node reference pointing at a MyNode and you would reach Node.setData(Object), which is the wrong method and would write an arbitrary object into a field the subclass believes holds an Integer.
The repair
The compiler generates a third method into MyNode:
class MyNode extends Node {
// Bridge method generated by the compiler
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}Now MyNode does have a method with the superclass’s erased descriptor, so virtual dispatch finds it, and its body casts and delegates: “The bridge method MyNode.setData(Object) delegates to the original MyNode.setData(Integer) method.”
The cast in that generated body is load-bearing. It is the runtime check that the value flowing through the erased path really is an Integer. If something upstream violated the static guarantee, through a raw type or an unchecked cast, the ClassCastException is thrown here, inside a method that does not appear in the source. This is one of the most common ways a program discovers an unchecked warning it ignored hours or days after the fact.
Reading a bridge method in a stack trace
A
ClassCastExceptionwhose stack trace names a method you wrote, at a line that contains no cast, with the expected and found types both being ordinary domain classes, is nearly always a bridge method. The frame is real, the line number points at the method declaration rather than a statement, and the cast being reported is the one the compiler generated. The fix is never at that line. It is at whatever earlier point put the wrong type into a container the type system thought it knew.
Where else bridges appear
Overriding a generic method is the textbook case, but the same mechanism covers two more situations.
Covariant return types. Overriding Object clone() with MyType clone() produces two different descriptors, since the JVM’s notion of a method signature includes the return type. The compiler emits a bridge with the erased return type that calls the narrower one.
Interface implementations across erasure. Implementing Comparable<MyType> gives you compareTo(MyType), while the interface’s erased method is compareTo(Object). Every class that implements a generic interface with a concrete type argument carries a bridge for it, which is why javap on a simple Comparable implementation shows more methods than the source does.
Why this is a reasonable design
The alternative would be to teach the JVM about type arguments so that overriding could be decided on generic rather than erased signatures. That is reification, and it was ruled out by the compatibility constraint that shaped the whole feature. Given erasure, the choices are broken polymorphism, a change to dispatch, or generated code, and generated code is the only one that leaves existing class files and existing JVMs untouched.
The mechanism generalizes past Java. Any system that compiles a richer type discipline onto a poorer runtime ends up emitting adapters at the boundary, whether they are called bridges, thunks, or shims. The general form is in Objects, Classes, and Dispatch, and the languages that avoid the problem avoid it by not erasing: see Reified Generics in the CLR and Traits and Generic Bounds in Rust.
Related Notes
- Generics and Type Erasure in Java - the transformation that creates the mismatch
- Reifiable Types and What Erasure Forbids - the other consequences of the same decision
- Raw Types and Migration Compatibility - where the bad value usually enters
- Objects, Classes, and Dispatch - vtables and the general dispatch mechanism a bridge sits inside
- The Class File and Classloading - why a descriptor mismatch means no override
Sources
- “Bridge Methods,” The Java Tutorials. https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html . Supports the
Node/MyNodeexample, both classes’ erased forms, the quoted explanation that the signatures do not match after erasure soMyNode.setData(Integer)does not overrideNode.setData(Object), the generated bridge method’s exact body, and the statement that the bridge delegates to the original method. - “Type Erasure,” The Java Tutorials. https://docs.oracle.com/javase/tutorial/java/generics/erasure.html . Supports that the compiler generates bridge methods to preserve polymorphism in extended generic types, and that it inserts casts to preserve type safety.