Covariant Returns and Bridge Methods — Practice Tasks¶
Eight hands-on exercises. Each forces you to compile real code, inspect bytecode with javap, and verify the bridge's presence/behaviour. Treat this as lab work — your goal is to see bridges, manipulate them, and learn to predict them on sight.
For each task: (1) write the code, (2) run javac and javap -p -v, (3) record what you see in the bytecode, (4) write a small test or assertion that confirms your understanding.
Task 1 — Comparable and finding the bridge¶
Implement a Score class:
public class Score implements Comparable<Score> {
private final int value;
public Score(int v) { this.value = v; }
@Override public int compareTo(Score other) {
return Integer.compare(this.value, other.value);
}
public int value() { return value; }
}
Steps:
javac Score.java.javap -p -v Score | grep -A2 'compareTo'.- Identify both
compareTomethods. Record their descriptors and flags. - Write a small driver that calls
compareTothrough both aScorereference and aComparable<Score>reference. Use-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilationto see which compilation tier the methods reach.
Deliverable: the two descriptors ((LScore;)I and (Ljava/lang/Object;)I), the flag mask of the bridge (0x1041), and a brief note on whether the bridge appears in +PrintCompilation output.
Task 2 — Covariant returns three levels deep¶
Build the hierarchy:
class Animal { public Animal copy() { return new Animal(); } }
class Dog extends Animal { @Override public Dog copy() { return new Dog(); } }
class Puppy extends Dog { @Override public Puppy copy() { return new Puppy(); } }
Steps:
javac *.java.javap -p -v Puppy. How manycopymethods are listed? You should see three: the realPuppy copy()plus two bridges (Animal copy()andDog copy()).javap -p -v Dog. How many? (Two — real plusAnimalbridge.)- Trace, on paper, what happens when:
Animal a = new Puppy(); a.copy();is called.Dog d = new Puppy(); d.copy();is called.Puppy p = new Puppy(); p.copy();is called.
Deliverable: for each of the three call sites, the chain of methods invoked (caller → bridge(s) → real method).
Task 3 — Debug a Mockito test that fails because of bridge methods¶
Given:
public abstract class GenericHandler<T> {
public abstract String handle(T input);
}
public class StringHandler extends GenericHandler<String> {
@Override public String handle(String input) { return input.toUpperCase(); }
}
Write a failing Mockito test (Mockito 4.x):
@ExtendWith(MockitoExtension.class)
class HandlerTest {
@Mock GenericHandler<String> handler;
@Test void demonstrateBridgeRouting() {
when(handler.handle(any())).thenReturn("stubbed");
@SuppressWarnings({"rawtypes","unchecked"})
GenericHandler raw = handler;
Object result = raw.handle("hello");
assertEquals("stubbed", result);
}
}
Steps:
- Run the test. Observe whether it passes (Mockito version dependent).
- If it fails, log
Mockito.mockingDetails(handler).getInvocations()and confirm whichMethodwas recorded. - Fix by always using the typed reference:
handler.handle("hello"). - Add
verify(handler).handle("hello")to confirm matcher works on typed call.
Deliverable: the exact failure message, the recorded Method object (real vs. bridge), and the fix.
Task 4 — Reflection filtering with Method.isBridge¶
Write a utility:
public class MethodScanner {
public static List<Method> userDeclaredMethods(Class<?> c) {
return Arrays.stream(c.getDeclaredMethods())
.filter(m -> !m.isBridge() && !m.isSynthetic())
.toList();
}
}
Steps:
- Apply to
Score,StringHandler,Puppy. For each, list real methods only. - Apply to a class with both
Comparable<T>and a covariant-return override. - Compare with
c.getDeclaredMethods()directly. Note the synthetic methods present. - Verify
getMethods()(returns inherited public) also includes bridges by counting before and after filter.
Deliverable: for each test class, the count of real methods vs. all methods, and a one-line conclusion about what getDeclaredMethods() returns.
Task 5 — Override a default method with covariant return¶
public interface Cloner<T> {
default T clone() { return null; }
}
public class DogCloner implements Cloner<Dog> {
@Override public Dog clone() { return new Dog(); }
}
Steps:
- Compile and run
javap -p -v DogCloner. Where does the bridge live — on the interface or on the implementing class? - Add another implementer
CatCloner implements Cloner<Cat>with covariant return. ConfirmCatClonerhas its own bridge. - Confirm the interface
Clonerhas no bridges (open the class file withjavap -p -v Cloner). - Trace what happens when
Cloner<Dog> c = new DogCloner(); c.clone();runs — through the itable, into the bridge, into the real method.
Deliverable: the bridge's flag mask and bytecode for DogCloner, and a confirmation that the interface itself has none.
Task 6 — A generic factory and its bridges¶
public interface Factory<T> {
T create();
}
public class UserFactory implements Factory<User> {
@Override public User create() { return new User("default"); }
}
Steps:
- Compile and inspect
UserFactorywithjavap. Find the bridgeObject create(). - Write a test that calls
create()through aFactory<User>reference and asserts the runtime class isUser. - Add
AdminFactory extends UserFactorywith@Override public Admin create() { return new Admin(); }. How many bridges doesAdminFactoryhave? (Two: one forFactory.create()returningObject, one forUserFactory.create()returningUser.) - Verify with
javap.
Deliverable: the bridge count on UserFactory (one) and AdminFactory (two), with the descriptors of each.
Task 7 — Annotation processor inspecting bridges¶
Write a small annotation processor (or a runtime equivalent) that:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
public @interface Audited {}
public class AuditedHandler implements GenericHandler<String> {
@Override @Audited public String handle(String input) { return input; }
}
Steps:
- Reflectively scan
AuditedHandler.getDeclaredMethods(). For each method, checkm.getAnnotation(Audited.class). Is the annotation present on the bridge? - Spring's
BridgeMethodResolver.findBridgedMethod(m)is the resolution helper; replicate the algorithm yourself by walking the class hierarchy. - Confirm that
m.isBridge() == trueandm.getAnnotation(Audited.class) == nullfor the bridge, but the real method has the annotation. - Conclude: a naive scan that picks the bridge will miss the annotation.
Deliverable: a working realMethod(Method m) helper that returns the bridged method when m is a bridge, plus a test demonstrating it surfaces the @Audited annotation correctly.
Task 8 — MethodHandle resolution to real vs. bridge¶
public class Score implements Comparable<Score> {
@Override public int compareTo(Score other) { return 0; }
}
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle real = lookup.findVirtual(Score.class, "compareTo",
MethodType.methodType(int.class, Score.class));
MethodHandle bridge = lookup.findVirtual(Score.class, "compareTo",
MethodType.methodType(int.class, Object.class));
Steps:
- Both
findVirtualcalls succeed. Confirm they return different method handles. - Invoke each with the same arguments and confirm the result is identical (the bridge forwards correctly).
- Use
MethodHandleInfo(vialookup.revealDirect(handle)) to see the underlying method's descriptor for each. - Benchmark a tight loop calling
real.invokeExactvsbridge.invokeExact. Quantify the cost difference (expect a few nanoseconds at most, likely below noise after warmup).
Deliverable: the two MethodHandleInfo outputs, the benchmark numbers, and a one-line conclusion about which MethodType to specify when generating handles programmatically.
Self-check questions¶
After completing the tasks:
- For a class with
Comparable<T>and a covariant clone, how many bridges doesjavap -p -vshow? - What's the flag mask for a bridge method? (
ACC_PUBLIC | ACC_BRIDGE | ACC_SYNTHETIC = 0x1041.) - What does
Method.isBridge()return for a manually writtencompareTo(Object)? (False — only the compiler-generated one is flagged.) - Why does Spring need
BridgeMethodResolvereven though it could just filter bridges? (Because annotations live on the real method, but AOP needs to advise both call paths.)
Memorize this: the way to internalise bridges is to see them with javap -p -v again and again. Do all eight tasks; by the end you will predict the bridge count and descriptors before you compile.