skip to content

What problems can bridge methods cause when you process methods via reflection, and how do you handle them?

level: seniorimportance: should knowfreq 28%

answer

  1. getDeclaredMethods() returns bridges too → method appears twice
  2. skip with m.isBridge() (and often isSynthetic())
  3. bridge has erased signature → wrong/Object types in metadata
  4. annotations historically not copied to bridges (improved ~Java 8)
  5. Spring BridgeMethodResolver maps bridge → real method

basics

~20 s

Reflection lists bridge methods alongside real ones, so you can see two methods with the same name. You should skip bridge methods (check Method.isBridge()) so you don't process duplicates or pick the one with the wrong, erased parameter/return types.

solid answer

~40 s

getDeclaredMethods() returns compiler-generated bridge methods too, so a generic override appears twice: your real method (e.g. set(String)) and the bridge (set(Object)). Code that scans methods — DI frameworks, ORMs, serializers, mocking libraries, annotation processors — can double-process, fail to find annotations, or invoke the erased-signature bridge that performs an unchecked cast. The standard remedy is to filter with Method.isBridge() (and often isSynthetic()) and ignore those. Historically annotations were not copied onto bridges, so reading annotations off a bridge returned nothing; this was improved (Java 8 copies many), but you still shouldn't rely on it. When you must invoke, target the real method by matching the specific (non-erased) parameter types, or resolve the most specific method. Frameworks like Spring provide a BridgeMethodResolver to map a bridge back to the method it bridges to.

code

java · 12 lines
java
interface Setter<T> { void set(T value); }

class StringSetter implements Setter<String> {
    public void set(String value) { /* real */ }
    // bridge generated: public void set(Object v) { set((String) v); }
}

for (Method m : StringSetter.class.getDeclaredMethods()) {
    if (m.isBridge() || m.isSynthetic()) continue; // skip the bridge
    // process only set(String) here
    System.out.println(m.getName() + Arrays.toString(m.getParameterTypes()));
}

go deeper

for a junior

May not realize reflection returns extra methods; can at least recognize that there can be duplicates of the same name.

for a middle

Knows to skip bridge methods with isBridge() when iterating and that bridges carry erased types.

for a senior

Explains the concrete failure modes (duplicates, lost annotations, erased metadata, ClassCastException), the isBridge/isSynthetic distinction, and how to resolve a bridge back to the real method.

for a principal

Reasons about framework design impact across DI/ORM/serializers, the annotation-copying history and its version-dependence, and designs robust method-scanning that is correct across javac versions and generic hierarchies.

## Why this matters A huge amount of Java infrastructure inspects classes with the Reflection API: dependency injection (Spring, CDI), object-relational mappers (Hibernate), JSON/XML serializers (Jackson), validation, mocking (Mockito), and annotation processors. All of them call `Class.getDeclaredMethods()` or `getMethods()` and iterate. Bridge methods quietly change what that iteration returns. ## What reflection actually returns Given a generic override: ```java interface Setter<T> { void set(T value); } class StringSetter implements Setter<String> { public void set(String value) { ... } } ``` After erasure `StringSetter` contains **two** methods in bytecode: - `void set(String)` — your real method. - `void set(Object)` — the synthetic bridge that casts to `String` and forwards. So `StringSetter.class.getDeclaredMethods()` returns *both*. Naive code that, say, "finds the method named set and invokes it" may pick the bridge. ## Concrete problems 1. **Duplicate processing.** A scanner that registers every method will register `set` twice — e.g. exposing two endpoints, two columns, or two mock stubs. 2. **Wrong parameter/return types.** The bridge has the *erased* signature (`Object`, or the bound). If you build metadata from it you record `Object` instead of `String`, losing precision. 3. **Missing annotations.** Originally the compiler did not copy your method's annotations onto the bridge, so reading `@Column`/`@JsonProperty` off the bridge returned `null`/empty. The compiler was changed (around Java 8) to copy many annotations to bridges, but behavior varies by annotation `@Target`/`@Retention` and javac version, so you should not depend on it. 4. **Unchecked cast surprises.** Invoking the bridge with the wrong runtime type throws `ClassCastException` from inside the bridge — a confusing stack trace that points at a method you never wrote. ## The standard remedies - **Filter them out.** When you only need each logical method once, skip bridges: ```java for (Method m : type.getDeclaredMethods()) { if (m.isBridge() || m.isSynthetic()) continue; process(m); // only the real, specific-typed method } ``` `isBridge()` is the precise check; `isSynthetic()` is broader (also catches other compiler-generated members like access$ methods and lambda bodies). - **Resolve to the real method.** When you have a bridge in hand and need the genuine target, find the most specific overriding method with matching erased parameter types but non-bridge flag. Spring ships `org.springframework.core.BridgeMethodResolver.findBridgedMethod(Method)` for exactly this; other frameworks have equivalents. - **Prefer the most specific.** When choosing among methods of the same name, prefer the one whose parameter types are *not* the erased bound — that is your real override. ## When you might WANT the bridge If you are invoking through a raw/erased view (you only have an `Object` argument), the bridge is the right entry point because it does the cast for you. But for metadata extraction, almost always skip it. ## Summary rule of thumb *"When scanning methods reflectively, skip `m.isBridge()`; when you must map a bridge back to source, use a bridge resolver."* This avoids duplicate processing, lost annotations, and erased-type metadata.

  • What is the difference between Method.isBridge() and Method.isSynthetic()?
    isSynthetic() is true for any compiler-generated member not in source (bridges, accessor methods, lambda bodies). isBridge() is the narrower flag for bridge methods specifically. Every bridge is synthetic, but not every synthetic member is a bridge.
  • How can a framework recover the real method from a bridge method?
    By matching the bridge's name and erased parameter types against the class's non-bridge methods to find the most specific override — Spring's BridgeMethodResolver.findBridgedMethod does this.

saying these in an interview costs you the question

  • Assuming getDeclaredMethods() returns only source-written methods.
  • Relying on annotations always being present on the bridge method.
  • Filtering only by name and not by isBridge(), then invoking the erased Object-typed bridge.
  • Confusing isBridge() with isSynthetic() — every bridge is synthetic, but not every synthetic is a bridge.

context