Serializing complex objects securely, how Java handles untrusted code now that the Security Manager is gone, Strategy vs State, Observer in event-driven apps, plugins with class loaders, Class.forName vs loadClass, the class-loader hierarchy since Java 9, circular constructor dependencies, an O(1) LRU cache, LinkedHashSet vs TreeSet, final fields and reflection, enum singletons and strategies, Externalizable, and coding standards.
Published September 25, 2026
This lesson mixes JVM internals with design idioms. Several popular answers are outdated: the Security Manager, rt.jar, "the Extension class loader". Giving the Java 17/21 reality marks you as current. Where a question invites it, finish with the safer, modern alternative.
Short answer:
transient, or better, omit them from DTOs. Validate on read (compact constructors, Bean Validation).serialVersionUID;writeReplace/readResolve), so invariants are re-checked through the constructor;readObject;ObjectInputFilter allow-list;ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.acme.orders.*;java.base/*;!*"); // allow only known classes
try (var in = new ObjectInputStream(bytes)) {
in.setObjectInputFilter(filter);
Order order = (Order) in.readObject();
}
Learn it in depth → Exception Design & Serialization Edge Cases
Short answer: Historically (applets, RMI), the Security Manager, with a policy file, sandboxed untrusted code through permission checks on file, network and reflection access. It was deprecated for removal in Java 17 (JEP 411), and permanently disabled in Java 24 (JEP 486). Applets are gone too.
The layers that remain are:
Today, isolation comes from outside the JVM: separate processes or containers, restricted OS users, seccomp, network policies. Don't load untrusted code into your JVM. If you must run user code, use a sandboxed process or WebAssembly.
Short answer: Both delegate behaviour to an interchangeable object, but they differ in who chooses it and why:
PLACED → PAID → SHIPPED, and cancel() behaves differently in each state. It replaces big switch-on-status blocks.Learn it in depth → State Pattern
Short answer:
OrderPlaced), and let independent listeners react (@EventListener, or @TransactionalEventListener + @Async). The publisher doesn't know its subscribers.Design points:
Learn it in depth → Observer Pattern
Short answer:
URLClassLoader, with the application's API loader as the parent, for isolation.ServiceLoader (entries in META-INF/services), instead of hard-coded class names.close() the loader.URL[] urls = { pluginJar.toUri().toURL() };
URLClassLoader loader = new URLClassLoader("plugin-" + name, urls, PaymentPlugin.class.getClassLoader());
ServiceLoader<PaymentPlugin> plugins = ServiceLoader.load(PaymentPlugin.class, loader);
plugins.findFirst().ifPresent(registry::register);
Key points to cover:
ThreadLocals from the plugin), version conflicts between plugins (hence the isolation), and security, because plugins run with full privileges. Load only trusted, signed plugins. Frameworks: PF4J, OSGi, or the JPMS ModuleLayer.Class.forName() and ClassLoader.loadClass()?Short answer:
Class.forName(name) loads, links and initialises the class (static initialisers run), using the caller's class loader by default. Class.forName(name, initialize, loader) gives you control over both.loader.loadClass(name) loads the class, but doesn't initialise it. Initialisation happens on first active use.Key points to cover:
Class.forName("com.mysql.cj.jdbc.Driver") precisely for its initialisation side effect: the driver registered itself. Since JDBC 4, drivers are found automatically through ServiceLoader.Short answer (Java 9+):
java.base…), and appears as null.Custom loaders sit below these (app servers per web application, OSGi, Spring Boot's LaunchedClassLoader, DevTools' restart loader). Loading follows parent delegation.
Common trap: describing rt.jar and the jre/lib/ext extension directory. Both were removed in Java 9 by the module system.
Learn it in depth → JVM, Memory & Class Loading
ClassA and ClassB each need the other in their constructors. How do you resolve the circular dependency?Short answer: First, treat it as a design smell. Two classes that can't exist without each other usually share a responsibility that belongs in a third class, or one of them only needs to notify the other (use events or callbacks). If a bidirectional link really is required:
Supplier/provider, resolved lazily.@Lazy or ObjectProvider on one side. Spring Boot 2.6+ forbids circular references by default.final class A { private B b; void attach(B b) { this.b = b; } }
final class B { private final A a; B(A a) { this.a = a; a.attach(this); } } // wiring in one place (or a factory)
LinkedList".)Short answer: A plain LinkedList makes both lookup and move-to-front O(n). The correct O(1) design is a HashMap from key to node, plus a doubly linked list of nodes: the map finds the node, and the list moves it to the head or evicts the tail in O(1). In Java, LinkedHashMap with accessOrder = true implements exactly this.
final class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
LruCache(int capacity) { super(16, 0.75f, true); this.capacity = capacity; } // accessOrder = true
@Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; }
}
Key points to cover:
Learn it in depth → Thread-Safe LRU Cache
LinkedHashSet outperform a TreeSet, and when is it the other way round?Short answer:
LinkedHashSet (a hash table plus a linked list) gives O(1) add, remove and contains, with insertion-order iteration. It wins for fast membership checks and "unique, in arrival order".TreeSet (a red-black tree) gives O(log n) operations with sorted order, plus navigation: first, ceiling, headSet, range views. It wins when you need sorted iteration, range queries, or nearest-element lookups.Key points to cover:
TreeSet uniqueness uses compareTo, not equals.final field with reflection?Short answer:
final fields of ordinary classes, field.setAccessible(true) followed by set(...) succeeds, but the result is unreliable. The JIT may have constant-folded or cached the old value, and other threads may never see the change, because the final-field memory guarantees assume no mutation.static final primitives or Strings with constant initialisers) are inlined at compile time, so readers never see the change.static final fields can't be modified through Field.set (it throws IllegalAccessException).Future JDKs are moving to forbid final-field mutation by default ("integrity by default").
Key points to cover:
MethodHandles with sanctioned access, instead.Short answer:
INSTANCE. It's thread-safe (initialised with the class), serialization-safe, and reflection-proof.public enum ShippingStrategy {
STANDARD { public BigDecimal cost(Order o) { return new BigDecimal("40"); } },
EXPRESS { public BigDecimal cost(Order o) { return new BigDecimal("120"); } },
FREE_OVER_999 { public BigDecimal cost(Order o) {
return o.total().compareTo(new BigDecimal("999")) >= 0 ? BigDecimal.ZERO : new BigDecimal("40"); } };
public abstract BigDecimal cost(Order o);
}
ShippingStrategy chosen = ShippingStrategy.valueOf(request.shippingMode());
Key points to cover:
Externalizable and Serializable?Short answer:
Serializable | Externalizable | |
|---|---|---|
| Control | Automatic (with optional writeObject/readObject hooks) | Full manual control: writeExternal/readExternal |
| Constructor | Not called (the first non-serializable superclass's no-arg constructor runs) | Public no-arg constructor required, called before readExternal |
| Metadata written | Class descriptors plus all non-transient fields | Only what you write (smaller, often faster) |
| Evolution / versioning | serialVersionUID, with default compatibility rules | Entirely your responsibility |
| Risk | Gadget attacks if deserializing untrusted data | The same risks, plus hand-written bugs |
Key points to cover:
Short answer (show that standards are automated and agreed):
Optional for absent return values, no null collections, equals/hashCode contracts, and careful concurrency.Q: What is ServiceLoader?
A: The JDK's built-in plugin discovery mechanism. Providers are declared in META-INF/services/<interface>, or with provides in module-info, and loaded lazily with ServiceLoader.load(Interface.class).
Q: What is the thread context class loader, and why do frameworks use it?
A: A class loader attached to each thread (Thread.getContextClassLoader()). Framework code, loaded by a parent loader, uses it to load application classes and resources that it can't see through its own loader. That's typical in application servers and plugin systems.
Q: Why is reflection slower, and has it improved?
A: Reflection used to bypass JIT optimisations, and needed access checks. Since Java 18 it's implemented on method handles (JEP 416), which performs much better once warmed up. MethodHandles and LambdaMetafactory offer near-direct-call speed for hot paths.
Q: How do you prevent a class from being deserialized at all?
A: Throw InvalidObjectException from readObject / readObjectNoData, or use a serialization proxy that rejects direct deserialization. Better still, don't implement Serializable.