When writing a custom java.lang.ClassLoader subclass, why is overriding findClass(String) the recommended approach while overriding loadClass(String) is discouraged? What changes behaviourally if you override loadClass?
answer
- loadClass = policy (delegation); findClass = mechanism (bytes)
- New source → findClass; new order → loadClass
- findClass is called only after all ancestors fail
- Overriding loadClass: keep findLoadedClass, java.* delegation, per-name lock
- Child-first price: duplicate types, ClassCastException at boundaries
basics
~20 sfindClass is the hook the inherited delegation algorithm calls only after every ancestor has failed, so overriding it adds a new source of bytes while preserving parent-first order. loadClass is the delegation algorithm, so overriding it replaces the policy — usually turning the loader child-first, with duplicate definitions as the risk.
solid answer
~60 s`ClassLoader.loadClass` implements the policy: check already-loaded, delegate to the parent, and only on failure call `findClass`. `findClass` implements the mechanism: fetch bytes for this name from wherever this loader gets them, and call `defineClass`. **Override `findClass`** to teach a loader a *new place to look* — a network location, an encrypted archive, generated bytecode, a database. You inherit the correct delegation, the per-name locking, and the already-loaded check for free. This is the documented, intended extension point. **Override `loadClass`** only when you intend to change the *policy itself* — for example searching locally before the parent so a component can pin its own version of a library. That is a real, legitimate need in containers and plugin systems, but you take on responsibility for everything the inherited method did: still checking `findLoadedClass` first, still delegating core JDK packages upward unconditionally, still acquiring `getClassLoadingLock(name)`, and accepting that any type you now define locally is a *different* type from the parent's version of it, with `ClassCastException` at boundaries as the consequence. Rule of thumb: new source of classes, override `findClass`; new search order, override `loadClass` and know exactly what you are trading.
code
java · 17 linespublic class BlobClassLoader extends ClassLoader {
static { registerAsParallelCapable(); }
private final BlobStore store;
public BlobClassLoader(BlobStore store, ClassLoader parent) {
super(parent);
this.store = store;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = store.read(name.replace('.', '/') + ".class");
if (b == null) throw new ClassNotFoundException(name); // never return null
return defineClass(name, b, 0, b.length);
}
}go deeper
Know that findClass is the method you are meant to override to supply bytes, and that loadClass contains the delegation logic you should leave alone.
Explain the policy-versus-mechanism split, what you inherit for free (order, already-loaded check, locking), and the ClassNotFoundException contract.
Show you know when overriding loadClass is legitimate and exactly what obligations come with it — shared-package allow-list, per-name locking, duplicate-type consequences.
Frame it as an isolation-versus-sharing boundary decision: which packages must remain single-definition contracts and which may be duplicated per component, documented deliberately rather than discovered through a cast error.
## Two methods, two different jobs The `ClassLoader` API deliberately separates *policy* from *mechanism*: - **`loadClass(String, boolean)` is the policy.** It is the parent-delegation algorithm: consult `findLoadedClass`, delegate to the parent, fall back to `findClass`, optionally resolve. Its behaviour is the contract every other loader in the process assumes. - **`findClass(String)` is the mechanism.** Its only job is: given a binary name, produce the bytes for that class and turn them into a `Class` via `defineClass`. The base implementation throws `ClassNotFoundException`, because the base class has nowhere to look. The API is designed so that the overwhelmingly common case — "I want classes to come from somewhere unusual" — is served by overriding only the mechanism. ## The recommended shape ```java public class BlobClassLoader extends ClassLoader { static { registerAsParallelCapable(); } private final BlobStore store; public BlobClassLoader(BlobStore store, ClassLoader parent) { super(parent); // wire the parent explicitly this.store = store; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = store.read(name.replace('.', '/') + ".class"); if (bytes == null) { throw new ClassNotFoundException(name); // required contract } return defineClass(name, bytes, 0, bytes.length); } } ``` What you get by not touching `loadClass`: - **Correct order.** Core JDK and parent-visible types keep resolving to the parent's definitions, so instances flow across the boundary without cast errors. - **Already-loaded check.** You never accidentally define the same name twice within this loader — which would throw `LinkageError: attempted duplicate class definition`. - **Per-name locking.** `getClassLoadingLock(name)` plus `registerAsParallelCapable()` give concurrent loading of different names without a loader-wide bottleneck. - **Correct failure semantics.** Throwing `ClassNotFoundException` from `findClass` is how you say "not here"; the caller above handles it. Note the two easy mistakes even in this small class: forgetting `registerAsParallelCapable()` (which downgrades you to a single lock per loader), and returning `null` instead of throwing `ClassNotFoundException` when the bytes are missing, which produces a confusing `NullPointerException` far from the cause. ## When overriding loadClass is legitimate The honest answer is not "never." Servlet containers, OSGi frameworks, application servers and plugin systems override `loadClass` on purpose, because they *want* a component's own bundled library version to win over the container's. There is no way to express that by overriding `findClass`, since `findClass` is by construction consulted last. If you do it, you inherit obligations: 1. **Always check `findLoadedClass(name)` first.** Skipping it risks duplicate definition errors. 2. **Always delegate core packages upward unconditionally.** At minimum `java.*`; in practice a configurable list of package prefixes that must stay shared (the platform APIs, and any package that forms the contract between container and component). Attempting `java.*` locally will fail anyway at `defineClass` with a prohibited-package `SecurityException`, but failing early and clearly is better. 3. **Hold `getClassLoadingLock(name)`** and register the loader as parallel-capable; hand-rolled `synchronized` on the loader reintroduces the deadlock the per-name locks were designed to avoid, especially when two loaders can delegate to each other. 4. **Understand what you have chosen.** Any class you now define locally that the parent could also have defined becomes a *second* runtime type with the same name. Passing instances across the boundary yields `ClassCastException`; static state exists twice; and the duplicated classes are pinned in memory as long as the loader is reachable. ```java @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null) { if (isSharedPackage(name)) { // java.*, container contract API c = super.loadClass(name, false); // parent-first for these } else { try { c = findClass(name); // child-first for everything else } catch (ClassNotFoundException e) { c = super.loadClass(name, false); } } } if (resolve) resolveClass(c); return c; } } ``` That is roughly the structure every child-first container converges on: a shared-package allow-list plus local-then-parent for the rest. ## The decision rule Ask what you are changing: - *Where do the bytes come from?* → override `findClass`. Delegation policy untouched. - *Who gets asked first?* → override `loadClass`, keep the shared-package list, keep the locking, and accept duplicate types as the deliberate price of isolation. If you cannot articulate why the second is required, the first is the answer. ## What interviewers are checking That you know `loadClass` contains the delegation algorithm and `findClass` is the hook it calls last; that overriding the wrong one silently changes the loader's contract with the rest of the process; and that you can name the concrete failure mode — the same-name-different-loader `ClassCastException` — rather than just saying "it breaks delegation."
- If you do override loadClass, what must you still do that the inherited implementation did for you?Check findLoadedClass(name) first so you never attempt a duplicate definition; delegate core packages — at minimum java.* — to the parent unconditionally, plus whatever packages form the shared contract with the parent; and hold getClassLoadingLock(name) rather than synchronizing on the loader, after calling registerAsParallelCapable in a static initialiser. Skipping any of these produces duplicate-definition LinkageErrors, prohibited-package SecurityExceptions, or loader deadlocks under concurrent loading.
- What should findClass do when it cannot find the bytes for a name?Throw ClassNotFoundException with the name. That is the contract the delegation algorithm depends on — 'not here' is normal control flow that the caller above catches and handles. Returning null instead produces a NullPointerException at a point far from the real cause, and swallowing the exception to return some other class breaks the identity guarantees the rest of the process assumes.
saying these in an interview costs you the question
- Believing findClass is called before the parent is consulted — it is the last resort, not the first
- Overriding loadClass by default for any custom loader, without needing a policy change
- Returning null from findClass instead of throwing ClassNotFoundException
- Overriding loadClass and forgetting findLoadedClass, then hitting duplicate class definition LinkageError
- Synchronizing on the loader instance instead of using getClassLoadingLock plus registerAsParallelCapable