Skip to content

Covariant Returns and Bridge Methods — Interview Q&A

20 questions covering covariant return rules, bridge generation, JVMS flags, reflection behaviour, and the framework interactions (Mockito, Spring AOP, Jackson) that bridges routinely affect.


Q1. What is a covariant return type, and since which Java version?

A covariant return type is a return-type override where the subclass narrows the return — declares a return type that is a subtype of the parent's. It was added in Java 5 as part of the same JLS revision that introduced generics. Specified in JLS §8.4.5 under "return-type substitutability". Classic example: Animal copy() overridden by Dog copy().

Trap: Many candidates think it's older. It is not — pre-Java 5, every override had to use the parent's exact return type.


Q2. What is a bridge method?

A bridge method is a synthetic helper method generated by javac to satisfy the JVM's descriptor-based dispatch when source-level signatures and erased/parent signatures differ. It carries the parent's (or erased) descriptor and forwards via invokevirtual to the real method. Flag mask: ACC_BRIDGE | ACC_SYNTHETIC | ACC_PUBLIC = 0x1041.

You don't write bridges; the compiler does. You read them in javap -p -v output.


Q3. When does the compiler generate a bridge method?

Two trigger conditions:

  • Covariant return: subclass override narrows the return type relative to the parent.
  • Generic narrowing: a class implements a generic interface or extends a generic class, and a method's source-level signature uses the type parameter (which erases to a wider type, usually Object).

In both cases, the source-level signature and the erased parent signature differ, so two methods are needed to satisfy both contracts.


Q4. Why are bridges necessary — couldn't the JVM dispatch by source signature?

The JVM resolves methods by descriptor (parameter types + return type after erasure), not by source signature. JVMS §5.4.3.3 and §5.4.6 define dispatch in terms of descriptor matching. If Score.compareTo(Score) has descriptor (LScore;)I but Comparable.compareTo(Object) has descriptor (Ljava/lang/Object;)I, those are different methods. To "override" Comparable.compareTo, the compiler must add a method with the right descriptor — that's the bridge.


Q5. What flags identify a bridge method in the class file?

ACC_BRIDGE (0x0040) and ACC_SYNTHETIC (0x1000). Both per JVMS §4.6, Table 4.6-A. Bridges are also typically ACC_PUBLIC (matching the access of the real method), giving a combined flag mask of 0x1041. Reflection's Method.isBridge() checks the ACC_BRIDGE bit.


Q6. Show a minimal Java class with a bridge. What does javap -p -v show?

public class Score implements Comparable<Score> {
    @Override public int compareTo(Score s) { return 0; }
}

javap -p -v shows two compareTo methods:

public int compareTo(Score);
  descriptor: (LScore;)I
  flags: (0x0001) ACC_PUBLIC

public int compareTo(java.lang.Object);
  descriptor: (Ljava/lang/Object;)I
  flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
  Code:
    0: aload_0
    1: aload_1
    2: checkcast     #2  // class Score
    5: invokevirtual #19 // Method compareTo:(LScore;)I
    8: ireturn

Q7. How does the bridge dispatch when called through a parent reference?

Comparable<Score> c = new Score(...); c.compareTo(other); compiles to invokeinterface Comparable.compareTo:(Object)I. At runtime, JVMS §5.4.6 picks the override on Score that matches the descriptor — that's the bridge. The bridge checkcasts the argument to Score, then invokevirtuals the real compareTo(Score). Two frames, both inlined by C2 at hot paths.


Q8. Is the bridge a separate vtable slot, or does it share the slot with the real method?

A separate slot. The bridge has its own descriptor, its own bytecode, its own method-table entry. The two methods are independent at the JVM level; one cannot replace the other.

Trap: "Bridges are just aliases." False. They're distinct methods that happen to forward.


Q9. What does Class.getDeclaredMethods() return for a class with a bridge?

Both methods — the real one and the bridge. Iteration order is not specified by the JVM spec. To get "only what the user wrote", filter:

.filter(m -> !m.isBridge() && !m.isSynthetic())

Class.getMethods() (which returns inherited public methods) also includes the bridge.


Q10. How does Method.isBridge() differ from Method.isSynthetic()?

Every bridge is synthetic, but not every synthetic method is a bridge. Examples of synthetic-but-not-bridge:

  • Compiler-generated lambda bodies (lambda$0).
  • Access methods generated for private access across nested classes (access$000).
  • values() and valueOf() on enums.

Method.isBridge() flags exactly the covariant-return / generic-erasure bridges. Use it for that specific concern.


Q11. How many bridges does a three-level covariant-return chain produce?

For:

class Animal { public Animal copy(); }
class Dog    extends Animal { public Dog   copy(); }
class Puppy  extends Dog    { public Puppy copy(); }
  • Dog has one bridge: Animal copy() forwarding to Dog copy().
  • Puppy has two bridges: Animal copy() and Dog copy(), each forwarding to Puppy copy().

Each level of narrowing contributes one bridge to every descendant class.


Q12. How do bridges interact with Spring AOP?

Spring AOP must intercept method calls whether they come through a typed or raw reference. Pre-Spring 4.0 had bugs where the framework matched the pointcut against the bridge but missed annotations (which live on the real method). Spring's BridgeMethodResolver.findBridgedMethod(m) walks bridges back to the real method for annotation inspection. From version 4.x onward this is built into AOP and @Transactional processing.

If you write a custom MethodInterceptor or BeanPostProcessor, you must call BridgeMethodResolver yourself.


Q13. How do bridges interact with Mockito?

Mockito 2.x and later (using ByteBuddy) correctly route invocations through bridges in normal use. Pre-2.x had real issues. Edge cases that still trip people up:

  • Calling a mocked method through a raw reference may land on the bridge frame, causing stubbing/verification mismatches with older Mockito versions.
  • Stubbing a method that exists only through a generic parent class can confuse the matcher.

Recommendation: stay on Mockito 4.x or later, always use typed references in tests.


Q14. Can a developer write the erased signature manually instead of letting the compiler generate the bridge?

Technically yes, but it's a bug. If you write:

public class Score implements Comparable<Score> {
    public int compareTo(Score s)  { return 0; }
    public int compareTo(Object o) { return 1; }   // manual erased version
}

The compiler will not generate the bridge (the slot is taken). Polymorphic calls through Comparable.compareTo(Object) route to your manual method, which doesn't forward to compareTo(Score). Dead code in disguise.

Always use @Override and the source-typed signature only. Trust the bridge.


Q15. Why are annotations not automatically copied to the bridge?

The JLS does not require it; most compilers do not do it by default. Bridges are implementation detail; their job is dispatch forwarding, not metadata reflection. Frameworks that read annotations off methods must walk to the bridged method:

Method real = m.isBridge()
    ? BridgeMethodResolver.findBridgedMethod(m)
    : m;
Annotation a = real.getAnnotation(SomeAnno.class);

This is the source of many "annotation missing" bugs.


Q16. What does Method.getGenericReturnType() return on a bridge?

The erased type. Bridges do not carry a Signature attribute (JVMS §4.7.9) — they only have the raw descriptor. So getGenericReturnType() returns the same as getReturnType() on a bridge, with no generic information. Tools that need parameterised type info (Jackson type adapters, JAX-RS resource scanners) must read from the real method.


Q17. What's the runtime cost of going through a bridge?

At hot, JIT-compiled paths: zero. C2 inlines the bridge body (typically 5 bytecodes) into the caller, then inlines the real method's body into the bridge. The combined optimised machine code looks identical to a direct call.

At cold paths (interpreter, startup, megamorphic): one extra invokevirtual and one checkcast per erased reference parameter. A few nanoseconds. Negligible except in micro-benchmarks of startup-only code.


Q18. Can a bridge be invoked through super?

No — super.method() is invokespecial (non-virtual), and it targets the next-most-derived implementation, not the bridge. If you write super.copy() in a class with a covariant chain, you get the parent's real method, not the parent's bridge. The bridge is only entered via dispatched (virtual) calls from outside the class hierarchy.


Q19. Does the JVM verify bridges differently from regular methods?

No. From the JVM's perspective, bridges are normal methods. The ACC_BRIDGE flag is informational — it tells reflection tools and the compiler internals that the method is synthetic. Verification (JVMS §4.10) applies the same rules: type-safe bytecode, correct stack frames, valid descriptors. Bridges pass verification the same way any other method does.


Q20. Give a real-world scenario where ignoring bridges causes a production bug.

A custom annotation @Audited is read by a BeanPostProcessor that wraps the bean to log invocations. The processor scans bean.getClass().getDeclaredMethods() and registers wrappers on each method with @Audited. The bean implements a generic interface GenericService<T>.

  • For typed callers: wrapper registered on the real method, invocation flows through it, audit log appears.
  • For raw-typed callers (or framework code calling through the parent interface): invocation routes through the bridge first; the bridge has no annotation; the processor didn't install a wrapper on the bridge; audit log is missing.

The fix: either also install wrappers on bridge methods (with the same target), or ensure all callers use typed references. Most teams pick the former because they can't audit every caller.

This is the failure mode that motivated Spring's BridgeMethodResolver in 2008 and continues to bite frameworks in 2025.


Quick-recall cheat sheet

  • Covariant return: JLS §8.4.5. Java 5+. Subclass returns subtype of parent's return.
  • Bridge method: synthetic forwarder generated by javac, flag ACC_BRIDGE | ACC_SYNTHETIC (0x1040), forwarder bytecode = checkcast + invokevirtual + return.
  • Method.isBridge(): bitmask check; use it to filter reflection results.
  • Spring AOP: handled by BridgeMethodResolver; you may need to call it in custom infrastructure.
  • Mockito: 4.x handles bridges correctly; older versions and raw-typed calls cause trouble.
  • Annotations: not copied to bridges by default; walk to the real method.
  • Cost: zero at JIT-hot paths; one extra frame at cold/reflective paths.
  • Cross-references: dispatch in ../01-jvm-method-dispatch/; vtable slots in ../02-vtable-and-itable/; design principles in ../../03-design-principles/.

Memorize this: every generic class with a typed override has a bridge; every covariant return narrowing has a bridge; reflection sees them; frameworks must either filter them or resolve through them. The compiler does the heavy lifting — your job is to recognise the pattern and not break it.