Java Interview Questions & Answers
A curated collection of Java interview questions and answers organized by topic and difficulty level. Questions are drawn from real interview scenarios and cover core Java through expert-level topics.
Difficulty Levels:
- ๐ข Intermediate โ Solid fundamentals expected
- ๐ก Advanced โ Deep understanding required
- ๐ด Expert โ Production experience and system design knowledge
1. Core Java & OOPโ
๐ข What does Java use: pass by value or pass by reference?โ
Java uses pass by value. For primitive types, Java copies the actual value. For objects, Java copies the reference value (the memory address), not the object itself. Changes to the parameter inside a method do not affect the original reference outside the method, but you can modify the object's internal state through the copied reference.
void modify(List<String> list) {
list.add("item"); // Modifies the object โ visible to caller
list = new ArrayList<>(); // Reassigns local reference only โ NOT visible to caller
}
๐ข What is the impact of declaring a method as final on inheritance?โ
Declaring a method as final prevents it from being overridden in any subclass. This ensures the method's behavior remains consistent across the class hierarchy. It's commonly used when a specific algorithm or security-sensitive operation must not be altered by subclasses.
๐ข Can method overloading be determined at runtime?โ
No. Method overloading is resolved at compile-time based on the method signature (name + parameter types). This differs from method overriding, which is resolved at runtime via dynamic dispatch based on the object's actual type.
๐ข What is a marker interface?โ
A marker interface has no methods or fields. It "marks" a class with a certain capability, enabling instanceof checks at runtime. Examples include Serializable and Cloneable.
// Custom marker interface
public interface Transmittable {}
// Usage: only transmit objects that implement Transmittable
if (data instanceof Transmittable) {
transmit(data);
}
๐ข Can you modify a final object reference in Java?โ
You cannot reassign a final reference to a different object. However, the object itself can still be mutated if it's mutable:
final List<String> list = new ArrayList<>();
list.add("item"); // โ
Allowed โ mutating the object
list = new ArrayList<>(); // โ Compile error โ reassigning the reference
๐ก What are inner classes in Java?โ
Inner classes are classes defined within another class. They have access to the outer class's members (even private ones). Types include:
| Type | Description |
|---|---|
| Non-static inner class | Tied to an instance of the outer class |
| Static nested class | Not tied to an outer instance; can have static members |
| Local class | Defined inside a method |
| Anonymous class | Unnamed class defined and instantiated inline |
Non-static inner classes cannot contain static declarations, because they are associated with an instance of the outer class. Static nested classes can.
๐ก What is TypeErasure?โ
Type Erasure is the process by which the Java compiler removes generic type information after compilation. At runtime, List<Integer> and List<String> are both just List. This ensures backward compatibility with pre-generics code but means generic type information is unavailable via reflection.
// At compile time: type-safe
List<String> strings = new ArrayList<>();
// At runtime: type information erased
// Both are just ArrayList
๐ก Why can't we create an array of generic types in Java?โ
Arrays require concrete type information at runtime to enforce type safety (they perform runtime type checks on insertion). Due to type erasure, generic type information is unavailable at runtime, creating a fundamental incompatibility:
// โ Compile error:
// T[] array = new T[10];
// โ
Workaround using Object array with cast:
@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[10];
๐ก How would generics help maintain type safety and reduce code duplication?โ
Generics allow classes, methods, and collections to use type parameters, catching type mismatches at compile time instead of runtime. A single generic implementation works with multiple types, eliminating the need for type-specific duplicates:
// Without generics: separate methods or unsafe casting
Object item = list.get(0);
String s = (String) item; // ClassCastException risk
// With generics: compile-time safety, no duplication
List<String> list = new ArrayList<>();
String s = list.get(0); // Type-safe, no cast needed
๐ด What happens if a final field is changed using reflection?โ
Reflection can bypass compile-time restrictions and modify a final field using field.setAccessible(true). However, this breaks the immutability contract and can lead to unpredictable behavior because the JIT compiler may inline final field values at compile time. This should be avoided in production code.
๐ด How have records and sealed classes impacted OOP?โ
Records (Java 14+) provide a concise way to model immutable data, auto-generating equals(), hashCode(), and toString(), reinforcing encapsulation and immutability.
Sealed classes (Java 15+) restrict which classes can extend them, giving precise control over inheritance hierarchies and enabling exhaustive pattern matching:
// Record: immutable data carrier
record Point(int x, int y) {}
// Sealed class: controlled hierarchy
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
2. Collections & Data Structuresโ
๐ข What are the potential issues with using mutable objects as HashMap keys?โ
If a key object's state changes after insertion, its hashCode() changes, making the entry unreachable in the map โ even though it still exists. This causes data loss and potential memory leaks. Always use immutable objects as map keys.
๐ข What happens if you override only equals() and not hashCode()?โ
The HashMap contract requires that equal objects must have the same hash code. Without consistent hashCode(), the map may store duplicate keys or fail to find existing entries, since it uses hash codes to locate buckets before checking equals().
๐ข What is the difference between HashMap and IdentityHashMap?โ
| Aspect | HashMap | IdentityHashMap |
|---|---|---|
| Key comparison | equals() + hashCode() (logical equality) | == (reference equality) |
| Use case | General-purpose mapping | Identity-based operations (e.g., serialization graphs) |
๐ข How does Collections.sort() work internally?โ
It uses TimSort, a modified merge sort that is stable (preserves equal-element order) and optimized for partially sorted data. It breaks the list into small runs, sorts them with insertion sort, and merges them.
๐ก What causes ConcurrentModificationException and how do you prevent it?โ
It occurs when a collection is structurally modified while being iterated. Prevention strategies:
- Use iterator's
remove()method during iteration - Use concurrent collections (
CopyOnWriteArrayList,ConcurrentHashMap) - Use
removeIf()for conditional removal - Collect items to remove in a separate list, then remove after iteration
// โ Throws ConcurrentModificationException
for (String s : list) {
if (s.isEmpty()) list.remove(s);
}
// โ
Safe removal
list.removeIf(String::isEmpty);
๐ก When would LinkedHashSet outperform TreeSet and vice versa?โ
| Scenario | Best Choice | Reason |
|---|---|---|
| Frequent insertions/lookups | LinkedHashSet | O(1) operations |
| Insertion order preservation | LinkedHashSet | Maintains order by design |
| Sorted element access | TreeSet | Auto-sorted, O(log n) operations |
Range queries (subSet, headSet) | TreeSet | Navigable sorted structure |
๐ก How would you implement an LRU cache?โ
Use a LinkedHashMap with access-order enabled, overriding removeEldestEntry():
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // true = access order
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
๐ก What is the difference between Collections.sort() and Stream.sorted()?โ
| Aspect | Collections.sort() | Stream.sorted() |
|---|---|---|
| Mutability | Mutates the original list | Returns a new sorted stream |
| Style | Imperative | Functional/declarative |
| Chaining | Standalone operation | Chainable in a pipeline |
| Source | Works only on List | Works on any stream source |
๐ด How does ConcurrentHashMap work internally?โ
Java 7: Uses segment-based locking โ the map is divided into segments, each with its own lock, allowing concurrent writes to different segments.
Java 8+: Replaced segments with node-level locking using CAS (Compare-And-Swap) operations and synchronized blocks on individual bins. Read operations are generally lock-free using volatile reads. The structure uses an array of nodes, where each bin can be a linked list or a red-black tree (when a bin exceeds 8 entries).
3. Java 8 Featuresโ
๐ข What is a Functional Interface?โ
An interface with exactly one abstract method. It can have default/static methods. The @FunctionalInterface annotation provides compile-time enforcement:
| Interface | Method | Purpose |
|---|---|---|
Function<T,R> | R apply(T) | Transform T โ R |
Predicate<T> | boolean test(T) | Filter T โ boolean |
Consumer<T> | void accept(T) | Side-effect T โ void |
Supplier<T> | T get() | Factory () โ T |
๐ข What is the difference between map() and flatMap() in Streams?โ
map() transforms each element 1:1. flatMap() transforms each element into a stream and then flattens all resulting streams into one:
// map: List<String> โ Stream of uppercase strings
list.stream().map(String::toUpperCase);
// flatMap: List<List<String>> โ single Stream<String>
listOfLists.stream().flatMap(Collection::stream);
๐ข What is the difference between Optional.of() and Optional.ofNullable()?โ
| Method | Null handling | Use when |
|---|---|---|
Optional.of(value) | Throws NullPointerException if null | Value is guaranteed non-null |
Optional.ofNullable(value) | Returns Optional.empty() if null | Value might be null |
๐ข What is the difference between findFirst() and findAny() in Streams?โ
findFirst() returns the first element in encounter order โ deterministic and useful for sequential streams. findAny() returns any element and is optimized for parallel streams where it can return whichever element is found first across threads.
๐ก What is the difference between peek() and map()?โ
map() transforms elements and returns a new stream of transformed values. peek() is for side effects (like logging) and returns the same stream unmodified. Caution: peek() behavior is unpredictable for purposes other than debugging, since intermediate operations may not execute if there's no terminal operation.
๐ก How does Java 8 handle parallel processing with Streams?โ
parallelStream() or .parallel() splits data into chunks processed concurrently via the ForkJoinPool.commonPool(). The pool typically has Runtime.getRuntime().availableProcessors() - 1 threads. The framework handles data splitting, parallel execution, and result merging automatically.
Caution: Parallel streams are not always faster. Overhead from splitting, thread coordination, and merging can outweigh gains for small datasets or simple operations.
๐ก Can you use this and super in a Lambda expression?โ
Yes, but they refer to the enclosing instance, not the lambda itself (lambdas have no this). this refers to the class where the lambda is defined; super refers to its superclass. This differs from anonymous classes, where this refers to the anonymous class instance.
๐ก What happens if you modify a local variable inside a Lambda?โ
It causes a compile-time error. Local variables accessed from within a lambda must be final or effectively final (not modified after initialization). This ensures thread safety and prevents side effects in functional-style code.
๐ก How do Default Methods in interfaces affect design decisions vs abstract classes?โ
Default methods blur the line between interfaces and abstract classes by allowing interfaces to provide implementations. Key differences remain:
| Feature | Interface (with defaults) | Abstract Class |
|---|---|---|
| Multiple inheritance | โ A class can implement many | โ Single inheritance |
| State (fields) | โ Only constants | โ Instance fields |
| Constructors | โ None | โ Supported |
| Access modifiers | public only (until Java 9) | Any access level |
Choose interfaces for shared behavior across unrelated types. Choose abstract classes when shared state or a common base is needed.
๐ก Can a Lambda throw an exception?โ
Yes. Unchecked exceptions can be thrown freely. Checked exceptions must either be caught within the lambda or the functional interface must declare them. Since standard functional interfaces (e.g., Function, Predicate) don't declare checked exceptions, you need a try-catch inside the lambda or a custom functional interface.
4. Concurrency & Multithreadingโ
๐ข How would you ensure safe access to a shared resource by multiple threads?โ
Use synchronization mechanisms:
synchronizedkeyword on methods or blocksReentrantLockfor advanced locking with fairness and timeouts- Atomic classes (
AtomicInteger,AtomicReference) for lock-free thread safety - Concurrent collections (
ConcurrentHashMap,CopyOnWriteArrayList)
๐ข What is the significance of volatile in Java concurrency?โ
The volatile keyword ensures that reads and writes to a variable go directly to main memory, bypassing CPU caches. This guarantees visibility โ changes by one thread are immediately visible to others. However, volatile does not provide atomicity for compound operations (e.g., count++).
๐ข Can volatile replace synchronized?โ
No. volatile ensures visibility but not mutual exclusion. For compound operations (check-then-act, read-modify-write), you still need synchronized or Lock:
// โ Not thread-safe even with volatile
volatile int count = 0;
count++; // This is read + increment + write (3 operations)
// โ
Thread-safe alternatives
synchronized(this) { count++; }
// or
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
๐ข What are the differences between Runnable and Callable?โ
| Feature | Runnable | Callable<V> |
|---|---|---|
| Method | void run() | V call() |
| Return value | None | Returns a result |
| Checked exceptions | Cannot throw | Can throw |
| Usage with Executor | execute() or submit() | submit() only |
๐ก What is the difference between synchronized and ReentrantLock?โ
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Lock acquisition | Implicit (enter block) | Explicit (lock() / unlock()) |
| Fairness | No control | Configurable fair/unfair |
| Try-lock | Not possible | tryLock(timeout) supported |
| Interruptible | No | lockInterruptibly() supported |
| Multiple conditions | One wait-set per monitor | Multiple Condition objects |
| Automatic release | Yes (on block exit/exception) | Manual (must call unlock() in finally) |
๐ก Can a deadlock occur with a single thread?โ
A single thread can experience a self-deadlock if it tries to recursively acquire a non-reentrant lock it already holds. This is rare and typically a programming error. ReentrantLock and synchronized (which is reentrant) prevent this by design.
๐ก What is the difference between synchronized and concurrent collections?โ
Synchronized collections (e.g., Collections.synchronizedList()) wrap standard collections with a single lock โ only one thread accesses at a time. Concurrent collections (e.g., ConcurrentHashMap, CopyOnWriteArrayList) use fine-grained locking or lock-free algorithms, allowing higher throughput under contention.
๐ก What is CountDownLatch vs CyclicBarrier?โ
| Feature | CountDownLatch | CyclicBarrier |
|---|---|---|
| Reusable | โ One-time use | โ Can be reset |
| Direction | N threads count down, 1+ threads wait | N threads wait for each other |
| Action on completion | None built-in | Optional barrier action |
| Use case | Wait for services to initialize | Multi-phase computation |
๐ก How does the Executor Framework handle task interruption?โ
Tasks check for interruption via Thread.interrupted() or isInterrupted(). Best practices:
- Regularly check interruption status in long-running tasks
- Catch
InterruptedExceptionand clean up resources - Use
Future.cancel(true)to interrupt running tasks - Restore the interrupt flag if catching
InterruptedExceptionwithout terminating
๐ก What is RejectedExecutionHandler in ThreadPoolExecutor?โ
Handles tasks that cannot be executed when the pool and queue are full. Built-in policies:
| Policy | Behavior |
|---|---|
AbortPolicy (default) | Throws RejectedExecutionException |
CallerRunsPolicy | Executes task in the submitting thread |
DiscardPolicy | Silently discards the task |
DiscardOldestPolicy | Discards oldest queued task, retries submission |
๐ด Explain the difference between visibility and atomicity in multithreading.โ
Visibility: Whether changes made by one thread are seen by other threads. Solved by volatile, synchronized, or memory barriers.
Atomicity: Whether an operation completes as an indivisible unit. A volatile long read/write is atomic on 64-bit, but count++ on a volatile int is NOT atomic (it's read + modify + write). Atomic classes or locks are needed for compound operations.
๐ด Explain the internal working of ThreadPoolExecutor.โ
- Task submitted โ If active threads <
corePoolSize, create a new worker thread - Core full โ Place task in the
workQueue(e.g.,LinkedBlockingQueue) - Queue full โ If active threads <
maximumPoolSize, create a new thread - Max reached + queue full โ Invoke
RejectedExecutionHandler - Idle threads exceeding
corePoolSizeare terminated afterkeepAliveTime
States: RUNNING โ SHUTDOWN (no new tasks, complete existing) โ STOP (interrupt all) โ TIDYING โ TERMINATED
๐ด How many threads does a parallel stream use?โ
Parallel streams default to the ForkJoinPool.commonPool(), which has availableProcessors() - 1 threads. You can customize this:
// Custom pool with specific parallelism
ForkJoinPool customPool = new ForkJoinPool(8);
customPool.submit(() ->
list.parallelStream()
.filter(...)
.collect(Collectors.toList())
).get();
๐ด Write the Producer/Consumer problem using wait/notify.โ
class ProducerConsumer {
private final LinkedList<Integer> buffer = new LinkedList<>();
private final int CAPACITY = 5;
private int value = 0;
public void produce() throws InterruptedException {
while (true) {
synchronized (this) {
while (buffer.size() == CAPACITY) {
wait(); // Release lock and wait for space
}
System.out.println("Produced: " + value);
buffer.add(value++);
notify(); // Notify consumer
}
}
}
public void consume() throws InterruptedException {
while (true) {
synchronized (this) {
while (buffer.isEmpty()) {
wait(); // Release lock and wait for items
}
int consumed = buffer.removeFirst();
System.out.println("Consumed: " + consumed);
notify(); // Notify producer
}
}
}
}
Key points:
whileloop (notif) for wait condition โ guards against spurious wakeupswait()releases the monitor lock and suspends the threadnotify()wakes one waiting thread;notifyAll()wakes all
5. Memory Management & JVMโ
๐ข How does Java handle memory leaks?โ
Java's garbage collector automatically reclaims unreachable objects. However, memory leaks still occur when objects are unintentionally retained:
- Static collections holding references indefinitely
- Unclosed resources (streams, connections)
- Listener/callback accumulation
- Inner classes holding outer class references
- ThreadLocal variables not cleaned up
๐ข What is the difference between NoClassDefFoundError and ClassNotFoundException?โ
| Error | When | Cause |
|---|---|---|
ClassNotFoundException | Runtime (Class.forName, ClassLoader) | Class not on classpath |
NoClassDefFoundError | Runtime | Class was available at compile time but not at runtime (e.g., static initializer failure) |
๐ข How does the static keyword affect memory management?โ
Static fields and methods are stored in the Method Area/Metaspace (not per-instance heap memory). They are created when the class is loaded and persist as long as the class is loaded, shared among all instances. This can inadvertently cause memory leaks if static collections grow unboundedly.
๐ก What is Metaspace and how does it differ from PermGen?โ
| Aspect | PermGen (โค Java 7) | Metaspace (Java 8+) |
|---|---|---|
| Location | JVM heap | Native memory |
| Sizing | Fixed (-XX:MaxPermSize) | Dynamic (grows as needed) |
| OOM risk | Frequent (OutOfMemoryError: PermGen) | Rare (uses native memory) |
| Content | Class metadata, string pool, static vars | Class metadata only |
๐ก What are Strong, Weak, Soft, and Phantom References?โ
| Type | GC Behavior | Use Case |
|---|---|---|
| Strong | Never collected while reachable | Normal object references |
| Soft | Collected only when JVM is low on memory | Memory-sensitive caches |
| Weak | Collected at next GC cycle | WeakHashMap, canonicalizing maps |
| Phantom | Enqueued after finalization, before memory reclaim | Resource cleanup tracking |
๐ก How does garbage collection handle circular references?โ
Java's GC uses reachability analysis from GC roots (stack frames, static fields, JNI references), not reference counting. Objects in a circular reference are collected if none of them are reachable from any GC root โ the circular references don't prevent collection.
๐ก What is the difference between Class.forName() and ClassLoader.loadClass()?โ
| Method | Initialization | Use when |
|---|---|---|
Class.forName() | Loads AND initializes (runs static blocks) | Class needs immediate initialization |
ClassLoader.loadClass() | Loads but does NOT initialize | Deferring initialization for performance |
๐ด How would you investigate an OutOfMemoryError?โ
- Check JVM settings:
-Xms,-Xmxheap size configuration - Capture heap dump:
-XX:+HeapDumpOnOutOfMemoryErrororjmap -dump:live,format=b - Analyze with tools: Eclipse MAT, VisualVM, JProfiler
- Review code: Look for unbounded caches, unclosed resources, large collections
- Monitor runtime: Use JConsole/VisualVM to track heap usage patterns over time
- Check Metaspace: If the error mentions Metaspace, investigate class loading leaks
๐ด Explain all garbage collectors up to the latest Java release.โ
| Collector | Threads | Pause | Best For |
|---|---|---|---|
| Serial GC | Single | Stop-the-world | Small apps, single-core |
| Parallel GC | Multi | Stop-the-world | Throughput-focused batch jobs |
| CMS | Concurrent mark + sweep | Short pauses | Legacy responsive apps (deprecated) |
| G1 GC | Concurrent + parallel | Predictable pauses | General purpose (default Java 11+) |
| ZGC | Concurrent | Ultra-low (< 1ms) | Large heaps, latency-critical |
| Shenandoah | Concurrent | Low-latency | Large heaps, similar to ZGC |
Defaults by version:
- Java 8โ10: Parallel GC
- Java 11+: G1 GC
- ZGC and Shenandoah available from Java 15+
๐ด How would you structure code to avoid memory leaks in long-running applications?โ
- Close resources with try-with-resources
- Use weak references for cache objects (
WeakHashMap,SoftReference) - Avoid static references to large or growing collections
- Unregister listeners/callbacks when no longer needed
- Clean up
ThreadLocalvariables (callremove()) - Use connection pooling for database/network resources
- Profile regularly with VisualVM or Java Flight Recorder
๐ด How do you create a high-performance system with minimal GC?โ
- Reduce object creation: prefer primitives over wrapper types
- Object pooling for frequently created/destroyed objects
- Reuse collections with
clear()instead of re-allocating - Use off-heap storage for large datasets (
ByteBuffer.allocateDirect()) - Choose the right GC: ZGC or Shenandoah for low-latency
- Tune JVM:
-Xms=-Xmxto avoid heap resizing, appropriate young/old gen ratios
6. Design Patterns & Best Practicesโ
๐ข What is the Builder Pattern and how does it differ from Factory?โ
| Aspect | Builder Pattern | Factory Pattern |
|---|---|---|
| Purpose | Construct complex objects step by step | Create objects in a single step |
| Control | Fine-grained control over construction | Hides creation logic from client |
| Use case | Many optional parameters | Choosing between related types |
// Builder pattern
User user = User.builder()
.name("Alice")
.age(30)
.build();
๐ก What is the difference between Strategy and State patterns?โ
Both use composition and polymorphism, but serve different purposes:
| Aspect | Strategy | State |
|---|---|---|
| Purpose | Select an algorithm at runtime | Change behavior based on internal state |
| Trigger | External (client chooses strategy) | Internal (state transitions) |
| Awareness | Strategies are independent | States know about transitions |
๐ก How would you apply the Observer pattern in an event-driven application?โ
Observers (listeners) register with a subject (event source). When the subject triggers an event, all registered observers are notified and react accordingly. This decouples event sources from response logic:
// Subject
interface EventPublisher {
void subscribe(EventListener listener);
void unsubscribe(EventListener listener);
void publish(Event event);
}
// Observer
interface EventListener {
void onEvent(Event event);
}
๐ก How can you break a Singleton? How do you prevent it?โ
| Attack Vector | Prevention |
|---|---|
| Reflection | Throw exception in constructor if instance exists |
| Serialization | Implement readResolve() returning the singleton |
| Cloning | Override clone() to throw CloneNotSupportedException |
| Multiple classloaders | Use enum-based singleton |
Best approach: Use an enum singleton, which prevents all of the above by design:
public enum ConfigManager {
INSTANCE;
public String getConfig(String key) { /* ... */ }
}
๐ก Implement a thread-safe Singleton without synchronized.โ
Bill Pugh Singleton โ leverages the classloader mechanism for lazy, thread-safe initialization:
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
The inner class Holder is loaded only when getInstance() is first called, and the JVM guarantees class loading is thread-safe.
๐ด How would you implement Singleton and Strategy patterns using enums?โ
// Singleton via enum
public enum AppConfig {
INSTANCE;
private final Properties props = new Properties();
public String get(String key) { return props.getProperty(key); }
}
// Strategy via enum
public enum SortStrategy {
QUICKSORT {
@Override public <T> void sort(List<T> list, Comparator<T> c) { /* quicksort impl */ }
},
MERGESORT {
@Override public <T> void sort(List<T> list, Comparator<T> c) { /* mergesort impl */ }
};
public abstract <T> void sort(List<T> list, Comparator<T> c);
}
// Usage: SortStrategy.QUICKSORT.sort(myList, comparator);
7. Serialization & Class Loadingโ
๐ข Can you serialize static fields in Java?โ
No. Serialization captures the object's instance state. Static fields belong to the class, not individual objects, and are excluded from serialization.
๐ข What happens if a Serializable class contains a non-serializable member?โ
A NotSerializableException is thrown during serialization. Solutions:
- Mark the field as
transient(excluded from serialization) - Make the member class implement
Serializable - Provide custom
writeObject()/readObject()methods
๐ก What are the differences between Externalizable and Serializable?โ
| Feature | Serializable | Externalizable |
|---|---|---|
| Implementation effort | None (marker interface) | Must implement writeExternal()/readExternal() |
| Control | Default mechanism | Complete control over serialization |
| Performance | Can be slower (serializes everything) | Can be faster (selective serialization) |
transient fields | Supported | Not needed (you control what's written) |
๐ก What are the different types of class loaders in Java?โ
- Bootstrap ClassLoader โ Loads core Java classes (
java.lang,java.utilfromrt.jar) - Extension/Platform ClassLoader โ Loads extension classes from
lib/ext - Application/System ClassLoader โ Loads classes from application classpath
They follow the parent delegation model: each loader delegates to its parent first, only loading the class itself if the parent cannot.
๐ด What are dynamic proxies in Java?โ
Dynamic proxies create proxy instances for interfaces at runtime, without explicit class definitions. They intercept method calls via an InvocationHandler:
interface UserService {
User findById(long id);
}
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class},
(proxyObj, method, args) -> {
System.out.println("Calling: " + method.getName());
// Delegate to real implementation, add logging/caching/etc.
return realService.findById((long) args[0]);
}
);
Used extensively in frameworks for AOP, transaction management, and lazy loading.
๐ด What is a hidden class (Java 15+)?โ
A hidden class is a non-discoverable, dynamically created class that cannot be found by name or via reflection. It's used by frameworks for runtime-generated classes (like lambda expressions and proxy classes). Benefits:
- Cannot pollute the application classpath
- Reduces classloader memory leaks
- Can be unloaded independently when no longer needed
8. Exception Handlingโ
๐ข What happens when an exception is thrown in a static initialization block?โ
It wraps the exception in ExceptionInInitializerError, and subsequent attempts to use the class throw NoClassDefFoundError because the class failed to initialize.
๐ข When would you use a checked exception over an unchecked one?โ
Use checked exceptions for recoverable conditions that the caller should handle (e.g., IOException, SQLException). Use unchecked exceptions for programming errors (e.g., NullPointerException, IllegalArgumentException).
๐ก Why is catching Throwable considered bad practice?โ
Throwable is the superclass of both Exception and Error. Catching it intercepts JVM-level errors like OutOfMemoryError and StackOverflowError, which typically indicate unrecoverable conditions. Handling these errors can mask critical problems and lead to system instability.
๐ก What unexpected behavior can the finally block cause?โ
If a new exception is thrown in finally, it replaces the original exception from the try block, causing it to be lost:
try {
throw new RuntimeException("Original");
} finally {
throw new RuntimeException("From finally"); // Original exception is lost!
}
// Only "From finally" propagates
Best practice: Use try-with-resources instead of manual finally blocks for resource cleanup.
9. Modern Java Features (Java 9+)โ
๐ก How does the module system (Java 9) impact application architecture?โ
The Java Platform Module System (JPMS) enables:
- Explicit dependencies via
module-info.java - Strong encapsulation โ internal packages are hidden by default
- Reduced memory footprint โ load only required modules
- Improved security โ no access to unexported internals
module com.myapp.core {
requires java.sql;
exports com.myapp.core.api; // Public API
// com.myapp.core.internal is hidden
}
๐ก What is the purpose of @Retention and @Target annotations?โ
@Retention controls how long an annotation is available:
| Value | Available at |
|---|---|
SOURCE | Source code only (discarded by compiler) |
CLASS | Compiled bytecode (not available via reflection) |
RUNTIME | Runtime (accessible via reflection) |
@Target restricts where an annotation can be applied: METHOD, FIELD, TYPE, CONSTRUCTOR, PARAMETER, etc.
๐ก What is a record in Java (14+)?โ
A concise declaration for immutable data carriers that auto-generates equals(), hashCode(), toString(), and accessor methods:
record Point(int x, int y) {}
// Equivalent to a class with:
// - final fields x, y
// - Constructor Point(int x, int y)
// - Accessors x(), y()
// - equals(), hashCode(), toString()
๐ก What is a sealed class (Java 15+)?โ
A sealed class restricts which classes can extend it, enabling exhaustive type hierarchies:
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
final class Triangle implements Shape { /* ... */ }
Benefits: Type safety, exhaustive pattern matching, controlled extensibility.
๐ด How does the Java Reflection API work, and what are its use cases?โ
Reflection allows runtime inspection and modification of classes, methods, and fields. The API can:
- Create instances dynamically
- Invoke methods by name
- Access and modify private fields
- Inspect annotations
Use cases: Dependency injection (Spring), ORM mapping (Hibernate), test frameworks (JUnit), serialization
Caution: Reflection bypasses access control, has performance overhead, and can break encapsulation. Use sparingly and prefer compile-time alternatives when possible.
10. Performance & Troubleshootingโ
๐ก What tools and techniques identify memory leaks?โ
| Tool | Purpose |
|---|---|
| VisualVM | Real-time heap monitoring, thread analysis |
| Eclipse MAT | Heap dump analysis, dominator tree, leak suspects |
| JProfiler / YourKit | CPU + memory profiling |
| Java Flight Recorder | Low-overhead production profiling |
| jmap | Heap dump generation |
| jstat | GC statistics monitoring |
๐ก What are the disadvantages of JIT compilation?โ
- Higher memory usage during compilation of bytecode to native code
- Increased CPU load during initial execution (warm-up phase)
- Startup overhead โ short-lived applications may terminate before JIT benefits kick in
Consider disabling JIT (-Djava.compiler=NONE) for development/debugging or use AOT compilation (GraalVM Native Image) for fast-startup scenarios.
๐ด How would you improve scalability and memory efficiency of a large Java application?โ
- Efficient data structures โ choose the right collection for access patterns
- Object pooling โ reuse expensive objects (connections, threads)
- Caching โ Redis/Caffeine for frequently accessed data
- Lazy initialization โ create objects only when needed
- Connection pooling โ HikariCP for database connections
- JVM tuning โ heap size, GC selection, Metaspace config
- Horizontal scaling โ distribute load across instances
- Remove unnecessary references โ prevent memory leaks
- Profile continuously โ identify hotspots with JFR/async-profiler
๐ด What performance optimizations have you applied in Java projects?โ
Common optimization strategies:
- Caching (Redis, Caffeine) to reduce database calls
- Query optimization โ proper indexing, batch operations
- Appropriate data structures โ
ConcurrentHashMapoversynchronized HashMap - Connection pooling (HikariCP) for database access
- JVM tuning โ GC selection, heap sizing
- Lazy loading โ defer expensive initialization
- Async processing โ
CompletableFuturefor non-blocking operations
11. Senior Expert Questionsโ
๐ด Explain how Stream pipelines execute lazily. What does "element-by-element" mean?โ
Stream operations are not executed when chained โ only when a terminal operation triggers them. When triggered, each element passes through the entire pipeline before the next element is processed:
// Interleaved execution order (NOT filter-all then map-all):
// filter: Alice โ map: Alice โ filter: Bob (filtered out) โ filter: Charlie โ map: Charlie
names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
This enables short-circuiting: findFirst() stops processing as soon as a match is found โ the remaining elements are never evaluated.
๐ด What are the traps when using parallel streams?โ
// Trap 1: Non-thread-safe accumulation
List<String> result = new ArrayList<>();
list.parallelStream().forEach(result::add); // โ Race condition on ArrayList
// Trap 2: forEach ordering is not guaranteed
list.parallelStream().forEach(System.out::println); // โ Random order
// Trap 3: Blocking operations in ForkJoinPool.commonPool()
list.parallelStream().map(this::callDatabase).collect(...); // โ Starves commonPool
// โ
Correct: use collectors (thread-safe merge) and custom pool
ForkJoinPool pool = new ForkJoinPool(4);
pool.submit(() -> list.parallelStream().map(this::heavyTransform).collect(Collectors.toList())).get();
LinkedList and TreeMap split poorly for parallel streams because they lack the SIZED Spliterator characteristic โ prefer ArrayList, arrays, or IntStream.range().
๐ด How does StructuredTaskScope fix the problems with CompletableFuture.allOf()?โ
CompletableFuture.allOf() has two fundamental problems:
- If one future fails, the others keep running and consuming resources โ no automatic cancellation
- Threads can outlive their logical scope, making error tracking and cancellation complex
StructuredTaskScope enforces that all subtasks finish (normally or cancelled) before the scope exits:
// With allOf: both fetches run even if one fails
CompletableFuture.allOf(fetchUser(), fetchOrders()).join();
// With StructuredTaskScope: failure cancels all remaining work
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var user = scope.fork(this::fetchUser);
var orders = scope.fork(this::fetchOrders);
scope.join().throwIfFailed();
return new Dashboard(user.get(), orders.get());
} // cancelled subtasks guaranteed to finish here
๐ด Why does JIT deoptimization cause latency spikes in production?โ
JIT compiles methods based on observed runtime behavior. If it assumes a virtual method is monomorphic (one concrete type) and inlines it aggressively โ then a new type appears โ it must deoptimize: throw away the compiled version and fall back to interpreter.
This can happen when:
- A rarely-used code path introduces a new type for an inlined call site
- A cache miss causes previously-cold code paths to execute
- Kubernetes pods receive production traffic before JVM warm-up completes
Detection: -XX:+PrintCompilation shows deoptimization events. Async-profiler can pinpoint which methods deoptimize frequently.
๐ด What is a G1 "humongous" object and why is it a GC problem?โ
Objects larger than 50% of a G1 region (default region = 1โ32 MB) are humongous objects. They:
- Are allocated directly in Old gen (skipping Young gen)
- Occupy multiple contiguous regions
- Are only collected during a full GC (or when the region becomes empty)
Frequent large object allocation (e.g., large JSON payloads, big byte arrays) can fill Old gen with humongous regions that GC cannot efficiently reclaim, causing eventual Full GC stop-the-world pauses.
Fix: Increase region size (-XX:G1HeapRegionSize=32m) or reduce object sizes via streaming/chunking.
๐ด When would you use ScopedValue over ThreadLocal?โ
ThreadLocal pitfalls in virtual thread / structured concurrency world:
- Thread pool reuse: a virtual thread might be rescheduled on a different carrier thread after an I/O yield, leaving stale
ThreadLocalvalues - Memory leaks: forgetting to call
remove()in large thread pools - No bounded lifetime: values persist for the thread's entire lifecycle
ScopedValue advantages:
// ThreadLocal: survives beyond its logical scope
static final ThreadLocal<User> USER = new ThreadLocal<>();
USER.set(authenticatedUser);
asyncTask(); // forked task inherits stale user โ dangerous
// ScopedValue: bounded, immutable, safe with virtual threads
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> {
asyncTask(); // only accessible within this scope
});
Use ScopedValue for request-scoped context (auth tokens, trace IDs) in Java 21+ applications using virtual threads.
๐ด Why does HashMap use power-of-2 capacity and a 0.75 load factor?โ
Power-of-2 capacity: Enables index = hash & (capacity - 1) โ a fast bitwise AND instead of slow modulo division. When the capacity is a power of 2, capacity - 1 is a bitmask of all 1s in the lower bits.
Load factor 0.75: Empirically balances:
- Too low (e.g., 0.5): Wastes memory, more rehashing, better lookup performance
- Too high (e.g., 0.9): Less memory waste, but longer chains โ worse lookup O(n) performance
At 0.75, a map of capacity 16 rehashes at 12 entries, giving a good mix of time and space efficiency.
๐ด How can a custom ClassLoader cause a ClassCastException at runtime even when types look identical?โ
Each ClassLoader has its own namespace. If ClassA is loaded by two different ClassLoaders, the JVM treats them as two different classes โ even if the bytecode is identical.
ClassLoader1.loadClass("com.example.User") โ User (version A)
ClassLoader2.loadClass("com.example.User") โ User (version B)
// These are NOT the same class to the JVM!
User u = (User) obj; // ClassCastException if obj was loaded by different loader
This is the root cause of many ClassCastException bugs in Tomcat, OSGi, and hot-reload frameworks where multiple ClassLoaders exist. The fix is ensuring both sides of a cast use the same ClassLoader.
๐ด What is the difference between thenApply, thenCompose, and handle in CompletableFuture?โ
| Method | When it runs | Transform type | Use case |
|---|---|---|---|
thenApply(f) | On success | T โ U (synchronous) | Simple value transform |
thenCompose(f) | On success | T โ CompletableFuture<U> | Chain another async call |
handle(f) | Always (success + failure) | (T, Throwable) โ U | Recovery + transform in one |
exceptionally(f) | On failure only | Throwable โ T | Error fallback value |
whenComplete(f) | Always | (T, Throwable) โ void | Side-effect only (logging) |
thenCompose vs thenApply is the most common interview trap:
// thenApply wraps: returns CompletableFuture<CompletableFuture<Order>>
CompletableFuture<CompletableFuture<Order>> bad = userFuture.thenApply(u -> fetchOrders(u));
// thenCompose flattens: returns CompletableFuture<Order> โ correct
CompletableFuture<Order> good = userFuture.thenCompose(u -> fetchOrders(u));
๐ด How would you write a custom Collector for grouping and counting in a single pass?โ
// Built-in: two passes (one to group, one to count)
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity, Collectors.counting()));
// Custom Collector for advanced aggregation:
Collector<Person, Map<String, long[]>, Map<String, Double>> averageSalaryByCity =
Collector.of(
HashMap::new, // supplier
(map, p) -> map.computeIfAbsent( // accumulator
p.getCity(), k -> new long[]{0L, 0L})
.let(a -> { a[0] += p.getSalary(); a[1]++; }),
(map1, map2) -> { // combiner (parallel)
map2.forEach((k, v) -> map1.merge(k, v,
(a, b) -> new long[]{a[0]+b[0], a[1]+b[1]}));
return map1;
},
map -> map.entrySet().stream() // finisher
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> (double) e.getValue()[0] / e.getValue()[1]))
);
For senior interviews, show awareness of the four components: supplier (mutable container), accumulator (add element), combiner (merge parallel results), finisher (transform to final result).
๐ด What is the "Happens-Before" relationship in the Java Memory Model, and why is it important?โ
Happens-Before is a formal memory visibility contract. If action A happens-before action B, the memory writes made by A are guaranteed to be visible to the thread performing B, and the JIT compiler/CPU is prohibited from reordering these operations.
Without a happens-before relationship, the JVM can reorder instructions or cache variables in CPU registers indefinitely, leading to stale reads and data corruption. Key triggers for happens-before include:
- Releasing a monitor lock (
unlock) happens-before subsequently acquiring that same lock (lock). - A write to a
volatilefield happens-before any subsequent read of that field. - Calling
Thread.start()happens-before any action in the started thread.
๐ด How does StampedLock optimize performance compared to ReentrantReadWriteLock?โ
StampedLock (Java 8+) introduces Optimistic Reading. In standard read-write locks, acquiring a read lock blocks write operations. Under heavy read contention, writers can suffer from starvation.
StampedLock solves this by allowing non-blocking reads:
- It returns a numeric stamp representing the lock's state without acquiring a read lock.
- The thread reads the fields into local variables.
- It then calls
lock.validate(stamp)to check if a write lock was acquired concurrently. - If validation succeeds, the read data is safe. If it fails, the thread falls back to a blocking read lock.
Optimistic reads are completely lock-free, avoiding CAS operations and memory barriers, which drastically increases scalability in high-read, low-write backend services.
๐ด Why does ThreadLocal cause memory leaks in web servers, and how do you prevent it?โ
Web application containers (like Tomcat, Spring Boot) use a pool of worker threads that are reused across HTTP requests.
Each Thread contains a ThreadLocalMap where keys are weak references to the ThreadLocal object, but the values are strong references.
- When a request ends, the
ThreadLocalreference in the stack frame is discarded and cleaned by GC. - The key in the map becomes
null(since it is weakly referenced). - However, because the thread is returned to the pool and remains alive, the value object remains strongly reachable via the thread's map, causing a memory leak.
Prevention: Always call ThreadLocal.remove() in a finally block before the request execution completes.
๐ด Explain the internal mechanism of WeakHashMap and its caching use cases.โ
WeakHashMap stores its keys wrapped in WeakReference objects.
- When a key has no other strong references in the application, the garbage collector clears it.
- During garbage collection, the cleared reference is put into a
ReferenceQueue. - On subsequent operations on the map (like
get(),put(), orsize()),WeakHashMappolls the queue and removes the corresponding entries (stale values) from its internal table.
It is useful for memory-sensitive caching (e.g., metadata maps associated with dynamic classloaders or database connection drivers) where you want cached entries to be collected automatically when the owner object is discarded.
๐ด How would you troubleshoot a thread deadlock or a CPU spike in a running production JVM?โ
- For Deadlocks: Generate a thread dump using
jcmd <pid> Thread.printorjstack <pid>. The JVM automatically identifies deadlocked threads and prints their stack traces, indicating which lock they hold and which lock they are waiting for. - For CPU Spikes:
- Identify the high-CPU native thread ID (TID) using
top -H -p <pid>. - Convert the decimal TID to hexadecimal (e.g.,
12345\rightarrow0x3039). - Capture a thread dump using
jstack <pid>. - Search the dump for
nid=0x3039to find the exact thread name, state, and line of code causing the CPU utilization.
- Identify the high-CPU native thread ID (TID) using
๐ด How did Java 9+ optimize String concatenation under the hood?โ
Prior to Java 9, string concatenation (e.g., "a" + "b" + "c") compiled to nested StringBuilder.append() calls. This hardcoded the optimization strategy in compiled bytecode.
Since Java 9, the compiler emits an invokedynamic call pointing to StringConcatFactory.
- The bootstrap method resolves the concatenation strategy dynamically at runtime (e.g., allocating a single byte array and copying elements directly).
- This decouples compiling from runtime optimizations: the JVM can improve string allocation strategies (reducing memory allocations by up to 10% in hot paths) without requiring developers to recompile their classes.
Advanced Editorial Pass: Interview Mastery with System Thinkingโ
What Differentiates Senior Answersโ
- Links language knowledge to system reliability, scalability, and maintainability.
- Uses trade-offs and measurable outcomes, not only definitions.
- Shows clear reasoning under uncertainty and failure scenarios.
Preparation Pitfallsโ
- Relying on memorized one-liners without implementation context.
- Ignoring performance diagnostics and observability dimensions.
- Weak examples that do not show decision quality.
Practice Strategyโ
- Reframe each answer around context, decision, trade-off, and result.
- Add one failure case and one prevention strategy per question.
- Practice whiteboard explanations that include data flow and bottlenecks.