Java Developer Interview Questions
Core Overview
Master core Java concepts, object-oriented programming, concurrency, streams, JVM memory mechanics, and Spring Boot.
Ready to test your knowledge?
Launch a focused practice session to review questions without distraction.
What is the difference between the `==` operator and the `equals()` method in Java?
Direct Answer
The == operator compares primitive values or reference addresses, whereas the equals() method is overridden to compare logical values.
Detailed Explanation
In Java, the == operator performs reference comparison for objects (checks if they point to the same memory location) and value comparison for primitive types. The equals() method is inherited from java.lang.Object and defaults to reference comparison (==). However, classes like String, Integer, and Double override equals() to implement value equality. When overriding equals(), the developer must also override hashCode() to maintain the contract that equal objects must produce identical hash codes.
Code Example
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false (different references in memory)
System.out.println(s1.equals(s2)); // true (logical content is identical)
String s3 = "hello";
String s4 = "hello";
System.out.println(s3 == s4); // true (String pool reference sharing)
Common Interview Pitfalls
- Using == to compare Strings instead of using String.equals().
- Forgetting to override hashCode() when overriding equals().
Is Java pass-by-value or pass-by-reference?
Direct Answer
Java is strictly pass-by-value. Primitives pass a copy of the value; objects pass a copy of the reference address.
Detailed Explanation
Java does not support passing arguments by reference. Every argument is passed by copying its value.
Code Example
public class PassTest {
public static void main(String[] args) {
int x = 10;
modifyPrimitive(x);
System.out.println(x); // Outputs 10
Dog d = new Dog("Max");
modifyObject(d);
System.out.println(d.name); // Outputs "Buddy" (mutated heap object)
reassignObject(d);
System.out.println(d.name); // Outputs "Buddy" (reassignment failed to change caller)
}
static void modifyPrimitive(int val) { val = 20; }
static void modifyObject(Dog dog) { dog.name = "Buddy"; }
static void reassignObject(Dog dog) { dog = new Dog("Fido"); }
}
Common Interview Pitfalls
- Believing that mutating an object parameter modifies the reference pointer itself.
- Claiming Java passes objects by reference.
How do primitive wrapper classes cache values in Java, and what are the performance impacts of autoboxing?
Direct Answer
Java caches specific ranges of wrapper objects (like Integer from -128 to 127) using IntegerCache. Autoboxing outside this range creates new objects, causing memory churn.
Detailed Explanation
Wrapper classes (e.g. Integer, Long, Boolean) contain a caching mechanism to save memory. For example, Integer caches values from -128 to 127 by default. When autoboxing occurs (like Integer x = 10) or calling Integer.valueOf(), Java returns a cached instance. If values are outside this range, a new object is allocated. Autoboxing in tight loops leads to significant object creation overhead, increasing GC work.
Code Example
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true (cached range)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false (outside default cached range)
// High GC overhead pattern
long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i; // autoboxing Long into primitive long is fine, but wrapper sums are slow
}
Common Interview Pitfalls
- Using wrapper types for loop counters, leading to extensive boxing/unboxing overhead.
- Assuming two Integers outside the cache range are equal via == operator.
What are the memory and execution differences between primitive types and reference types in Java?
Direct Answer
Primitives are stored directly on the stack or inline inside object headers, while reference types reside on the heap and require pointer dereferencing.
Detailed Explanation
Primitives (e.g., int, double, boolean) are lightweight value carriers. When declared as local variables, they are stored directly on the thread's execution stack frame. Reference types (e.g., String, custom objects) store only their reference address on the stack, while the actual object resides on the garbage-collected heap. Primitives avoid heap allocation and pointer dereference overhead, making them faster and more memory-efficient.
Code Example
// Memory layout differences:
int primitiveVal = 42; // 4 bytes directly on the stack frame
Integer objectVal = Integer.valueOf(42);
// 8-byte reference on stack, pointing to 16-byte object on the heap
Common Interview Pitfalls
- Using reference wrappers inside database model entities where nullability is not required, causing memory bloat.
- Failing to consider cache locality differences when handling large primitive arrays vs arrays of objects.
What are the main phases of the Java Class Loading lifecycle, and how does class initialization work?
Direct Answer
The Class Loading lifecycle contains Loading, Linking (Verification, Preparation, Resolution), and Initialization phases. Initialization runs static blocks in order.
Detailed Explanation
Class loading is handled by the JVM to dynamically load type metadata.
1. Loading: Reads binary byte arrays and creates java.lang.Class instances.
2. Linking:
3. Initialization: Executes static initializers and static variable assignments in source order, triggered by actions like new, static method calls, or class access.
Code Example
public class LoadTest {
static {
System.out.println("Static Initializer block called"); // runs during initialization
}
public static final int CONSTANT = 100; // compiled inline, access may not trigger init
}
Common Interview Pitfalls
- Accessing compile-time constants (static final primitives/strings) expecting it to trigger class initialization.
- Causing circular class dependencies during static initialization, leading to ClassNotFoundException or deadlocks.
What is the Java String Pool, and how does the `String.intern()` method work under the hood?
Direct Answer
The String Pool is a JVM hash map in the heap. String.intern() registers literals or retrieves matching instances to share references.
Detailed Explanation
The String Pool is a table of unique String literals maintained in the Java Heap (since Java 7). String literals are automatically added to this pool at class load time. The intern() method allows dynamic strings to seek registration. If the pool already contains a string equal to the target string, it returns the pooled instance; otherwise, it adds the string to the pool and returns the reference. This saves memory but requires matching keys in a hashtable.
Code Example
String s1 = new String("test").intern();
String s2 = "test";
System.out.println(s1 == s2); // true (both refer to the same pool instance)
String s3 = new String("test");
System.out.println(s3 == s2); // false (s3 is a separate heap allocation)
Common Interview Pitfalls
- Calling intern() on millions of unique dynamically generated strings, causing String Pool hashtable collisions and slow execution.
- Using == for string comparisons hoping that intern() was called implicitly.
What is the difference between `StringBuilder` and `StringBuffer` in Java?
Direct Answer
StringBuilder is non-synchronized and faster for single-thread modifications; StringBuffer is synchronized and thread-safe but has synchronization overhead.
Detailed Explanation
Both StringBuilder and StringBuffer represent mutable sequences of characters, expanding their capacity automatically using underlying char/byte arrays. The key difference is synchronization:
Code Example
// Preferred for thread-local string construction
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");
String result = sb.toString();
Common Interview Pitfalls
- Using StringBuffer inside local helper methods where thread sharing is impossible, paying unnecessary synchronization cost.
- Allocating builders without specifying capacity when build lengths are known, causing repeated array allocations.
How do you design and write a custom immutable class in Java, and why is deep copying required?
Direct Answer
Make the class final, make all fields private final, provide no setter methods, and perform deep copying on mutable reference fields during construction and retrieval.
Detailed Explanation
An immutable class cannot have its state modified after creation. Rules for immutability:
1. Declare class as final to prevent subclassing.
2. Make all fields private final.
3. Do not expose mutator methods (setters).
4. If the class has fields pointing to mutable objects (e.g. java.util.Date, collections), perform deep copies in both constructors and getters to prevent reference leaking.
Code Example
import java.util.Date;
public final class ImmutableUser {
private final String name;
private final Date registrationDate;
public ImmutableUser(String name, Date registrationDate) {
this.name = name;
// Deep copy mutable Date object to protect internal state
this.registrationDate = new Date(registrationDate.getTime());
}
public String getName() { return name; }
public Date getRegistrationDate() {
// Return defensive copy instead of original mutable reference
return new Date(registrationDate.getTime());
}
}
Common Interview Pitfalls
- Leaking internal references to mutable collections or dates via getter methods.
- Forgetting to declare the class final, allowing subclass modifications.
What is the difference between compile-time and runtime String concatenation in Java?
Direct Answer
Compile-time concatenation combines literals into a single String in bytecode. Runtime concatenation uses invokedynamic or StringBuilders.
Detailed Explanation
Java handles concatenation based on compiler optimizations:
"a" + "b") are evaluated by the compiler and replaced with a single literal ("ab") in the class constant pool.s1 + s2) happens at runtime. In Java 8, this compiled to StringBuilder. Since Java 9, it uses dynamic method calls (invokedynamic calling StringConcatFactory.makeConcatWithTemplate), which reduces memory overhead and allows JVM updates.Code Example
String a = "hello " + "world"; // compile-time: optimized to "hello world"
String s1 = "hello ";
String s2 = "world";
String b = s1 + s2; // runtime: invokes dynamic string concatenation
Common Interview Pitfalls
- Using string concatenation (+) inside loops, causing repeated dynamic allocations instead of reusing a single StringBuilder.
- Assuming runtime string concatenations are automatically cached in the String Pool.
What is the difference between an abstract class and an interface in Java, especially after Java 8 and 9?
Direct Answer
Abstract classes support state (fields) and single inheritance, while interfaces support multiple implementation inheritance and (since Java 8/9) default, static, and private methods.
Detailed Explanation
In Java, an abstract class is a class that cannot be instantiated. It can define state (instance variables) and methods with or without implementations. Classes inherit from an abstract class using extends (limited to single inheritance).
Interfaces declare behavior. Since Java 8, interfaces can provide default method implementations (default) and static methods. Java 9 added private methods inside interfaces to reuse code blocks. Unlike abstract classes, interfaces cannot maintain instance fields (all fields are implicitly public static final), and classes can implement multiple interfaces using implements.
Code Example
interface RunnableTask {
default void start() {
logStart();
run();
}
void run();
private void logStart() {
System.out.println("Starting interface task...");
}
}
Common Interview Pitfalls
- Trying to declare instance fields inside an interface.
- Expecting multiple inheritance of classes when using abstract classes.
How does dynamic method dispatch implement runtime polymorphism in Java?
Direct Answer
Dynamic method dispatch resolves overridden method calls at runtime using the actual object type via vtables, rather than the reference type.
Detailed Explanation
Polymorphism allows a superclass reference pointer to point to subclass objects. Dynamic method dispatch is the mechanism by which overridden methods are resolved at runtime. When an overridden method is called, the JVM looks up the actual runtime object type using its virtual method table (vtable), rather than using the reference pointer class determined at compile time.
Code Example
class Animal {
void speak() { System.out.println("Generic sound"); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("Bark"); }
}
Animal pet = new Dog();
pet.speak(); // Prints "Bark" (resolved dynamically using Dog instance type)
Common Interview Pitfalls
- Assuming that static methods or class fields can be overridden and dynamically dispatched (they are resolved at compile time).
- Confusing dynamic dispatch (runtime polymorphism) with method overloading (compile-time polymorphism).
What is the difference between encapsulation and delegation in Java, and how do they support clean object designs?
Direct Answer
Encapsulation hides an object's internal state and forces interactions through public methods; delegation passes a task to another helper class.
Detailed Explanation
Encapsulation restricts direct access to object fields, guarding state using access modifiers (like private) and validating values in getters/setters. Delegation is a design pattern where an object handles a request by passing it to an internal helper object. Delegation allows modular composition instead of coupling code through class inheritance hierarchies.
Code Example
// Delegation example
class Printer {
void print(String doc) { System.out.println("Printing: " + doc); }
}
class OfficeSuite {
private final Printer printer = new Printer();
// Delegate printing behavior to Printer helper
void printDocument(String document) {
printer.print(document);
}
}
Common Interview Pitfalls
- Leaking encapsulated class references to mutable objects, violating the encapsulation barrier.
- Over-using delegation for simple, unrelated tasks, which creates wrapper overhead and class bloat.
Why is composition often preferred over class inheritance in Java designs?
Direct Answer
Composition constructs behaviors by combining decoupled instances, avoiding the tight coupling and fragility of inheritance.
Detailed Explanation
Inheritance represents an "is-a" relationship, creating tight compile-time coupling between subclass and superclass. If the superclass changes, the subclass can break (the fragile base class problem). Composition represents a "has-a" relationship, combining instances of independent classes through interfaces. Composition allows changing helper behaviors at runtime by swapping objects, improving testability.
Code Example
// Composition over inheritance
interface Engine { void start(); }
class V8Engine implements Engine { public void start() { /* ... */ } }
class Car {
private final Engine engine; // Composed, not inherited
public Car(Engine engine) {
this.engine = engine;
}
public void start() { engine.start(); }
}
Common Interview Pitfalls
- Using inheritance solely to share code (e.g. subclassing utility classes), which violates the logical "is-a" relationship.
- Creating deep class inheritance hierarchies that are hard to refactor.
What is the difference between the `Comparable` and `Comparator` interfaces in Java?
Direct Answer
Comparable defines the natural sorting order of the object class, while Comparator defines custom sorting rules externally.
Detailed Explanation
Comparable is implemented by the object class itself, overriding the compareTo(T o) method to establish a natural sort order (e.g. alphabetical for Strings, numeric for Integers). Comparator is an external interface implemented to define alternative sorting rules, overriding compare(T o1, T o2). Since Java 8, Comparator offers functional builder methods (like comparingInt(), thenComparing()) to chain sorting steps.
Code Example
import java.util.Comparator;
public class Sorting {
public static void main(String[] args) {
// Natural order comparable sorting:
// Collections.sort(listOfComparableObjects);
// Custom comparator sorting by age then name:
Comparator<Person> byAgeThenName = Comparator
.comparingInt(Person::getAge)
.thenComparing(Person::getName);
}
}
class Person implements Comparable<Person> {
private String name;
private int age;
public int getAge() { return age; }
public String getName() { return name; }
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name); // natural order by name
}
}
Common Interview Pitfalls
- Returning simple subtraction differences in compareTo() (e.g., this.value - other.value) which can cause integer overflow issues.
- Forgetting that compareTo() should be consistent with equals() (if compareTo returns 0, equals should return true).
What are Java Record classes, and how do they differ from standard POJOs?
Direct Answer
Records are immutable data carriers that automatically generate constructor, getter, equals, hashCode, and toString methods.
Detailed Explanation
Records (introduced in Java 16) are classes designed specifically to carry immutable data. The compiler automatically generates private final fields, a canonical constructor, public getter methods (without the "get" prefix, e.g., name()), and standard implementations of equals(), hashCode(), and toString(). Records cannot extend other classes (since they implicitly extend java.lang.Record) and are final, but they can implement interfaces.
Code Example
// Declaring a record:
public record UserRecord(String username, int age) {}
// Usage:
UserRecord user = new UserRecord("yatish", 28);
System.out.println(user.username()); // "yatish" (no get prefix)
System.out.println(user); // toString auto-generated
Common Interview Pitfalls
- Trying to declare instance fields inside a record body (only static fields are allowed).
- Expecting records to have standard JavaBean setter methods (fields are immutable).
How does a HashMap work internally in Java, and how has it changed in Java 8?
Direct Answer
HashMap uses hashing to resolve buckets via key hashCode(). In Java 8, bucket structures treeify from linked lists to red-black trees when collision thresholds exceed 8.
Detailed Explanation
HashMap works on the principle of hashing. It contains an array of Node buckets. When put(key, value) is called, the hash code of the key is computed and mapped to an index in the array using hash & (n-1). If multiple keys map to the same index (collision), they are chained.
Before Java 8, collisions were stored in singly-linked lists. In Java 8, if a bucket size exceeds the threshold of TREEIFY_THRESHOLD = 8 and total map capacity is at least MIN_TREEIFY_CAPACITY = 64, the linked list is converted into a balanced Red-Black Tree. This reduces worst-case retrieval time from O(N) to O(log N). If bucket size falls below 6 during resizing, it untreeifies back to a list.
Code Example
HashMap<String, Integer> map = new HashMap<>(32, 0.75f);
map.put("Apple", 1);
// Index resolved via (hashCode() ^ (hashCode() >>> 16)) & (capacity - 1)
Common Interview Pitfalls
- Using mutable objects as HashMap keys, which alters the hash code and makes values unretrievable.
- Assuming HashMap maintains any insertion or sorted ordering (use LinkedHashMap or TreeMap instead).
What is the difference between fail-fast and weakly consistent or snapshot-based iterators in Java?
Direct Answer
Fail-fast iterators throw ConcurrentModificationException immediately on concurrent modification, while weakly consistent iterators read live values or snapshots without throwing exceptions.
Detailed Explanation
This difference relates to collection traversal during modification:
ArrayList, HashMap) use an internal modification counter (modCount). If the collection is modified directly during iteration, the iterator detects modCount mismatch and throws ConcurrentModificationException immediately.ConcurrentHashMap, CopyOnWriteArrayList) do not throw modifications exceptions. They iterate over a clone/snapshot or read live node links, tolerating modifications during traversal.Code Example
List<String> list = new ArrayList<>(List.of("A", "B"));
Iterator<String> it = list.iterator();
list.add("C");
// it.next(); // Throws ConcurrentModificationException (Fail-fast)
Common Interview Pitfalls
- Modifying a collection directly inside a for-each loop instead of using Iterator.remove().
- Assuming weakly consistent iterators always reflect concurrent writes in real time.
How does ConcurrentHashMap achieve high concurrency under concurrent read and write operations?
Direct Answer
ConcurrentHashMap uses bucket-level locking on write, Lock-Free read operations via volatile node pointers, and CAS loops for empty buckets.
Detailed Explanation
In Java 8+, ConcurrentHashMap has abandoned segment-level locking (used in Java 7) in favor of bucket-level locking. When inserting into an empty bucket, it uses Compare-And-Swap (CAS) loops. If the bucket already contains nodes, it locks only the first node (head) of the bucket using synchronized, leaving other buckets unblocked. Reads are completely lock-free because node values (val) and next pointers (next) are declared volatile, ensuring happens-before visibility.
Code Example
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Thread-safe atomic operation:
map.computeIfAbsent("Key", k -> 42);
Common Interview Pitfalls
- Using external synchronized blocks around ConcurrentHashMap methods, which eliminates its performance benefits.
- Using composite actions (like check-then-act) without atomic helpers like computeIfAbsent() or merge(), causing race conditions.
What is Type Erasure in Java Generics, and what are its runtime limitations?
Direct Answer
Type Erasure removes generic type parameter parameters at compile-time, replacing them with bounds (or Object) and inserting casts in bytecode.
Detailed Explanation
Generics were added in Java 5 to provide compile-time type safety. To ensure backward compatibility with older non-generic class files, the compiler performs Type Erasure. The compiler replaces all generic types in bytecode with their upper bounds (or Object if unbound) and inserts cast instructions where needed. Consequently, generic type information is unavailable at runtime. You cannot instantiate a generic type parameter (new T()), check instance types via instanceof T, or construct arrays of generics.
Code Example
public class ErasureTest<T> {
private T value; // compiles to: private Object value;
public void printInfo() {
// if (value instanceof T) {} // Compile Error: Cannot perform instanceof check on generic type
}
}
Common Interview Pitfalls
- Expecting generic type arguments to be available at runtime using reflection (except for class signatures).
- Declaring raw types (e.g. List instead of List<String>), which bypasses compiler type checking.
What is the PECS wildcard rule in Java Generics, and when do you use `extends` versus `super`?
Direct Answer
PECS stands for Producer Extends, Consumer Super. Use `? extends T` when reading items from a collection, and `? super T` when writing items to a collection.
Detailed Explanation
The PECS rule guides wildcard usage to maximize collection reuse:
producer), use ? extends T. This guarantees that the items are subclasses of T (covariance), allowing safe reads as type T.consumer), use ? super T. This guarantees that the collection holds objects that are superclasses of T (contravariance), allowing safe writes of type T. If you need to perform both read and write operations, do not use wildcards.Code Example
public class Wildcards {
// Producer extends: Reads elements of type Number or its subclasses safely
public static double sum(List<? extends Number> list) {
double total = 0.0;
for (Number n : list) total += n.doubleValue();
return total;
}
// Consumer super: Writes elements of type Integer safely
public static void addNumbers(List<? super Integer> list) {
list.add(1); // safe to write Integer
// Object item = list.get(0); // Reads are only safe as Object
}
}
Common Interview Pitfalls
- Attempting to write elements (using add()) into a collection declared with <? extends T>.
- Attempting to read specific subclasses from a collection declared with <? super T>.
How do you choose between `ArrayList`, `LinkedList`, `HashSet`, and `HashMap` in Java?
Direct Answer
ArrayList is chosen for random indexing; LinkedList for queue-like inserts/removes; HashSet for unique entries; HashMap for key-value association.
Detailed Explanation
Your choice depends on complexity trade-offs:
O(1)). Appends are fast (O(1) amortized), but inserts or deletions in the middle require shifting elements (O(N)).O(1)), but indexing requires traversal (O(N)). Higher memory overhead due to pointer objects.O(1)), but does not maintain order.O(1) average) using hashing.Code Example
// Random access target:
List<String> userIds = new ArrayList<>();
// Unique elements check target:
Set<String> uniqueEmails = new HashSet<>();
Common Interview Pitfalls
- Using LinkedList for general listing tasks where ArrayList offers better cpu cache locality and lower memory footprint.
- Forgetting that HashSet does not preserve any element iteration order.
What is the contract between `equals()` and `hashCode()` in Java, and why must they be overridden together?
Direct Answer
If two objects are equal according to equals(), they must return the same hashCode(). Failing this contract breaks hash collections like HashMap.
Detailed Explanation
The contract defined in java.lang.Object specifies:
1. If o1.equals(o2) is true, then o1.hashCode() == o2.hashCode() must be true.
2. If o1.hashCode() == o2.hashCode() is true, the objects are not necessarily equal (collision).
If you override equals() but not hashCode(), equal objects will return different hash codes. When put in a HashMap or HashSet, the objects will map to different buckets, causing duplicates or failure to retrieve values.
Code Example
public class CustomKey {
private final String id;
public CustomKey(String id) { this.id = id; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CustomKey)) return false;
return this.id.equals(((CustomKey) o).id);
}
@Override
public int hashCode() {
return this.id.hashCode(); // Consistent with equals
}
}
Common Interview Pitfalls
- Overriding equals() but using a default or constant hashCode(), which degrades hash structures to search lists (O(N)).
- Including mutable fields in equals/hashCode calculation, leading to element leaks in sets.
What is the difference between checked and unchecked exceptions in Java, and when should you use each?
Direct Answer
Checked exceptions are verified at compile-time and subclass Exception; unchecked exceptions run at runtime and subclass RuntimeException.
Detailed Explanation
Java categorizes throwables into distinct types:
java.lang.Exception (except RuntimeException). The compiler forces calling methods to declare them via throws or handle them inside try-catch. Use checked exceptions for recoverable errors outside application control (e.g., IOException, SQLException).java.lang.RuntimeException. They bypass compile-time checks. Use unchecked exceptions for programming errors or unrecoverable faults (e.g., NullPointerException, IllegalArgumentException).Code Example
// Checked: File access might fail due to environmental factors
public void readFile(String path) throws IOException {
throw new IOException("File not found");
}
// Unchecked: Developer bug, parameter must be checked before calling
public void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
}
Common Interview Pitfalls
- Catching generic Throwable or Exception instead of specific subclasses, which hides unexpected runtime errors.
- Using checked exceptions for logical validation checks, forcing callers to write extensive boilerplates.
How does try-with-resources work in Java, and what is the role of the `AutoCloseable` interface?
Direct Answer
Try-with-resources guarantees closing resource variables at block exit. The resources must implement the AutoCloseable interface.
Detailed Explanation
Introduced in Java 7, try-with-resources simplifies resource management (streams, sockets, database connections). Resources declared inside try(...) are automatically closed at block completion (whether normally or via exception). This replaces manual finally close statements. The resource must implement java.lang.AutoCloseable. Exceptions thrown during close operations are attached as "suppressed exceptions" to the primary block exception, readable using getSuppressed().
Code Example
import java.io.*;
public void readFirstLine(String path) throws IOException {
// BufferedReader implements AutoCloseable, closed automatically
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
System.out.println(br.readLine());
}
}
Common Interview Pitfalls
- Forgetting that resources declared inside try-with-resources are implicitly final and cannot be reassigned.
- Declaring resources outside the try-with-resources block declaration, which bypasses automated cleanup.
What is exception chaining, and what is the runtime performance overhead of throwing exceptions in Java?
Direct Answer
Exception chaining wraps low-level errors inside custom abstractions to preserve root causes. Throwing exceptions is slow because filling the stack trace requires JVM traversal.
Detailed Explanation
Exception chaining allows developers to wrap low-level exceptions (e.g. SQLException) in high-level domain exceptions (e.g. DataAccessException) while preserving the root cause using the constructor Throwable(String message, Throwable cause).
Creating a Throwable is slow because of the fillInStackTrace() method call. The JVM must traverse the execution thread stack to record class names, method names, and line numbers. For high-throughput services, throwing exceptions as control flow degrades performance.
Code Example
public void saveUser(User user) {
try {
database.insert(user);
} catch (SQLException e) {
// Exception chaining preserves root database exception
throw new CustomDataException("Failed to save user info", e);
}
}
Common Interview Pitfalls
- Using exceptions for normal flow control (e.g. throwing NoSuchElementException to end iteration loops).
- Wrapping exceptions without passing the original cause, losing root stack trace contexts.
What is the difference between `map` and `flatMap` in Java Streams?
Direct Answer
map performs a 1-to-1 conversion of elements, whereas flatMap converts each element to a stream and flattens them into a single stream.
Detailed Explanation
Both map and flatMap are intermediate stream operations:
T to R. It returns a stream of the transformed elements (1-to-1 mapping).T to Stream<R>. It merges (flattens) these individual sub-streams into a single continuous stream of type R (1-to-many or 1-to-0 mapping). This is useful for nesting collections or flattening lists of lists.Code Example
// Map: converts list of strings to list of their lengths
List<Integer> lengths = List.of("Java", "Stream")
.stream()
.map(String::length)
.toList(); // [4, 6]
// FlatMap: flattens list of lists into a single flat list
List<List<String>> nested = List.of(List.of("A", "B"), List.of("C"));
List<String> flat = nested.stream()
.flatMap(List::stream)
.toList(); // ["A", "B", "C"]
Common Interview Pitfalls
- Using map where flatMap is needed, resulting in a nested Stream<Stream<T>> structure.
- Forgetting that flatMap requires returning Stream objects, not simple Collections.
How does lazy evaluation work in Java Streams, and why is it beneficial?
Direct Answer
Intermediate stream operations do not execute until a terminal operation is called. This allows optimization and short-circuiting.
Detailed Explanation
Java Streams are split into intermediate (e.g. filter, map) and terminal (e.g. collect, forEach) operations. Intermediate operations return a new stream but perform no calculations. Instead, they build an execution pipeline.
Execution occurs only when a terminal operation is invoked. The pipeline is optimized to process elements in a single pass (loop fusion). This allows short-circuiting operations (like findFirst, anyMatch, limit) to stop processing elements as soon as the condition is satisfied, avoiding unnecessary computations.
Code Example
List.of("apple", "banana", "cherry")
.stream()
.filter(s -> {
System.out.println("Filter: " + s);
return s.startsWith("b");
})
.map(s -> {
System.out.println("Map: " + s);
return s.toUpperCase();
})
.findFirst(); // Terminal operation triggers execution
// Console output will show only "apple" and "banana" are processed.
// "cherry" is skipped entirely (short-circuiting).
Common Interview Pitfalls
- Expecting intermediate operations (like filter/map) to modify external collection states without invoking a terminal operation.
- Creating infinite streams (like Stream.iterate) without adding short-circuit terminal constraints, causing OutOfMemoryError.
How do you implement a custom Collector in Java Streams using the `Collector` interface?
Direct Answer
Implement the Collector interface by providing a supplier (container), accumulator (append), combiner (merge threads), and finisher (result).
Detailed Explanation
A custom Collector collects elements from a stream into a custom structure. You implement the java.util.stream.Collector interface which requires 5 parameters:
1. Supplier: Supplier<A> creates the mutable accumulator container (e.g. ArrayList::new).
2. Accumulator: BiConsumer<A, T> folds a stream element into the container.
3. Combiner: BinaryOperator<A> merges two containers (for parallel streams).
4. Finisher: Function<A, R> performs the final transformation of container A to result R.
5. Characteristics: Set of characteristics (like IDENTITY_FINISH, UNORDERED).
Code Example
import java.util.stream.Collector;
import java.util.StringJoiner;
// Custom collector joining strings with brackets
public class CustomCollectors {
public static Collector<String, ?, String> joiningWithBrackets() {
return Collector.of(
() -> new StringJoiner(", ", "[", "]"), // supplier
StringJoiner::add, // accumulator
StringJoiner::merge, // combiner
StringJoiner::toString // finisher
);
}
}
Common Interview Pitfalls
- Forgetting to implement a thread-safe combiner method, which causes random crashes during parallel stream collections.
- Violating characteristics declarations, leading to casting errors at runtime.
How do parallel streams execute in Java, and what are the performance risks of the shared ForkJoinPool?
Direct Answer
Parallel streams split collections using Spliterators and run tasks concurrently on the shared ForkJoinPool.commonPool(). Blocking tasks can starve other threads.
Detailed Explanation
Parallel streams split data source arrays/collections using Spliterator wrappers. Tasks are executed concurrently across multiple threads using the shared JVM ForkJoinPool.commonPool().
Because the common pool is shared across all parallel streams and async tasks (like CompletableFuture), blocking operations (like HTTP requests or database calls) inside a parallel stream can starve the pool, locking other concurrent features in the application.
Code Example
List<Integer> list = List.of(1, 2, 3, 4, 5);
// Process numbers concurrently
list.parallelStream()
.map(x -> x * 2)
.forEach(System.out::println);
Common Interview Pitfalls
- Running network or database blocking requests inside parallel streams, causing shared pool starvation.
- Using parallel streams on collections with high splitting overhead (like LinkedList) where sequential processing is faster.
What are the best practices for using `Optional` in Java, and what are common anti-patterns?
Direct Answer
Use Optional strictly as a return type for methods that might not return a value. Avoid using it for class fields, method parameters, or within collections.
Detailed Explanation
Optional was added in Java 8 to prevent NullPointerException by providing a clear API for return types.
Optional<T> as a method return type. Use monadic methods (like ifPresent, map, orElseGet) to provide fallbacks.Optional for class fields (it is not serializable). Do not pass Optional as method parameters (use standard null checks). Avoid calling get() without first checking isPresent(), which raises NoSuchElementException.Code Example
public Optional<User> findUserById(String id) {
User user = database.get(id);
return Optional.ofNullable(user);
}
// Consuming safely:
findUserById("123")
.map(User::getName)
.orElse("Guest User");
Common Interview Pitfalls
- Using Optional.get() without checking isPresent(), defeating the safety of the wrapper.
- Declaring class fields or model entity variables as Optional types, breaking serialization.
What is the purpose of the `volatile` keyword in Java, and how does it affect memory visibility?
Direct Answer
The volatile keyword ensures that thread reads/writes go directly to main memory, establishing happens-before visibility and preventing instruction reordering.
Detailed Explanation
In multi-threaded systems, processors cache variables in registers or CPU caches for speed. This can cause memory visibility issues where modifications made by one thread are not visible to others.
Declaring a field volatile guarantees that:
1. Visibility: All writes to the variable are flushed to main memory immediately, and reads pull directly from main memory.
2. Instruction Reordering: The compiler and JVM are restricted from reordering instructions around the volatile field, using hardware-level memory barriers (fences).
volatile does NOT provide atomicity. For compound actions (like count++), you must use locks or atomic wrapper classes.
Code Example
public class SharedFlag {
// Volatile ensures changes to active flag are visible across threads
private volatile boolean active = true;
public void stop() { active = false; }
public void run() {
while (active) {
// execute task
}
}
}
Common Interview Pitfalls
- Using volatile for counter variables (e.g. volatile int count = 0; count++) expecting thread-safe increments (requires atomic classes).
- Declaring volatile on objects expecting fields inside the object to also inherit volatile visibility guarantees.
How do Virtual Threads differ from traditional Platform Threads in Java 21, and how do they schedule execution?
Direct Answer
Platform threads map 1-to-1 with OS threads. Virtual threads are lightweight user-mode threads managed by the JVM, multiplexed on carrier threads.
Detailed Explanation
Virtual Threads (Project Loom, Java 21) address the scalability limits of platform threads:
Code Example
// Spawning a virtual thread:
Thread.startVirtualThread(() -> {
System.out.println("Running on virtual thread: " + Thread.currentThread());
});
Common Interview Pitfalls
- Pooling virtual threads using thread pools (like ExecutorService), which is redundant because they are lightweight.
- Pinning carrier threads during synchronized blocks containing blocking operations (use ReentrantLock instead).
What are the main thread-safety patterns in Java, and how do they prevent concurrency issues?
Direct Answer
Thread-safety patterns include stack confinement, immutability, volatile variables, thread-safe collection wrappers, and lock-based synchronization.
Detailed Explanation
To ensure thread safety (protecting shared state from race conditions):
1. Immutability: Make state unchangeable (e.g. final classes, Records). Immutable objects are inherently thread-safe.
2. Confinement: Keep state confined to a single thread (local variables on the stack, or ThreadLocal variables).
3. Locking: Synchronize access to shared mutable data using synchronized or explicit locks (ReentrantLock).
4. Concurrent Collections: Use thread-safe data structures like ConcurrentHashMap or CopyOnWriteArrayList to handle concurrent access safely.
Code Example
// ThreadLocal confinement pattern:
public class ConnectionManager {
private static final ThreadLocal<Connection> connectionHolder =
ThreadLocal.withInitial(() -> DriverManager.getConnection("jdbc:mysql://localhost/db"));
public static Connection getConnection() {
return connectionHolder.get(); // confined to calling thread
}
}
Common Interview Pitfalls
- Sharing non-thread-safe formatter utilities (e.g., SimpleDateFormat) across threads without using ThreadLocal or thread-safe equivalents (like DateTimeFormatter).
- Forgetting to call remove() on ThreadLocal instances in thread-pool environments, causing memory leaks.
What is the difference between the `synchronized` keyword and the explicit `ReentrantLock` class in Java?
Direct Answer
synchronized is an implicit, block-scoped lock; ReentrantLock is an explicit lock offering advanced features like lock polling, timeouts, and multiple condition variables.
Detailed Explanation
Both implement reentrant mutual exclusion locking, but they differ in capabilities:
lock() and unlock() in a try-finally block. It offers advanced features: fair lock ordering, interruptible locks, non-blocking polling (tryLock()), timed lock attempts, and multiple lock condition variables (Condition).Code Example
import java.util.concurrent.locks.ReentrantLock;
public class LockManager {
private final ReentrantLock lock = new ReentrantLock();
public void executeSecurely() {
lock.lock();
try {
// Protected critical section
} finally {
lock.unlock(); // Always release in finally block
}
}
}
Common Interview Pitfalls
- Forgetting to release ReentrantLock via unlock() in the finally block, which can cause permanent deadlocks.
- Using the synchronized keyword indiscriminately on large method bodies, which reduces application throughput.
How do you coordinate multiple asynchronous tasks in Java using `CompletableFuture`?
Direct Answer
CompletableFuture implements Promise chains. Use thenApply/thenAccept for sequence, thenCombine for parallel join, and allOf/anyOf for collection coordination.
Detailed Explanation
CompletableFuture (introduced in Java 8) implements the Future and CompletionStage interfaces, allowing non-blocking asynchronous coordination. You can chain tasks together using functional callbacks:
Code Example
import java.util.concurrent.CompletableFuture;
public class AsyncJob {
public void run() {
CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> "Task 1");
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> "Task 2");
CompletableFuture<String> combined = task1.thenCombine(task2, (r1, r2) -> r1 + " & " + r2);
combined.thenAccept(System.out::println);
}
}
Common Interview Pitfalls
- Using blocking get() calls inside CompletableFuture chains, defeating the purpose of non-blocking async execution.
- Forgetting to specify a custom ThreadPool executor for blocking tasks, which can starve the shared ForkJoinPool.commonPool().
What is the Java Memory Model happens-before guarantee, and how does it ensure thread safety?
Direct Answer
Happens-before is a set of ordering rules defined by the Java Memory Model to guarantee that memory writes by one thread are visible to another.
Detailed Explanation
The Java Memory Model (JMM) defines ordering rules called "happens-before." If action A happens-before action B, then the memory changes made by A are guaranteed to be visible to the thread executing B.
Key rules include:
1. Program Order Rule: Actions in a single thread happen in source code order.
2. Monitor Lock Rule: An unlock on a monitor lock happens-before every subsequent lock acquisition on the same monitor.
3. Volatile Variable Rule: A write to a volatile field happens-before every subsequent read of that same field.
4. Thread Start/Join Rules: Calling thread.start() happens-before any actions in the started thread.
Code Example
public class VisibilityTracker {
private int value = 0;
private volatile boolean ready = false;
public void writer() {
value = 42; // write primitive
ready = true; // volatile write (happens-before)
}
public void reader() {
if (ready) { // volatile read (guarantees visibility)
System.out.println(value); // Guaranteed to print 42
}
}
}
Common Interview Pitfalls
- Assuming that happens-before visibility guarantees automatically provide atomic operation properties for compound statements.
- Relying on double-checked locking without declaring the shared instance field volatile, allowing threads to see partially initialized objects.
How do atomic classes in Java implement lock-free concurrency using Compare-And-Swap (CAS)?
Direct Answer
Atomic classes use CPU instructions via JVM Unsafe/VarHandle calls to perform atomic Compare-And-Swap checks, avoiding thread blocking.
Detailed Explanation
Atomic classes (like AtomicInteger, AtomicReference) provide lock-free, thread-safe updates for single variables. Instead of using locks, they rely on hardware-level CPU instructions like Compare-And-Swap (CAS) via JVM internal APIs (Unsafe/VarHandle).
In a CAS operation, the CPU checks if the memory location holds the expected old value. If yes, it updates it to the new value in a single atomic step; if no, the operation fails, and the thread retries in a loop (busy-spin) until successful, avoiding the cost of thread blocking.
Code Example
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounter {
private final AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // Atomic CAS increment
}
public int get() { return counter.get(); }
}
Common Interview Pitfalls
- Using multiple independent atomic variables together expecting the entire compound action to be atomic (requires explicit locks).
- Forgetting that under high write contention, busy-spin CAS loops can cause high CPU utilization.
What are deadlocks, how can they be prevented in Java, and what tools can detect them in production?
Direct Answer
Deadlocks occur when two or more threads are blocked waiting for locks held by each other. Prevent by ordering lock acquisition or using timeouts, and detect using thread dumps.
Detailed Explanation
A deadlock occurs when Thread 1 holds Lock A and waits for Lock B, while Thread 2 holds Lock B and waits for Lock A. Neither thread can proceed.
ReentrantLock.tryLock()), or minimize nested locking.jstack or jcmd. The JVM's built-in thread MXBean (ThreadMXBean.findDeadlockedThreads()) can detect deadlocks programmatically. You can also analyze dumps in tools like VisualVM or JProfiler.Code Example
// Prevent deadlock: acquire locks in consistent order
public void safeTransfer(Account from, Account to, double amt) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized(first) {
synchronized(second) {
from.withdraw(amt);
to.deposit(amt);
}
}
}
Common Interview Pitfalls
- Acquiring multiple locks (e.g. database connections, object monitors) in arbitrary order across different threads.
- Failing to set timeouts when calling blocking lock acquisitions in high-throughput APIs.
What are the different memory regions of the Java Virtual Machine (JVM), and what does each store?
Direct Answer
The JVM contains Heap for objects; Stack for frame primitives/references; Metaspace for class definitions; Program Counter registers; and Native Method Stacks.
Detailed Explanation
The JVM divides memory into run-time data areas:
Code Example
public class MemoryModel {
private static final int CONSTANT = 42; // Metaspace
private int value; // Heap (field of object instance)
public void execute() {
int localVal = 10; // JVM Stack frame
Object obj = new Object(); // Stack reference points to heap object
}
}
Common Interview Pitfalls
- Believing objects are stored on the stack (references are stored on the stack, objects reside on the heap).
- Configuring Metaspace using Heap flags (-Xmx), which leads to OutOfMemoryError in native memory regions.
What is the difference between the G1 and ZGC Garbage Collectors in Java?
Direct Answer
G1 is a generational collector targeting throughput and pauses; ZGC is a concurrent, low-latency collector targeting sub-millisecond pauses.
Detailed Explanation
Both are region-based collectors but target different goals:
Code Example
// Enable ZGC in JVM startup options:
// -XX:+UseZGC
// Enable G1GC (default):
// -XX:+UseG1GC
Common Interview Pitfalls
- Choosing ZGC for throughput-dominated offline batch processing where G1 or Parallel GC yields better CPU utilization.
- Assuming ZGC pause times increase proportionally with the size of the heap.
What are the main types of `OutOfMemoryError` in Java, and what causes each?
Direct Answer
Main OOM errors are Java heap space (object leaks), Metaspace (class-loader leaks), and GC overhead limit exceeded (JVM spends 98% time collecting < 2% heap).
Detailed Explanation
An OutOfMemoryError (OOM) occurs when the JVM cannot allocate memory for an object because the heap is full and GC cannot free space. Common types:
1. java.lang.OutOfMemoryError: Java heap space: The heap is full, often due to memory leaks or loading too much data (e.g. large SQL queries).
2. java.lang.OutOfMemoryError: Metaspace: Dynamic class loading/generation (e.g. CGLIB, proxies) leaks class definitions.
3. java.lang.OutOfMemoryError: GC overhead limit exceeded: The JVM spends more than 98% of its time performing GC and collects less than 2% of the heap, preventing forward progress.
Code Example
// Causing Heap OOM:
List<byte[]> list = new ArrayList<>();
while(true) {
list.add(new byte[1024 * 1024]); // Allocate 1MB continuously
}
Common Interview Pitfalls
- Simply increasing heap size (-Xmx) to fix an OOM caused by a memory leak, which only delays the failure.
- Ignoring Metaspace configuration limits in applications that load classes dynamically.
How does JIT compilation work in the JVM, and what is tiered compilation?
Direct Answer
JIT compilation compiles JVM bytecode into native machine code at runtime. Tiered compilation uses C1 (quick start) and C2 (high optimization) compilers.
Detailed Explanation
The JVM is a hybrid execution engine. Initially, it interprets bytecode. As methods run frequently (become "hot"), the Just-In-Time (JIT) compiler compiles them into native machine code.
Tiered compilation optimizes this process across 5 levels:
Code Example
// Hot method pattern:
public void process(int[] data) {
for (int i = 0; i < data.length; i++) {
data[i] = data[i] * 2; // Loops executed millions of times trigger C2 JIT optimization
}
}
Common Interview Pitfalls
- Running benchmarks without a warm-up phase, measuring interpreted code speed instead of JIT optimized speed.
- Writing long methods with complex branches, which can exceed compiler inline size thresholds.
What constitutes a memory leak in a garbage-collected language like Java, and what are common causes?
Direct Answer
A memory leak occurs when unused objects are kept in memory because they are still referenced by active objects, preventing GC reclamation.
Detailed Explanation
Garbage collection identifies and reclaims unreachable objects. A memory leak in Java occurs when objects that are no longer needed remain reachable from GC Roots. Common causes:
1. Static Collections: Objects added to static fields or maps remain in memory for the lifecycle of the JVM.
2. Unclosed Resources: Not closing file/network streams or database connections, which keeps memory blocks active.
3. ThreadLocal variables: In web servers using thread pools, failing to call ThreadLocal.remove() keeps variables bound to reusable threads.
Code Example
public class LeakDemo {
// Memory leak source: static map holds objects indefinitely
private static final Map<String, Object> cache = new HashMap<>();
public void process(String id, Object data) {
cache.put(id, data); // never removed or cleared
}
}
Common Interview Pitfalls
- Using HashMaps as local caches without setting eviction limits, maximum capacities, or using WeakHashMap.
- Failing to remove listeners or observers, keeping long-lived model subscriptions active.
How does Escape Analysis optimize heap allocations and locking in the JVM?
Direct Answer
Escape Analysis checks if an object is accessible outside its declaring method. If confined, the JVM optimizes via scalar replacement and lock elision.
Detailed Explanation
Escape Analysis is an optimization technique used by the C2 JIT compiler to analyze object scopes. An object is said to "escape" if it is returned from a method, stored in a static field, or passed to another thread.
If the compiler confirms an object does not escape its declaring method:
1. Scalar Replacement: The JVM avoids heap allocation. It breaks the object into its primitive fields and stores them directly on the stack frame (stack allocation equivalent).
2. Lock Elision (Lock Pruning): If the object is synchronized but confined to a single thread, the compiler removes the synchronization code.
Code Example
public void execute() {
// Point does not escape method.
// JIT compiler can perform scalar replacement and store fields x/y on the stack.
Point p = new Point(10, 20);
System.out.println(p.getX() + p.getY());
}
Common Interview Pitfalls
- Assuming all local objects are stack-allocated (escape analysis is an optimization performed by the JIT compiler, not guaranteed for all code layouts).
- Over-optimizing code manually in ways that break the compiler's escape analysis heuristics.
How does Java Serialization work, and what is the role of the `transient` modifier and `serialVersionUID`?
Direct Answer
Serialization converts an object into a byte stream. transient prevents fields from being serialized, and serialVersionUID validates class version compatibility.
Detailed Explanation
Serialization converts an object's state into a byte stream to write to files or send over networks. The class must implement java.io.Serializable.
transient are skipped during serialization, returning to default values (e.g. null, 0) upon deserialization. Use for passwords, tokens, or database connection handles.InvalidClassException. If undefined, the compiler generates one automatically, but class changes will break compatibility.Code Example
import java.io.Serializable;
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
// Transient field is skipped during serialization
private transient String sessionToken;
}
Common Interview Pitfalls
- Forgetting to declare serialVersionUID explicitly, leading to deserialization failures after minor class changes.
- Serializing objects containing non-serializable fields without marking them transient, which throws NotSerializableException.
How does Pattern Matching for `instanceof` simplify type checking and casting in Java 16+?
Direct Answer
Pattern matching for instanceof combines type checking and local variable casting into a single atomic statement, eliminating explicit casts.
Detailed Explanation
Before Java 16, using instanceof required checking the type and then writing an explicit cast statement to assign the object to a local variable.
Pattern Matching for instanceof (standardized in Java 16) allows declaring a pattern variable directly in the type check block. If the check succeeds, the variable is cast and available in the enclosing block scope, reducing boilerplate and preventing casting bugs.
Code Example
// Modern pattern matching instanceof:
if (obj instanceof String s) {
// Variable s is automatically cast and available here
System.out.println(s.toLowerCase());
}
Common Interview Pitfalls
- Trying to access the pattern variable in the "else" branch or outside the conditional scope where the type check is not guaranteed to be true.
- Declaring a separate variable with the same name, causing shadowing errors.
What are Sealed Classes and Interfaces, and what design goals do they achieve in Java 17?
Direct Answer
Sealed classes restrict which classes can extend or implement them, allowing developers to define closed, predictable domain hierarchies.
Detailed Explanation
Sealed classes (introduced in Java 17) allow a class or interface to specify which subclasses are permitted to extend it using the permits keyword. Permitted subclasses must reside in the same module/package and declare their extension type (final, sealed, or non-sealed). This provides closed hierarchy control, enabling the compiler to check for exhaustiveness in switch expressions, reducing the need for default catch-all clauses.
Code Example
public sealed interface Shape permits Circle, Square {}
public final class Circle implements Shape { /* ... */ }
public final class Square implements Shape { /* ... */ }
Common Interview Pitfalls
- Declaring permitted subclasses in different modules or packages (they must be in the same package or module).
- Forgetting that permitted subclasses must declare whether they are final, sealed, or non-sealed.
How does Pattern Matching for `switch` expressions work in Java 21, and how does it support algebraic data types?
Direct Answer
Switch pattern matching allows testing expressions against patterns (types, records) with guard clauses, enforcing compiler-checked exhaustiveness.
Detailed Explanation
Standardized in Java 21, Pattern Matching for switch allows switch statements and expressions to test selector expressions against type patterns, record patterns, and null values.
Combined with sealed hierarchies, it supports Algebraic Data Types (ADTs). The compiler checks that all possible branches of a sealed hierarchy are handled, making the switch exhaustive without requiring a default case. It also supports guard clauses (when) to refine matching conditions.
Code Example
public double getArea(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
// Exhaustive switch: No default case needed since Shape is sealed permits Circle/Square
};
}
Common Interview Pitfalls
- Forgetting to cover all permitted subclasses of a sealed type in a switch expression, causing compile-time errors.
- Using type patterns in switch statements on unsealed types without a default branch, making the switch non-exhaustive.
How does the JUnit 5 test lifecycle work, and what is the purpose of `@RegisterExtension`?
Direct Answer
JUnit 5 runs tests in separate class instances by default. Extensions are registered declaratively using @ExtendWith or programmatically via @RegisterExtension.
Detailed Explanation
JUnit 5 manages test isolation by instantiating a new test class instance for every @Test method execution by default (equivalent to @TestInstance(Lifecycle.PER_METHOD)). Annotations like @BeforeEach, @AfterEach, @BeforeAll, and @AfterAll control method execution setup.
The JUnit 5 extension model replaced the old runner and rule models. While @ExtendWith allows declarative extensions (like Spring Extension), @RegisterExtension allows programmatic extension registration. This is useful when the extension needs configuration (e.g. passing server ports, credentials) using constructor parameters.
Code Example
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.RegisterExtension;
class LifecycleTest {
@RegisterExtension
static CustomServerExtension server = new CustomServerExtension(8080); // configuration
@BeforeEach
void setUp() {}
@Test
void runTest() {}
}
Common Interview Pitfalls
- Expecting instance variables modified in one test method to be preserved for subsequent tests (since instance is recreated).
- Declaring @BeforeAll or @AfterAll methods as non-static when using PER_METHOD test instance lifecycle.
How do you implement database integration testing in Java using Testcontainers?
Direct Answer
Testcontainers automates launching docker database containers during test initialization, providing isolated database environments for integrations.
Detailed Explanation
Integration testing against shared development databases can cause test pollution and race conditions. Testcontainers provides lightweight, throwaway instances of databases or brokers inside Docker containers.
During test setup, Testcontainers downloads the required Docker image, starts the container, dynamically maps ports, and exposes connection parameters. You configure these connection coordinates in your test framework (e.g. Spring dynamic JDBC datasource properties), run tests, and Testcontainers destroys the container when the JVM exits.
Code Example
import org.testcontainers.containers.PostgreSQLContainer;
import org.junit.jupiter.api.*;
class DbIntegrationTest {
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
@BeforeAll
static void startContainer() {
postgres.start();
// Set dynamic JDBC URL, username, and password in configuration context
}
@Test
void testWrite() {
// Run SQL writes against local Docker container
}
}
Common Interview Pitfalls
- Failing to reuse container instances across test suites, causing extensive container startup overhead and slow build pipelines.
- Hardcoding mapped database ports inside tests instead of reading the dynamic ports exposed by Testcontainers.
What is the difference between a mock and a spy in Mockito?
Direct Answer
A mock creates a complete dummy object with default null/empty behavior, while a spy wraps a real object, delegating calls to original methods unless stubbed.
Detailed Explanation
Mockito supports different double types:
@Mock or Mockito.mock(Class). It creates a shell object. All method calls return default values (null, 0, empty collections) unless specifically stubbed using when().@Spy or Mockito.spy(Object). It wraps an active instance of a real class. All method calls are delegated to the real object's methods unless stubbed. Stubbing spies requires doReturn().when() syntax to avoid executing the real method during the stub setup.Code Example
import static org.mockito.Mockito.*;
import java.util.*;
public class MockitoTest {
public void test() {
List<String> list = new ArrayList<>();
List<String> spyList = spy(list);
// Stubbing spy: doReturn prevents calling real list.get(0) which would throw IndexOutOfBounds
doReturn("stubbed").when(spyList).get(0);
System.out.println(spyList.get(0)); // Prints "stubbed"
spyList.add("real");
System.out.println(spyList.get(1)); // Prints "real" (delegated to real list method)
}
}
Common Interview Pitfalls
- Using when(spy.method()).thenReturn() on spies, which invokes the real method and can cause NullPointerExceptions during stubbing.
- Mocking core language classes or data structures (like HashMap or String) instead of creating real instances.
How do you identify, analyze, and debug a memory leak in a production JVM environment?
Direct Answer
Monitor heap trends via JVM telemetry, trigger a heap dump using jcmd/jmap, and analyze class instances in Eclipse Memory Analyzer (MAT).
Detailed Explanation
Debugging memory leaks follows a structured path:
1. Monitoring: Track JVM telemetry in real time. If heap usage continuously grows after full GC runs, a leak is present.
2. Collection: Generate a binary heap dump (.hprof file) from production without shutting down the server, using jcmd <pid> GC.heap_dump /path/to/dump.hprof or jmap.
3. Analysis: Load the dump into Eclipse Memory Analyzer (MAT) or JProfiler. Run the "Leak Suspects" report, check for classes with high instance counts, and trace the path to GC Roots (specifically looking for "incoming references") to find which active object holds the references.
Code Example
# Triggering heap dump via jcmd:
jcmd 12345 GC.heap_dump /tmp/production_heap_dump.hprof
Common Interview Pitfalls
- Generating a heap dump on a system under high load without budgeting resource overhead, which can pause the JVM for seconds (STW).
- Looking only at shallow heap sizes of leak classes instead of checking their retained sizes.
How do you design and implement a thread-safe Least Recently Used (LRU) Cache in Java?
Direct Answer
Extend LinkedHashMap override removeEldestEntry() and wrap the map in Collections.synchronizedMap() or use ReentrantReadWriteLock.
Detailed Explanation
An LRU cache discards the least recently accessed elements when capacity is reached. In Java, LinkedHashMap provides a built-in hook via removeEldestEntry(). To configure it for access-order rather than insertion-order, instantiate it with accessOrder = true in the constructor.
Since LinkedHashMap is not thread-safe, you must wrap it in Collections.synchronizedMap() or implement fine-grained thread synchronization using a ReentrantReadWriteLock to protect read/write operations.
Code Example
import java.util.*;
public class LRUCache<K, V> {
private final Map<K, V> cache;
public LRUCache(int capacity) {
// accessOrder = true enables LRU tracking
this.cache = Collections.synchronizedMap(
new LinkedHashMap<K, V>(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
);
}
public V get(K key) { return cache.get(key); }
public void put(K key, V val) { cache.put(key, val); }
}
Common Interview Pitfalls
- Forgetting to configure accessOrder = true, resulting in an insertion-ordered cache instead of LRU cache.
- Allowing cache structures to grow unbounded without setting capacity limits, causing heap exhaustion.
How do you design a thread-safe counter under high concurrent write contention in Java?
Direct Answer
Use LongAdder instead of AtomicLong or synchronized blocks to prevent thread contention bottleneck by distributing cell cells internally.
Detailed Explanation
Under low thread contention, AtomicLong is fast and simple. However, under high concurrent write contention (many threads writing to the same counter), threads continuously spin in CAS loops waiting to update the single value, consuming high CPU.
LongAdder (introduced in Java 8) avoids this bottleneck. It maintains a dynamically sized array of cells. Each thread updates its own cell, and the total value is computed when calling sum(). This distributes write contention, improving throughput at the cost of slightly higher memory and relaxed consistency during sums.
Code Example
import java.util.concurrent.atomic.LongAdder;
public class HighContentionCounter {
private final LongAdder counter = new LongAdder();
public void increment() {
counter.increment(); // Distributes writes across internal cells
}
public long getCount() {
return counter.sum(); // Sums up cell values
}
}
Common Interview Pitfalls
- Using synchronized methods for simple counter increments, causing unnecessary thread blocking.
- Using AtomicLong for counters under massive concurrent writes, leading to high CPU spin cycles.
How do database transaction isolation levels protect data consistency, and how are they configured in Java applications?
Direct Answer
Isolation levels (Read Uncommitted, Read Committed, Repeatable Read, Serializable) prevent dirty reads, non-repeatable reads, and phantom reads. Configure via JDBC or @Transactional.
Detailed Explanation
Database isolation levels manage concurrency anomalies between transactions:
1. Read Uncommitted: Allows dirty reads (reading uncommitted writes).
2. Read Committed: Prevents dirty reads. Only reads committed data. Most databases default here.
3. Repeatable Read: Prevents non-repeatable reads (re-reading the same row returns identical data).
4. Serializable: Prevents phantom reads (prevents concurrent inserts in queries ranges by locking ranges).
In Java, configure these via JDBC connection parameters (Connection.setTransactionIsolation()) or in Spring using @Transactional(isolation = Isolation.REPEATABLE_READ). Higher isolation levels decrease database concurrency and throughput.
Code Example
// Spring Framework configuration:
// @Transactional(isolation = Isolation.READ_COMMITTED)
// public void processPayment(Payment p) {
// database.save(p);
// }
Common Interview Pitfalls
- Assuming that setting isolation levels in Java guarantees identical locking behavior across all database engines (behavior depends on the underlying DB like Postgres or MySQL).
- Using Serializable isolation level indiscriminately, causing high transaction rollbacks and database deadlocks.
How does a Circuit Breaker protect API resilience, and how is it configured in Java using Resilience4j?
Direct Answer
Circuit breakers prevent cascading failures. They transition between CLOSED (normal), OPEN (fail fast), and HALF_OPEN (test) states based on error rates.
Detailed Explanation
A Circuit Breaker wraps remote API calls. When the downstream service fails repeatedly, the breaker opens, causing subsequent calls to fail fast immediately, avoiding resource exhaustion (like thread pool starvation).
State transitions in Resilience4j:
Code Example
// Resilience4j programmatic configuration:
// CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(
// CircuitBreakerConfig.custom()
// .failureRateThreshold(50) // Open breaker if 50% calls fail
// .waitDurationInOpenState(Duration.ofSeconds(10))
// .build()
// );
// CircuitBreaker breaker = registry.circuitBreaker("paymentService");
Common Interview Pitfalls
- Setting wait durations too short in the OPEN state, causing the breaker to repeatedly ping failing systems before they recover.
- Failing to configure fallback behaviors (like returning cached data or error response objects) for fast-fail events.
Want to tailer your resume for Java Developer roles?
Import your resume, scan it for critical Java Developer keywords, and compare it against ATS standards instantly.