skip to content

An application throws `java.lang.ClassCastException: com.acme.Config cannot be cast to com.acme.Config` — the identical fully-qualified name on both sides. How is that possible, and how would you confirm the cause on a running system?

level: seniorimportance: must knowfreq 42%

answer

  1. identical names, different defining loaders
  2. duplicate jar: shared path + app path
  3. child-first delegation + shared type crossing
  4. print getClassLoader() of both Class objects
  5. fix: define the shared type once, in a common ancestor

basics

~20 s

The two names refer to two different run-time types, because the class was defined by two different class loaders. Confirm it by printing the class loader and identity hash of each side's Class object, and by finding the duplicate class source with -verbose:class.

solid answer

~60 s

Identical names, different types: the class was **defined twice**, by two different class loaders, so the JVM sees two unrelated run-time types whose `toString` happens to match. Typical causes: - the same jar present both in a shared/parent path and inside a component's own path, so both loaders define it; - a component loader that does not delegate for that package (child-first or fully self-first), redefining a type the host already defined; - an object created inside one plugin/deployment handed to another, or to the host, across an isolation boundary; - a framework proxy or generated class defined by a temporary loader that does not share the interface's loader. To confirm on the running system, print, for both the object and the target type: ```java System.out.println(o.getClass().getClassLoader()); System.out.println(Config.class.getClassLoader()); ``` Different loader instances prove it. `-verbose:class` shows both definitions and the source of each, exposing the duplicate jar. The fix is architectural: the shared type must be defined **once**, by a common ancestor loader that both sides delegate to — remove the duplicate artifact from the component, or exclude it from the component's own path.

code

java · 4 lines
java
Class<?> actual = obj.getClass(), expected = com.acme.Config.class;
System.out.println(actual.getClassLoader() + " vs " + expected.getClassLoader());
System.out.println(actual.getProtectionDomain().getCodeSource());   // which jar
System.out.println(expected.isAssignableFrom(actual));              // false

go deeper

for a junior

Recognize the signature symptom and state the cause: the class was loaded by two different class loaders, so they are two different types.

for a middle

Name the common origins — duplicate jar on two paths, child-first delegation — and demonstrate the check by printing both class loaders and testing isAssignableFrom.

for a senior

Drive the diagnosis end to end with class-load logging and code-source inspection, then fix the packaging or the delegation policy so the shared type is defined once. Reject reflection workarounds explicitly.

for a principal

Set the boundary rule for the platform: which packages are host-owned and always delegated, which are plugin-private, and how contracts are expressed so isolated components cannot leak their own types across the boundary.

## Why the message looks impossible A `ClassCastException` prints `getName()` for both sides, and `getName()` returns only the binary name. But the JVM's notion of a type is the pair *(binary name, defining loader)*. When the same class file is defined by two loaders, there are two distinct types with identical names. The cast then fails exactly as a cast between unrelated classes would, and the message reads like a contradiction. Some JVM versions extend the message with the loader of each side, which resolves the confusion immediately. Where the message is bare, you have to establish it yourself. ## The four situations that produce it 1. **The artifact is on two paths.** A library jar sits in a container's shared/lib directory *and* is packaged inside the deployed application. If the application's loader defines its own copy — either because it is child-first, or because the shared copy is not visible for delegation — objects crossing between container code and application code carry the wrong type. 2. **Child-first (parent-last) delegation.** Deliberately inverted delegation is common in plugin hosts so a plugin can pin a newer dependency version. It is correct *until* an instance of a class that both sides define crosses the boundary. Types intended to be shared must be excluded from the inverted region and always delegated upward. 3. **An object escaping an isolation boundary.** Plugin A returns an object of type `com.acme.Config` defined by A's loader; the host casts it to `com.acme.Config` as defined by the host's loader. Anything reflective, serialized-then-deserialized in the wrong context, or passed through a generic registry can do this silently. 4. **Generated/proxy classes defined in a separate loader.** Dynamic proxies, bytecode-generated subclasses, or classes defined by an ad-hoc loader implement interfaces resolved from *that* loader's view. If it does not delegate to the loader that defined the interface, the generated class implements a same-named, different interface, and the cast at the use site fails. ## Confirming it on a live system The diagnostic is short and decisive. For the value and the expected type, print name, `Class` identity hash and loader: ```java Object o = registry.lookup("config"); Class<?> actual = o.getClass(); Class<?> expected = com.acme.Config.class; System.out.println(actual + " @" + Integer.toHexString(System.identityHashCode(actual)) + " loader=" + actual.getClassLoader()); System.out.println(expected + " @" + Integer.toHexString(System.identityHashCode(expected)) + " loader=" + expected.getClassLoader()); System.out.println("assignable: " + expected.isAssignableFrom(actual)); ``` Same names, different loaders, `assignable: false` — that is the confirmation. Two further tools: - **`-verbose:class`** logs every class definition with the source it came from. Grep the output for the class name: two lines with two different sources (or the same source twice) show where the duplicate definition entered. In modern JVMs the equivalent is unified logging: `-Xlog:class+load=info`. - **Where did the second copy come from?** `actual.getProtectionDomain().getCodeSource().getLocation()` on each `Class` object gives the jar or directory each definition was read from, which is usually the whole story. The complementary build-side check is a dependency-tree or duplicate-classes report showing the same coordinates packaged in more than one place. ## Fixing it properly The rule is: **a type that crosses a boundary must be defined exactly once, by a loader that is an ancestor of both sides.** - Remove the duplicate artifact from the component (mark it `provided`/`compileOnly` if the host supplies it), or remove it from the host if the component owns it. - If delegation is deliberately inverted, add the shared packages to the delegate-to-parent list so those types always come from the parent. - Define generated classes and proxies in a loader that sees the interface's defining loader — most proxy APIs take a loader argument for exactly this reason; pass the interface's loader. - Where genuinely independent versions must coexist, do not share the type at all: exchange data across the boundary in a form both sides define independently (a shared, host-provided interface; a plain string/byte payload; a host-owned DTO). ## Anti-patterns to avoid Do not "fix" it by reflection (`invoke` the getter by name to dodge the cast) — that hides a broken boundary and multiplies the failure modes. Do not add `equals`-by-name comparisons of `Class` objects. Do not set the thread context class loader at random until the exception disappears; decide deliberately which loader owns the shared type and make delegation reflect it.

  • Would switching the cast to reflection be an acceptable workaround?
    No. Calling methods reflectively bypasses the symptom while leaving two incompatible type worlds in the process — statics are still duplicated, callbacks and collections typed to one side still fail, and the failure resurfaces somewhere less obvious. The boundary must be fixed by making the shared type single-defined.
  • Two plugins genuinely need incompatible major versions of the same library. How can they still talk to the host?
    Keep the conflicting library entirely private to each plugin and never let its types cross the boundary. Define the exchange contract in host-owned types that both plugins delegate up for, or exchange neutral payloads such as strings, maps or serialized bytes. Isolation only works if the isolated types stay isolated.

saying these in an interview costs you the question

  • Concluding the JVM or the compiler is buggy because the names match
  • Assuming the two sides are the same class because getName() is equal
  • Fixing it by adding the library jar in more places rather than fewer
  • Working around it with reflection or by comparing class names as strings
  • Randomly changing the thread context class loader without deciding who owns the type

context