A Java service compiles cleanly but at runtime fails with NoSuchMethodError or AbstractMethodError against a third-party library. What family of JVM failures is this, and what actually causes it?
answer
- Compile against declarations, resolve against reality
- Descriptor includes return type
- AbstractMethodError = interface grew after implementor compiled
- IncompatibleClassChangeError = structural flip
- -Xlog:class+load names the winning JAR
basics
~20 sIt is the IncompatibleClassChangeError family under LinkageError: the runtime class no longer matches what the code was compiled against. Resolution is lazy and checked against the JAR actually on the classpath, so a version or duplicate-artifact mismatch shows up only when the member is first used.
solid answer
~50 sThese are **link-time inconsistency** errors. javac writes symbolic references (owner, name, descriptor) into the constant pool; the JVM resolves them lazily against whatever class is on the runtime classpath. If that class no longer declares a matching member, resolution fails with the appropriate `IncompatibleClassChangeError` subtype: - **`NoSuchMethodError`** / **`NoSuchFieldError`**: the member is gone, renamed, or its descriptor changed (return type or parameter types are part of the identity). - **`AbstractMethodError`**: an implementation exists but does not override an abstract or newly added interface method, so the interface grew after the implementor was compiled. - **`IncompatibleClassChangeError`** itself: a structural flip such as class-to-interface, or a static member now instance. The cause is almost always **classpath composition**: two versions of a library present and the wrong one winning, a transitive upgrade, shading that half-relocated packages, or a plugin loaded by a different loader. The fix lives in the build, not in the code: converge versions, isolate or shade deliberately, and verify with class-load logging which JAR actually defined the class.
code
text · 3 lines$ java -Xlog:class+load=info -jar app.jar | grep 'com.acme.Widget'
[0.412s][info][class,load] com.acme.Widget source: jar:file:/app/lib/acme-core-1.2.0.jar!/
# compiled against acme-core-2.0.0 -> method added in 2.0 resolves to nothing -> NoSuchMethodErrorgo deeper
Recall that the code was compiled against one version of a library and is running against another, and that the JVM checks method references only when the line executes.
Name the family members and what each implies, explain that descriptors include return type, and know that the fix is in dependency resolution, not the call site.
Diagnose from the message plus class-load logging to identify which archive defined the class, reconcile the resolved dependency graph, and choose between converging, shading or loader isolation.
Own the policy: single-version discipline via a platform/BOM, build-time duplicate and convergence checks, binary-compatibility gates on published artifacts, and a smoke test against the assembled runtime classpath.
## Separate compilation is the root cause Java compiles against *declarations*, not implementations. When javac sees `logger.info(msg, arg)`, it records a symbolic reference into the class file constant pool: the owner class name, the method name, and the **descriptor** (parameter types and return type). Nothing about the target's body is copied in. At run time the JVM resolves that reference the first time the instruction executes, looking at whatever class the loader hands it. This two-phase arrangement is what makes libraries upgradable without recompiling the world. It is also what allows the two views to drift apart. Compile against version 2 of a library, ship version 1, and every reference to a member added in version 2 becomes a bomb that detonates the first time that line runs, not at startup, not at deploy. ## The error taxonomy All of these extend `IncompatibleClassChangeError`, which extends `LinkageError`, which extends `Error`: - **`NoSuchMethodError`**: resolution found the class but no method with that exact name *and descriptor*. Note that the return type is part of the descriptor, so a library changing `List getAll()` to `Collection getAll()` breaks callers even though the source would still compile. Overload changes (`log(String)` to `log(CharSequence)`) do the same. - **`NoSuchFieldError`**: same, for a field reference, common when a public constant is removed or its type changes. - **`AbstractMethodError`**: the method resolved, but the receiver's class provides no concrete implementation. Classic shape: an interface gains a method in v2; your classpath has a v1 implementation of that interface compiled before the method existed; a v2 caller invokes it. Also seen when a library adds abstract methods to an abstract base class. - **`IncompatibleClassChangeError`** raw: a structural contract flip. A type that was a class became an interface (or the reverse), a `static` member became instance or vice versa, or a `final` class is now extended. The bytecode used the wrong instruction family (`invokevirtual` vs `invokeinterface`, `getstatic` vs `getfield`) for what the runtime type actually is. These sit alongside, but are distinct from, `NoClassDefFoundError` (the whole type is absent or erroneous). Method-level errors mean the type is present and simply the wrong shape. ## Why it happens in practice 1. **Diamond dependency conflict.** Two libraries transitively require different versions of a third. The build's conflict resolution picks one (Maven takes the nearest, Gradle the highest by default), and the loser's callers break. Compilation succeeded because the compile classpath was resolved the same way and the *compiled* call sites came from source that only saw the winner. 2. **Runtime classpath differs from compile classpath.** `provided`/`compileOnly` scopes, a container supplying its own copy of an API, or an application server whose loader wins over the application's copy. 3. **Duplicate classes.** Two JARs contain the same package and class name (an uber-JAR plus the original, a repackaged fork). The class that gets defined depends on classpath order, which is not stable across environments. 4. **Shading gone wrong.** Relocating `com.foo` to `shaded.com.foo` in some artifacts but not all leaves callers pointing at one copy and implementors at another. 5. **Genuine binary-incompatible upgrade.** The library made a change that is source-compatible but not binary-compatible. Binary compatibility rules are their own discipline: adding a method to an interface, changing a return type, or widening a parameter type all break already-compiled callers even when a recompile would succeed. ## Diagnosing The error text names the exact member. Steps that work: - Read the message: it prints owner, member and often the descriptor. That tells you which artifact should have declared it. - Run with `-Xlog:class+load=info` (Java 9+) or `-verbose:class` and grep for the owner class. The output names the **source file/JAR** each class was defined from, which immediately reveals a duplicate or the wrong version winning. - Ask the build for the resolved graph (`mvn dependency:tree`, `gradle dependencies`) and look for the version that actually resolved versus the one you compiled against. - Check for duplicate class entries across artifacts; build plugins exist for exactly this check. ## Fixing and preventing The fix is a build fix: converge on one version (dependency management block / BOM / platform), remove the duplicate artifact, or, when convergence is impossible, deliberately isolate: shade one copy under a private package, or give a plugin its own loader so the two versions live in separate namespaces. Prevention is checkable at build time. Enforce dependency convergence and fail the build on duplicate classes. Run a binary-compatibility checker over your own published artifacts so *you* do not do this to downstream consumers. And run at least one smoke test against the fully assembled artifact (fat JAR, container image, deployed module path), because linkage errors by construction cannot be caught by compiling: only executing the real, assembled classpath exercises resolution.
- How is AbstractMethodError different from NoSuchMethodError?NoSuchMethodError means resolution could not find any method with that name and descriptor on the target type. AbstractMethodError means the method resolved fine but the concrete receiver class supplies no implementation, typically because an interface or abstract class gained a method after the implementing class was compiled. The first points at a missing member, the second at a stale implementor.
- Why can a change be source-compatible but not binary-compatible?Source compatibility only requires that existing source still compiles; binary compatibility requires that already-compiled class files still link. Because call sites embed the full descriptor, changing a return type, narrowing or widening a parameter type, or adding an abstract method to an interface keeps sources compiling while breaking compiled callers, which fail at resolution with NoSuchMethodError or AbstractMethodError.
- Two JARs on the classpath contain the same class. Which one wins, and why is that dangerous?The first one found in classpath order by the defining loader wins, and the rest are invisible. It is dangerous because classpath order varies with build layout, container packaging and shell globbing, so the same artifact can link correctly in one environment and throw NoSuchMethodError in another. Detect duplicates at build time rather than relying on ordering.
saying these in an interview costs you the question
- Claiming the compiler should have caught it, missing that resolution is lazy and classpath-dependent
- Thinking method identity is just the name, so overloads and return-type changes are safe
- Fixing it by adding a try/catch around the call site instead of correcting the classpath
- Assuming classpath ordering is stable enough to rely on when duplicate classes exist
- Confusing it with NoClassDefFoundError, which means the whole type is missing or erroneous