All topics
library
advanced

Covariant Return Types & Bridge Methods

Understand method overriding with narrower return types and the invisible bridge methods the compiler generates.

An override may return a more specific type than the method it overrides.

Covariant return = a factory that advertises 'produces vehicles' (parent) but a subsidiary actually produces 'sports cars' (child). The subsidiary's product IS a vehicle (still compatible), just more specific. Bridge method = a redirect sign at the factory entrance that sends visitors to the sports car line.

Key Concepts

1
    class Animal { Animal reproduce() { return new Animal(); } }
2
    class Dog extends Animal {
      @Override
      Dog reproduce() { return new Dog(); }        // narrower -- legal
    }
3
Without this, callers would cast.
    Dog d = (Dog) myDog.reproduce();     // pre-Java 5
    Dog d = myDog.reproduce();            // now
4
It is safe because it only narrows. Anything expecting an Animal back still gets one, so no existing caller can break. Widening the return type would break them, and is rejected.
Animal
5
The JVM does not support it, which is where bridge methods come from. Bytecode dispatch includes the return type, so Dog reproduce() and Animal reproduce() are different methods down there.
Dog reproduce()Animal reproduce()
6
So the compiler writes a second method for you:
    // what javac actually puts in Dog
    Dog reproduce() { return new Dog(); }
7
    Animal reproduce() {                  // synthetic bridge
      return reproduce();                 // calls the one above
    }
8
Code compiled against Animal calls the bridge, which forwards. Binary compatibility is preserved.
Animal
9
Generics produce them too, and more often:
    class Order implements Comparable<Order> {
      public int compareTo(Order o) { ... }
10
      // bridge the compiler adds, because erasure made the
      // interface method compareTo(Object):
      // public int compareTo(Object o) { return compareTo((Order) o); }
    }
11
That cast inside the bridge is where a ClassCastException can surface from a line you did not write — if raw types let something of the wrong type reach it.
ClassCastException
12
You will see them in three places.
    javap -v                 flagged ACC_BRIDGE ACC_SYNTHETIC
    Method.isBridge()        true
    a stack trace or debugger, as an extra frame
13
Which is the practical value of knowing this. A duplicate-looking method in reflection output, or a mysterious frame in a stack trace, is a bridge. You never write one, and getDeclaredMethods() will hand them to you — so framework code usually filters on isBridge() or isSynthetic().
getDeclaredMethods()isBridge()isSynthetic()
14
What the interviewer is probing.1. "Why is narrowing a return type safe but widening is not?" Probing: substitutability. Stalls: "Both should be allowed." Moves up: anything expecting the parent type still receives something valid when you narrow; widening would hand callers a type they cannot use.
15
2. "Why does the compiler generate a bridge method?" Probing: the JVM limitation. Stalls: "For performance." Moves up: bytecode dispatch includes the return type, so the two signatures are different methods at that level; the bridge forwards from the erased one.
16
3. "Where would you notice a bridge method?" Probing: practical consequence. Stalls: "Nowhere." Moves up: duplicate-looking methods in reflection output and an extra frame in a stack trace, which is why framework code filters on isBridge.
17
4. "Where can a ClassCastException come from inside a bridge?" Probing: the hidden cast. Stalls: "It cannot." Moves up: the bridge casts its Object argument to the real type, so raw-type misuse surfaces there, in a line nobody wrote.