skip to content

Class Identity & Uniqueness

A type's runtime identity is the pair of its fully-qualified name and its defining class loader, not the name alone. Interviewers reach for this to explain the classic "X cannot be cast to X" error, and it is the mechanism behind every loader-based isolation scheme.

on this pageshow

questions

5

Inside one running JVM, is a fully-qualified name such as com.acme.Config enough to identify a type uniquely? Explain what actually determines a class's runtime identity.

level: middleimportance: must knowfreq 45%

answer

  1. identity = (binary name, defining loader)
  2. initiating loader ≠ defining loader
  3. two definitions → two Class objects, two sets of statics
  4. isAssignableFrom false both ways
  5. loader graph = namespace mechanism

basics

~20 s

No. A runtime type is identified by the pair (fully-qualified name, defining class loader). Load the same class file with two different loaders and you get two distinct, non-interchangeable types, each with its own Class object.

solid answer

~60 s

The JVM's unit of type identity is the **run-time type**, defined as the pair *(binary name, defining class loader)*. The name alone is not unique inside a single JVM. The *defining* loader is the loader that actually called `defineClass` for those bytes — not necessarily the one you asked. If a loader delegates to its parent and the parent defines the class, the parent is the defining loader; the child is merely an *initiating* loader. Delegation exists precisely so that shared types resolve to one defining loader and stay identical everywhere. When two different loaders each define the same bytes, the results are two separate types: - two distinct `Class` objects, so `c1 == c2` is false and `c1.equals(c2)` is false; - two separate sets of static fields, so any "singleton" exists twice; - no assignability between them: `isAssignableFrom` is false and a cast throws `ClassCastException` even though the printed names match. This is not a bug; it is the mechanism that makes loader-based isolation possible — containers, plugin systems and OSGi rely on it to let independent components carry conflicting library versions.

code

java · 7 lines
java
Class<?> a = loaderOne.loadClass("com.acme.Config");
Class<?> b = loaderTwo.loadClass("com.acme.Config");

a.getName().equals(b.getName());   // true  — names are identical
a == b;                            // false — different run-time types
a.isAssignableFrom(b);             // false — not interchangeable
a.getClassLoader();                // loaderOne (the defining loader)

go deeper

for a junior

Recall the rule: a type is (name + defining class loader), so the same class file loaded twice by different loaders yields two unrelated types.

for a middle

Distinguish initiating from defining loader, and list the consequences: separate Class objects, separate statics, no assignability.

for a senior

Explain why the design enables container and plugin isolation, and connect it to class unloading being tied to loader reachability.

for a principal

Reason about it as a namespace design decision — the loader graph is the process's type namespace, so what is shared versus duplicated is an architectural choice with versioning and lifecycle consequences.

## The definition The JVM specification says a class or interface is determined by its **binary name together with its defining class loader**. Two loaded types with the same name but different defining loaders are *different types*, with no more relationship to each other than to any unrelated class. It helps to distinguish two roles a loader can play for a given class: - **Initiating loader** — the loader you asked to load the class. `loadClass("com.acme.Config")` was called on it. - **Defining loader** — the loader that ultimately turned bytes into a class by calling `defineClass`. This is the loader recorded as part of the type's identity and returned by `Config.class.getClassLoader()`. Because loaders normally ask their parent first, the same class typically ends up **defined** once, high in the hierarchy, and *initiated* by many loaders below. Everyone then sees the same type. That single defining event is what makes types shareable at all: `java.lang.String` has exactly one definition (by the bootstrap loader) in a normal JVM, so every component agrees on what a `String` is. ## What changes when two loaders both define the class Suppose two loaders each read the same `com/acme/Config.class` bytes and each define it. Now there are two run-time types. Everything that is per-type is duplicated: 1. **The `Class` object.** `Class` instances are compared by reference; two definitions means two objects. `a.getClass() == b.getClass()` is false, and `Class.equals` is inherited identity equality, so that is false too. Comparing `getName()` returns equal strings — which is exactly why the situation is confusing. 2. **Static state.** Static fields belong to a type, so each definition has its own copy, and each is initialized by its own run of `<clinit>`. A class implementing a singleton, a registry, a cache, or a static counter now has two independent instances of that state. Frameworks that assume "static means global in this process" break here. 3. **Assignability.** `typeA.isAssignableFrom(typeB)` is false in both directions. A reference to one cannot be stored in a variable declared as the other, and the resulting failure is a `ClassCastException` naming the same class on both sides. 4. **Instances.** An object created from one definition is an instance of that type only; `instanceof` against the other definition is false. Note what is *not* duplicated: identity is per-type, so this has nothing to do with `equals`/`hashCode` you wrote — value equality between two objects of the two types is not even reachable, because you cannot pass one where the other is expected. ## Why the design is this way If name alone determined identity, a JVM could hold exactly one version of any given class, and two libraries needing incompatible versions of a third could never coexist in one process. Making the defining loader part of identity turns the loader graph into a **namespace mechanism**: each loader defines its own private namespace of types, and only what it delegates comes from outside. That is the foundation for: - application servers isolating deployed applications from each other; - plugin frameworks giving each plugin its own dependency set; - module systems such as OSGi wiring exports and imports explicitly; - test harnesses and hot-reload tooling loading a fresh definition of changed classes while the old ones are still referenced. It also has a lifecycle consequence: a class can only be unloaded together with its defining loader, when the loader, all its defined classes and all their instances are unreachable. Isolation and reloadability come from the same property. ## Making it visible The practical habit is to never trust the printed name alone. When two types "should" be the same, print identity: ```java System.out.println(obj.getClass().getName() + " @" + Integer.toHexString(System.identityHashCode(obj.getClass())) + " loader=" + obj.getClass().getClassLoader()); ``` Different loader instances (or different identity hashes for the `Class` objects) prove you have two types. `-verbose:class` shows each definition and the source it came from, which usually reveals the duplicate jar or the extra loader. ## The rule to remember A type's full name at run time is not `com.acme.Config`. It is `com.acme.Config` **as defined by** *this* loader. Any reasoning about "the same class" in a container, plugin host, or app server must carry that second half.

  • If a child loader delegates to its parent and the parent loads the class, which loader is part of the type's identity?
    The parent, because it called defineClass and is therefore the defining loader; the child is only an initiating loader. This is why delegation produces shared, identical types: many loaders initiate, one defines. getClassLoader() on the resulting class returns the parent.
  • What happens to static fields of a class loaded by two different loaders?
    Each definition has its own copy of every static field and runs its own static initializer. A singleton, cache or registry held in statics therefore exists twice, independently. Code that assumes static state is process-global will behave as if it were running in two separate applications.

Two people can both be named 'John Smith'; the passport number is the country plus the name. In the JVM the country is the defining class loader.

saying these in an interview costs you the question

  • Saying a fully-qualified name uniquely identifies a class within a JVM
  • Assuming getClassLoader() returns whichever loader you called loadClass on
  • Thinking two Class objects with equal getName() are equal
  • Believing static fields are always process-wide singletons
  • Treating duplicate definitions as a JVM bug rather than the isolation mechanism

context

open as a page

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%

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.

open as a page

If the same class file is defined by two different class loaders inside one JVM, what happens to that class's static fields, its Class object, and any singleton instance it holds?

level: middleimportance: should knowfreq 34%

basics

~20 s

Everything per-type is duplicated: two Class objects, two independent sets of static fields, two runs of the static initializer, and therefore two singletons. Neither side can see or affect the other's state, and objects from one are not instances of the other.

open as a page

You are designing a plugin system where each plugin ships its own dependency jars and different plugins may require incompatible versions of the same library. How does giving each plugin its own class loader deliver that isolation, and what must remain shared for the host and plugins to communicate at all?

level: principalimportance: should knowfreq 33%

basics

~20 s

Because a type is (name + defining loader), each plugin loader defines its own private copy of its dependencies, so conflicting versions coexist without clashing. But every type crossing the boundary — the plugin API interfaces and any exchanged data types — must be defined once by a shared ancestor loader that all plugins delegate to.

open as a page

Two classes both declare the package com.acme.util, but they are defined by different class loaders. Can one access the other's package-private members? Explain what the JVM treats as 'the same package' at run time.

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

No. The JVM's run-time package is the pair (package name, defining class loader), so same-named packages in different loaders are different packages. Package-private access across them fails with IllegalAccessError, even though the source-level package names match.

open as a page