Java isn't going anywhere. It's still the backbone of enterprise software, fintech, Android development, and massive distributed systems. If you're interviewing for a backend or full-stack role in 2026, there's a very good chance Java is on the table.
These are the 50 questions that actually come up in real interviews - not obscure trivia about serialization edge cases or deprecated APIs nobody uses - plus three bonus deep dives (questions 51-53) on the modern Java features interviewers increasingly probe: sealed classes, pattern matching for switch, and where Java concurrency is heading after virtual threads. Each one includes a clear, direct answer you can study and internalize, and you can drill the same ground interactively with our Java practice questions. If you prefer Python or JavaScript, check out our top 50 Python interview questions and top 50 JavaScript interview questions as well. Let's get into it.
Practice alongside this post. The Java question bank drills the same ground as timed multiple-choice, the free daily challenge gives you one question a day with no account needed, and when you want to write code under time pressure the coding challenges run your solution against real test cases in the browser. Keep the concurrency essentials cheat sheet open for Tier 3 and the Big-O complexity cheat sheet for Tier 2. Questions 54-64 at the end are new for 2026: the modern garbage collector line-up, what changed between Java 17, 21 and 25, the scenario questions senior and backend loops now open with, low-latency Java, and the coding tasks that show up in a Java screen.
Tier 1: Core Java Fundamentals
These are table stakes. If you can't nail these, interviewers will move on quickly.
1. What's the difference between ==, .equals(), and hashCode()?
== compares references - it checks whether two variables point to the exact same object in memory. .equals() compares logical equality - whether two objects are meaningfully the same. hashCode() returns an integer used for hash-based collections like HashMap and HashSet.
The contract: if two objects are equal according to .equals(), they must have the same hashCode(). The reverse isn't required - two unequal objects can share a hash code (that's a collision).
String a = new String("hello");
String b = new String("hello");
a == b; // false - different objects in memory
a.equals(b); // true - same content
a.hashCode() == b.hashCode(); // true - required by the contract
If you override .equals(), always override hashCode() too. Forgetting this is one of the most common bugs in Java code.
2. Why are Strings immutable? What is the String pool?
Strings in Java are immutable - once created, their value can't change. This enables the String pool (also called the intern pool), a special area in heap memory where Java caches string literals. When you write "hello" twice, both references point to the same object.
Why immutability matters:
- Thread safety - immutable objects are inherently safe to share across threads
- Caching -
hashCode()can be computed once and cached - Security - strings used in class loading, network connections, and file paths can't be tampered with
String a = "hello"; // goes to String pool
String b = "hello"; // reuses same pool object
String c = new String("hello"); // creates new object on heap
a == b; // true - same pool reference
a == c; // false - c is a separate heap object
3. StringBuilder vs StringBuffer - when do you use each?
Both are mutable alternatives to String for building strings incrementally. The difference is thread safety.
- StringBuilder - not synchronized, faster, use this 99% of the time
- StringBuffer - synchronized, thread-safe, use only when multiple threads modify the same buffer (which is rare)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" World");
String result = sb.toString(); // "Hello World"
In practice, if you need thread-safe string building, you're probably better off using local variables or other concurrency patterns rather than StringBuffer.
4. What are primitive types vs wrapper classes? What is autoboxing?
Java has 8 primitive types: byte, short, int, long, float, double, char, boolean. These live on the stack and hold values directly.
Wrapper classes (Integer, Double, Boolean, etc.) are objects that wrap primitives. They live on the heap and can be null.
Autoboxing is Java's automatic conversion between primitives and wrappers:
Integer x = 42; // autoboxing: int -> Integer
int y = x; // unboxing: Integer -> int
// Watch out for null unboxing
Integer z = null;
int w = z; // NullPointerException!
Gotcha: Integer caches values from -128 to 127, so == works for small values but fails for larger ones:
Integer a = 127;
Integer b = 127;
a == b; // true - cached
Integer c = 128;
Integer d = 128;
c == d; // false - different objects
5. Explain final, finally, and finalize
Three completely different things that share a name:
- final - keyword that means "can't change." A final variable can't be reassigned, a final method can't be overridden, a final class can't be extended.
- finally - block in try-catch that always runs, regardless of whether an exception was thrown. Used for cleanup.
- finalize - deprecated method called by the garbage collector before reclaiming an object. Don't use it. Use
try-with-resourcesorCleanerinstead.
final int x = 10;
// x = 20; // compilation error
try {
riskyOperation();
} catch (Exception e) {
handleError(e);
} finally {
cleanup(); // always runs
}
6. What does the static keyword do?
static means "belongs to the class, not to an instance." It has four uses:
- Static variables - shared across all instances of a class
- Static methods - called on the class itself, can't access instance members
- Static blocks - run once when the class is loaded, used for initialization
- Static inner classes - nested classes that don't hold a reference to the outer class
public class Counter {
static int count = 0; // shared across all instances
static { // runs once at class load
System.out.println("Class loaded");
}
static void increment() { // called as Counter.increment()
count++;
}
}
7. What's the difference between method overloading and overriding?
Overloading - same method name, different parameters. Resolved at compile time (static polymorphism).
Overriding - subclass provides its own implementation of a parent's method. Resolved at runtime (dynamic polymorphism).
// Overloading - same class, different params
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
}
// Overriding - subclass replaces parent method
class Animal {
void speak() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void speak() { System.out.println("Woof!"); }
}
Key rules for overriding: same method signature, return type must be the same or a subtype (covariant return), access can't be more restrictive, and you can't override static or final methods.
8. Abstract class vs interface - what changed after Java 8?
Before Java 8, the rule was simple: interfaces had only abstract methods, abstract classes could have implementations. Java 8 changed the game with default and static methods in interfaces.
Use an abstract class when you need constructors, instance fields, or non-public methods. Classes can only extend one abstract class.
Use an interface when you're defining a capability or contract. Classes can implement multiple interfaces.
// Interface with default method (Java 8+)
interface Loggable {
default void log(String msg) {
System.out.println("[LOG] " + msg);
}
void process(); // abstract
}
// Abstract class with state
abstract class Vehicle {
protected int speed;
abstract void accelerate();
void stop() { speed = 0; }
}
Post-Java 8, the line is blurrier. The practical rule: prefer interfaces for defining behavior contracts, and use abstract classes when you need shared state. For more on these concepts, practice our object-oriented design questions.
9. What are the four access modifiers in Java?
- public - accessible from everywhere
- protected - accessible within the same package and by subclasses
- default (no modifier) - accessible within the same package only
- private - accessible only within the same class
Think of it as a spectrum from most open to most restricted: public > protected > default > private.
10. Explain checked vs unchecked exceptions and try-with-resources
Checked exceptions - extend Exception, must be caught or declared in the method signature. The compiler enforces this. Examples: IOException, SQLException.
Unchecked exceptions - extend RuntimeException, don't require explicit handling. Examples: NullPointerException, IllegalArgumentException.
Try-with-resources (Java 7+) automatically closes resources that implement AutoCloseable:
// Old way - verbose and error-prone
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
String line = br.readLine();
} finally {
if (br != null) br.close();
}
// Modern way - clean and safe
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
String line = br.readLine();
} // br.close() called automatically
11. How do generics work? What is type erasure?
Generics let you write type-safe code that works with different types. They're a compile-time feature - at runtime, generic type info is erased (replaced with Object or the upper bound).
List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(42); // compile error - type safety
// Type erasure means at runtime, this is just List<Object>
This is why you can't do new T() or instanceof T - the type info doesn't exist at runtime. It also means List<String> and List<Integer> are the same class at runtime.
Bounded types let you constrain generics:
// T must be Comparable
public <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
12. When should you use enums?
Use enums when you have a fixed set of constants that belong together. They're type-safe, can have methods and fields, and work great in switch statements.
public enum Status {
PENDING("Waiting"),
ACTIVE("Running"),
COMPLETED("Done");
private final String description;
Status(String description) {
this.description = description;
}
public String getDescription() { return description; }
}
// Usage
Status s = Status.ACTIVE;
switch (s) {
case PENDING -> handlePending();
case ACTIVE -> handleActive();
case COMPLETED -> handleCompleted();
}
Enums are singletons by design, implement Serializable, and are thread-safe. They're often the best way to implement the singleton pattern.
13. Is Java pass by value or pass by reference?
Java is always pass by value. Always. No exceptions.
The confusion comes from object references. When you pass an object to a method, you're passing a copy of the reference (the pointer) by value. You can modify the object through that reference, but you can't make the original variable point to a different object.
void changeValue(StringBuilder sb) {
sb.append(" World"); // modifies the original object
sb = new StringBuilder("New"); // only changes the local copy
}
StringBuilder original = new StringBuilder("Hello");
changeValue(original);
System.out.println(original); // "Hello World" - not "New"
14. How do you create an immutable object?
An immutable object can't be modified after creation. Here's the recipe:
- Make the class
final(prevent subclassing) - Make all fields
privateandfinal - Don't provide setters
- Return defensive copies of mutable fields
- Initialize everything in the constructor
public final class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}
Immutable objects are thread-safe, cacheable, and make great map keys.
15. What are record classes (Java 14+)?
Records are a compact way to create immutable data carriers. The compiler generates the constructor, getters, equals(), hashCode(), and toString() for you.
public record Point(int x, int y) {}
// That's it. You get:
Point p = new Point(3, 4);
p.x(); // 3
p.y(); // 4
p.toString(); // "Point[x=3, y=4]"
Records can have custom constructors (compact form), static methods, and implement interfaces. They can't extend other classes or have mutable fields. Use them for DTOs, value objects, and anywhere you'd previously write a boilerplate POJO.
Tier 2: Collections and Data Structures
Collections questions test whether you actually understand the data structures you use every day.
16. ArrayList vs LinkedList - when do you use each?
ArrayList - backed by a dynamic array. O(1) random access, O(1) amortized append, O(n) insert/delete in the middle.
LinkedList - doubly-linked list. O(n) random access, O(1) insert/delete at known positions, higher memory overhead per element.
In practice, ArrayList wins almost always. Modern CPUs love sequential memory access (cache locality), which arrays provide. LinkedList's theoretical O(1) insertions rarely overcome the cache miss penalty.
Use LinkedList only when you're doing heavy insertion/removal at both ends (like a deque) and never accessing by index.
17. How does HashMap work internally?
This is a favorite interview question. Here's how it works:
- Hashing -
key.hashCode()is computed and spread across buckets using bitwise operations - Buckets - the hash determines which bucket (array index) the entry goes into
- Collisions - when multiple keys map to the same bucket, entries are stored in a linked list
- Treeification (Java 8+) - when a bucket has more than 8 entries, the linked list converts to a red-black tree (O(log n) lookup instead of O(n))
- Resizing - when the load factor (default 0.75) is exceeded, the array doubles and entries are rehashed
Map<String, Integer> map = new HashMap<>(16, 0.75f);
map.put("key", 42);
// hashCode("key") -> bucket index -> store entry
Key requirement: keys must have consistent hashCode() and equals() implementations. Mutable keys in a HashMap are a recipe for lost entries.
18. HashMap vs TreeMap vs LinkedHashMap
- HashMap - O(1) average lookup, no ordering guarantee
- TreeMap - O(log n) lookup, keys sorted in natural order (or by a Comparator)
- LinkedHashMap - O(1) average lookup, maintains insertion order (or access order for LRU caches)
Map<String, Integer> hash = new HashMap<>(); // fast, unordered
Map<String, Integer> tree = new TreeMap<>(); // sorted by key
Map<String, Integer> linked = new LinkedHashMap<>(); // insertion order
Choose based on your needs: speed (HashMap), sorted keys (TreeMap), or predictable iteration (LinkedHashMap).
19. How does HashSet work under the hood?
HashSet is literally a HashMap where the values are a dummy constant object. When you call set.add(element), it calls map.put(element, PRESENT) internally.
This means HashSet inherits all of HashMap's properties: O(1) average add/remove/contains, requires proper hashCode() and equals(), and has no ordering guarantee.
20. ConcurrentHashMap vs Hashtable vs Collections.synchronizedMap
All three are thread-safe, but the mechanism differs:
- Hashtable - every method is
synchronized. One lock for the entire table. Very slow under contention. Legacy, don't use it. - Collections.synchronizedMap() - wraps a HashMap with a single lock. Same performance problem as Hashtable.
- ConcurrentHashMap - uses fine-grained locking (lock striping in Java 7, CAS operations + synchronized blocks on individual bins in Java 8+). Much better concurrent performance.
// Don't do this
Map<String, String> old = new Hashtable<>();
// Do this instead
Map<String, String> modern = new ConcurrentHashMap<>();
ConcurrentHashMap doesn't allow null keys or values (unlike HashMap). This is intentional - null creates ambiguity in concurrent contexts.
21. Iterator vs ListIterator, fail-fast vs fail-safe
Iterator traverses forward only. ListIterator can go both directions and supports add/set operations during iteration.
Fail-fast iterators (ArrayList, HashMap) throw ConcurrentModificationException if the collection is modified during iteration. They detect this using a modification counter.
Fail-safe iterators (ConcurrentHashMap, CopyOnWriteArrayList) work on a copy or snapshot, so modifications during iteration don't cause exceptions - but you might not see the latest changes.
// Fail-fast - throws ConcurrentModificationException
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
list.remove(s); // boom
}
// Safe way to remove during iteration
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("b")) it.remove(); // safe
}
22. Comparable vs Comparator
Comparable - the object defines its own natural ordering by implementing compareTo(). One ordering per class.
Comparator - an external comparison strategy. You can define multiple different orderings.
// Comparable - natural ordering
public class Employee implements Comparable<Employee> {
String name;
int salary;
@Override
public int compareTo(Employee other) {
return Integer.compare(this.salary, other.salary);
}
}
// Comparator - custom ordering
Comparator<Employee> byName = Comparator.comparing(e -> e.name);
Comparator<Employee> bySalaryDesc = Comparator.comparingInt(Employee::getSalary).reversed();
employees.sort(byName);
23. What Queue and Deque implementations should you know?
- LinkedList - implements both Queue and Deque. General purpose.
- ArrayDeque - faster than LinkedList for stack and queue operations. Preferred choice.
- PriorityQueue - elements ordered by priority (natural order or Comparator). Not FIFO.
- BlockingQueue (LinkedBlockingQueue, ArrayBlockingQueue) - thread-safe, blocks on empty/full. Essential for producer-consumer patterns.
Queue<String> queue = new ArrayDeque<>();
queue.offer("first");
queue.offer("second");
queue.poll(); // "first"
Deque<String> stack = new ArrayDeque<>();
stack.push("bottom");
stack.push("top");
stack.pop(); // "top"
24. Stream API - map, filter, reduce, collect
Streams provide a functional approach to processing collections. They're lazy (operations are chained and only execute on a terminal operation) and can be parallelized.
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
// filter + map + collect
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
// ["ALICE", "CHARLIE", "DAVID"]
// reduce
int sum = IntStream.rangeClosed(1, 10)
.reduce(0, Integer::sum); // 55
// grouping
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));
Key rule: streams are single-use. You can't reuse a stream after a terminal operation.
25. Optional - when to use it and when not to
Optional is a container that may or may not hold a value. It's designed to be a return type for methods that might not have a result.
Optional<User> findUser(String id) {
return Optional.ofNullable(userMap.get(id));
}
// Usage
String name = findUser("123")
.map(User::getName)
.orElse("Unknown");
Do use Optional for: method return types where "no result" is a valid outcome.
Don't use Optional for: fields, method parameters, collections (return empty collections instead), or when null is genuinely impossible. Don't use Optional.get() without checking - use orElse(), orElseGet(), or ifPresent().
Tier 3: Concurrency and Multithreading
Concurrency questions separate mid-level from senior candidates. These come up in almost every backend interview.
26. Thread vs Runnable vs Callable
Three ways to define work for a thread:
- Thread - extend
Threadclass. Inflexible because Java doesn't support multiple inheritance. - Runnable - implement
Runnableinterface. Returns void, can't throw checked exceptions. - Callable - implement
Callable<V>interface. Returns a value and can throw checked exceptions.
// Runnable - no return value
Runnable task = () -> System.out.println("Running");
// Callable - returns a value
Callable<Integer> computation = () -> {
Thread.sleep(1000);
return 42;
};
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(computation);
int result = future.get(); // blocks until done, returns 42
Always prefer Runnable/Callable with an ExecutorService over extending Thread directly.
27. How does the synchronized keyword work?
synchronized provides mutual exclusion - only one thread can execute a synchronized block/method at a time. It works by acquiring a monitor lock on an object.
// Synchronized method - locks on 'this'
public synchronized void increment() {
count++;
}
// Synchronized block - locks on specific object
public void increment() {
synchronized (this) {
count++;
}
}
// Lock on a specific object for finer control
private final Object lock = new Object();
public void safeUpdate() {
synchronized (lock) {
// critical section
}
}
synchronized also establishes a happens-before relationship, ensuring memory visibility between threads. For more flexible locking, consider ReentrantLock from java.util.concurrent.locks.
28. What does the volatile keyword do?
volatile guarantees visibility - when one thread writes to a volatile variable, all other threads immediately see the new value. Without volatile, threads might read stale cached values.
private volatile boolean running = true;
// Thread 1
public void stop() {
running = false; // immediately visible to other threads
}
// Thread 2
public void run() {
while (running) { // guaranteed to see the updated value
doWork();
}
}
volatile does not provide atomicity. count++ on a volatile variable is still not thread-safe because it's a read-modify-write operation. Use AtomicInteger for that.
29. What is a ThreadPool and how does ExecutorService work?
Thread pools reuse a fixed set of threads to execute tasks, avoiding the overhead of creating and destroying threads constantly.
// Fixed thread pool
ExecutorService pool = Executors.newFixedThreadPool(4);
// Submit tasks
pool.submit(() -> processOrder(order1));
pool.submit(() -> processOrder(order2));
// Shutdown gracefully
pool.shutdown();
pool.awaitTermination(30, TimeUnit.SECONDS);
Common pool types:
- newFixedThreadPool(n) - fixed number of threads. Good default choice.
- newCachedThreadPool() - grows/shrinks as needed. Good for short-lived tasks.
- newSingleThreadExecutor() - one thread, tasks execute sequentially.
- newScheduledThreadPool(n) - for delayed or periodic tasks.
In production, prefer ThreadPoolExecutor with explicit parameters over the Executors factory methods, so you control queue size and rejection policy.
30. How does CompletableFuture work?
CompletableFuture is Java's answer to async/await. It represents a future result that you can chain operations on, combine with other futures, and handle errors.
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> fetchUserFromDB(userId))
.thenApply(user -> user.getName())
.thenApply(String::toUpperCase)
.exceptionally(ex -> "UNKNOWN");
// Combining multiple futures
CompletableFuture<String> nameFuture = fetchNameAsync(id);
CompletableFuture<Integer> ageFuture = fetchAgeAsync(id);
CompletableFuture<String> combined = nameFuture
.thenCombine(ageFuture, (name, age) -> name + " is " + age);
Use thenApply for synchronous transforms, thenCompose for chaining async operations (like flatMap), and thenCombine for combining independent futures.
31. What is a deadlock and how do you prevent it?
A deadlock occurs when two or more threads are each waiting for a lock held by the other, creating a circular wait where nobody makes progress.
// Classic deadlock
// Thread 1: locks A, then tries to lock B
// Thread 2: locks B, then tries to lock A
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
// Meanwhile, another thread does:
synchronized (lockB) {
synchronized (lockA) { /* ... */ } // deadlock!
}
Prevention strategies:
- Lock ordering - always acquire locks in a consistent global order
- Timeouts - use
tryLock()with a timeout instead of blocking indefinitely - Lock-free algorithms - use atomic operations and concurrent collections
- Reduce lock scope - hold locks for the shortest time possible
32. What are race conditions?
A race condition happens when the behavior of code depends on the timing of thread execution. The result is unpredictable and changes between runs.
// Race condition - count++ is not atomic
private int count = 0;
// Two threads running this simultaneously
public void increment() {
count++; // read -> modify -> write (three operations)
}
Fixes:
synchronizedblockAtomicInteger.incrementAndGet()ReentrantLock
The key insight: any time multiple threads read and write shared mutable state without synchronization, you have a potential race condition.
33. CountDownLatch, CyclicBarrier, and Semaphore
Three coordination utilities from java.util.concurrent:
CountDownLatch - a one-shot gate. Threads wait until a counter reaches zero.
CountDownLatch latch = new CountDownLatch(3);
// Three worker threads each call latch.countDown() when done
// Main thread waits:
latch.await(); // blocks until count reaches 0
CyclicBarrier - reusable synchronization point. All threads wait until everyone arrives, then they all proceed together. Good for phased computations.
Semaphore - controls access to a limited number of resources. Like a bouncer with a counter.
Semaphore semaphore = new Semaphore(3); // 3 permits
semaphore.acquire(); // get a permit (blocks if none available)
try {
accessLimitedResource();
} finally {
semaphore.release(); // return the permit
}
34. What is ThreadLocal?
ThreadLocal gives each thread its own independent copy of a variable. No synchronization needed because threads never share the data.
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// Each thread gets its own SimpleDateFormat instance
String date = dateFormat.get().format(new Date());
Common uses: per-thread database connections, user context in web apps, non-thread-safe objects like SimpleDateFormat.
Warning: ThreadLocal values can cause memory leaks in thread pools because threads are reused. Always call remove() when done, especially in web applications.
35. What are virtual threads (Java 21+)?
Virtual threads are lightweight threads managed by the JVM rather than the OS. You can create millions of them without exhausting system resources.
// Old way - limited by OS thread count
ExecutorService pool = Executors.newFixedThreadPool(200);
// New way - millions of virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
// Each task gets its own virtual thread
String result = httpClient.send(request);
process(result);
});
}
}
Virtual threads are ideal for I/O-bound workloads (HTTP calls, database queries) where threads spend most of their time waiting. They're mounted on platform (OS) threads and automatically unmount when blocked.
Key points: don't pool virtual threads (create new ones instead), avoid synchronized in favor of ReentrantLock for long-blocking operations, and they won't help with CPU-bound work.
Tier 4: JVM and Performance
These questions show up more in senior-level interviews, especially at companies that deal with scale.
36. Explain the JVM memory model
The JVM divides memory into several areas:
- Heap - where objects live. Shared across all threads. Divided into Young Generation (Eden + Survivor spaces) and Old Generation.
- Stack - each thread gets its own stack. Stores local variables, method call frames, and references.
- Metaspace (replaced PermGen in Java 8) - stores class metadata, method bytecode, and the constant pool. Grows dynamically.
- Program Counter - each thread has one, tracks the current instruction.
- Native Method Stack - for native (JNI) method calls.
The key interview point: objects on the heap are shared between threads (synchronization needed), while stack data is thread-private (inherently safe).
37. How does garbage collection work? What are G1 and ZGC?
Garbage collection automatically reclaims memory from objects that are no longer reachable. The basic idea: start from GC roots (static fields, local variables, active threads), trace all reachable objects, and free everything else.
G1 (Garbage First) - the default since Java 9. Divides the heap into regions. Does mostly concurrent collection with predictable pause times. Good for heaps from a few hundred MB to several GB.
ZGC - ultra-low latency collector. Pause times under 1ms regardless of heap size. Handles multi-terabyte heaps. Great for latency-sensitive applications.
Generational hypothesis: most objects die young. Young generation collection (minor GC) is fast and frequent. Old generation collection (major GC) is slower and less frequent.
38. How does the ClassLoader hierarchy work?
Java uses a delegation model with three main classloaders:
- Bootstrap ClassLoader - loads core Java classes (java.lang, java.util) from the JDK
- Platform ClassLoader (was Extension ClassLoader) - loads platform modules
- Application ClassLoader - loads your application classes from the classpath
When a class needs to be loaded, the request goes up the chain (child delegates to parent). If the parent can't find it, the child tries. This prevents application code from replacing core Java classes.
Custom classloaders are used in app servers, plugin systems, and hot-reloading frameworks.
39. What is JIT compilation?
JIT (Just-In-Time) compilation is how the JVM optimizes code at runtime. Java bytecode starts out being interpreted, but the JVM identifies "hot" methods (frequently called) and compiles them to native machine code.
The JVM uses two compilers:
- C1 (Client) - fast compilation, basic optimizations. Used for methods called a few times.
- C2 (Server) - slower compilation, aggressive optimizations (inlining, escape analysis, loop unrolling). Used for very hot methods.
This is called tiered compilation, and it's why Java can sometimes match or beat C++ performance for long-running applications - the JIT has runtime profiling data that static compilers don't.
40. How do memory leaks happen in Java?
Java has garbage collection, but memory leaks still happen. They occur when objects are no longer needed but still have a reachable reference.
Common causes:
- Static collections that grow without bounds
- Listeners/callbacks that are registered but never removed
- ThreadLocal values in thread pools (threads are reused, values persist)
- Inner classes holding implicit references to outer class instances
- Unclosed resources (streams, connections)
- Custom caches without eviction policies
// Classic leak - static map grows forever
private static final Map<String, Object> cache = new HashMap<>();
public void processRequest(String key, Object data) {
cache.put(key, data); // never removed
}
Use weak references (WeakHashMap), bounded caches (Caffeine, Guava), and profiling tools to find leaks.
41. What tools do you use for JVM profiling and monitoring?
Key tools every Java developer should know:
- jstack - dumps thread stacks. Essential for diagnosing deadlocks and thread issues.
- jmap - creates heap dumps. Use with memory analysis tools.
- jconsole / VisualVM - graphical monitoring of heap, threads, CPU, and GC activity.
- jstat - command-line GC statistics.
- Java Flight Recorder (JFR) - low-overhead production profiling built into the JVM. Records events like method execution, GC pauses, and lock contention.
- async-profiler - sampling profiler for CPU and allocation profiling with minimal overhead.
In production, JFR is the go-to because it has negligible overhead and can run continuously.
42. What is String deduplication?
String deduplication is a G1 GC feature that identifies String objects with identical char[] (or byte[]) arrays and makes them share the underlying array. This can significantly reduce memory usage since strings often make up 25-40% of a typical Java heap.
Enable it with: -XX:+UseStringDeduplication
It only works with G1 (and ZGC in newer versions). It's not the same as String.intern() - deduplication works on the internal array, not the String object itself, and it happens automatically during GC.
Tier 5: Spring and Modern Java
If you're interviewing for a backend Java role, Spring Boot questions are nearly guaranteed.
43. What is dependency injection and IoC?
Inversion of Control (IoC) means the framework controls object creation and lifecycle, not your code. You declare what you need, and the framework provides it.
Dependency Injection (DI) is how IoC works in practice - the framework "injects" dependencies into your objects instead of you creating them with new.
// Without DI - tightly coupled
public class OrderService {
private PaymentGateway gateway = new StripeGateway(); // hard-coded
}
// With DI - loosely coupled
@Service
public class OrderService {
private final PaymentGateway gateway;
@Autowired // Spring injects the right implementation
public OrderService(PaymentGateway gateway) {
this.gateway = gateway;
}
}
Constructor injection is preferred over field injection because it makes dependencies explicit, supports immutability, and works with testing.
44. How does Spring Boot auto-configuration work?
Spring Boot scans your classpath and automatically configures beans based on what libraries are present. Add spring-boot-starter-data-jpa to your dependencies, and Spring Boot automatically configures a DataSource, EntityManager, and transaction manager.
Under the hood:
@SpringBootApplicationincludes@EnableAutoConfiguration- Spring reads
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - Each auto-configuration class has
@Conditionalannotations that check for classes, beans, or properties - Configurations only activate when conditions are met
You can override any auto-configured bean by defining your own. Use application.properties or application.yml to customize settings.
45. @Component vs @Service vs @Repository vs @Controller
Functionally, they're all the same - they mark a class as a Spring-managed bean. The difference is semantic:
- @Component - generic bean. Use when nothing else fits.
- @Service - business logic layer. No special behavior, just clarity.
- @Repository - data access layer. Spring adds automatic exception translation (converting database-specific exceptions to Spring's
DataAccessException). - @Controller - web layer. Handles HTTP requests when combined with
@RequestMapping.
@Repository
public class UserRepository { /* data access */ }
@Service
public class UserService { /* business logic */ }
@RestController // @Controller + @ResponseBody
public class UserController { /* HTTP endpoints */ }
46. What are Spring Bean scopes and lifecycle?
Scopes:
- singleton (default) - one instance per Spring container
- prototype - new instance every time it's requested
- request - one per HTTP request (web apps)
- session - one per HTTP session (web apps)
Lifecycle:
- Bean instantiation
- Dependency injection
@PostConstructmethod called- Bean is ready to use
@PreDestroymethod called on shutdown
@Service
@Scope("prototype") // new instance each time
public class ReportGenerator {
@PostConstruct
public void init() {
// runs after all dependencies are injected
}
@PreDestroy
public void cleanup() {
// runs before bean is destroyed
}
}
47. How do you build a REST API with Spring?
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
User user = userService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ErrorResponse> handleValidation(ValidationException ex) {
return ResponseEntity.badRequest()
.body(new ErrorResponse(ex.getMessage()));
}
}
Key annotations: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PathVariable, @RequestParam, @RequestBody, @Valid.
48. What are the basics of Spring Security?
Spring Security provides authentication (who are you?) and authorization (what can you do?) for Spring applications.
Core concepts:
- SecurityFilterChain - configures HTTP security rules
- UserDetailsService - loads user data for authentication
- PasswordEncoder - hashes passwords (use BCrypt)
- @PreAuthorize - method-level security
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}
49. What microservices patterns should you know?
The big ones that come up in interviews:
- Circuit Breaker - stops calling a failing service after a threshold, returns fallback. Prevents cascade failures. (Resilience4j is the standard library.)
- Service Discovery - services register themselves and find each other dynamically (Eureka, Consul, or Kubernetes DNS).
- API Gateway - single entry point that routes requests, handles auth, rate limiting (Spring Cloud Gateway, Kong).
- Saga Pattern - manages distributed transactions across services using compensating actions instead of two-phase commit.
- Event-Driven Architecture - services communicate via events/messages (Kafka, RabbitMQ) instead of synchronous HTTP.
Know when to use microservices (large teams, independent deployment needs) and when a modular monolith is simpler and better (most cases). Our system design practice questions cover these patterns in depth, and if your loop has a dedicated architecture round, our top 50 microservices interview questions go deeper on each of these.
50. What recent Java features should you know?
Java has been releasing features every 6 months. Here are the ones that come up in interviews:
Sealed classes (Java 17) - restrict which classes can extend/implement them.
public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
Pattern matching for instanceof (Java 16):
if (obj instanceof String s) {
System.out.println(s.toUpperCase()); // s is already cast
}
Switch expressions (Java 14):
String result = switch (day) {
case MONDAY, FRIDAY -> "Work hard";
case SATURDAY, SUNDAY -> "Relax";
default -> "Midweek grind";
};
Text blocks (Java 15):
String json = """
{
"name": "Alice",
"age": 30
}
""";
Java Platform Module System (JPMS, Java 9) - adds strong encapsulation to packages. In practice, most applications don't use modules directly yet, but libraries and the JDK itself do.
Bonus Tier: Modern Java Deep Dives
The three questions above only skimmed sealed classes and pattern matching. Interviewers in 2026 increasingly go deep on them - here's the full treatment, plus where Java concurrency is heading next.
51. What are sealed classes and interfaces, and why do they exist?
Sealed types (Java 17) let you restrict which classes can extend or implement a type. You declare the complete set of permitted subtypes with permits, and the compiler enforces it.
public sealed interface PaymentMethod
permits CreditCard, BankTransfer, Crypto {}
public record CreditCard(String number, String cvv) implements PaymentMethod {}
public record BankTransfer(String iban) implements PaymentMethod {}
public final class Crypto implements PaymentMethod { /* ... */ }
Every permitted subtype must itself declare one of three modifiers:
- final - the hierarchy stops here
- sealed - it continues, but with its own explicit
permitslist - non-sealed - it deliberately reopens this branch to arbitrary subclassing
Why it matters: sealed types model closed domains - a payment is a card, a transfer, or crypto, and nothing else. Before sealed classes, "don't extend this" was a Javadoc comment; now it's a compile-time guarantee. The killer feature is exhaustiveness: when you switch over a sealed type, the compiler knows every possible case, so you don't need a default branch - and adding a new permitted subtype breaks every switch that doesn't handle it, at compile time, everywhere in the codebase.
The interview framing: records give you the data (product types), sealed interfaces give you the alternatives (sum types), and pattern matching gives you the logic. Together they're Java's version of algebraic data types.
52. How does pattern matching for switch work (Java 21)?
Pattern matching for switch, finalized in Java 21, turns switch from "match on a constant" into "match on the type and shape of a value." Four pieces to know:
Type patterns - match on runtime type and bind a variable in one step:
String describe(Object obj) {
return switch (obj) {
case Integer i -> "int: " + i;
case String s -> "string of length " + s.length();
case null -> "null"; // explicit null case - no NPE
default -> "something else";
};
}
Guarded patterns - refine a case with a when clause:
String categorize(PaymentMethod pm) {
return switch (pm) {
case CreditCard c when c.number().startsWith("4") -> "Visa";
case CreditCard c -> "Other card";
case BankTransfer b -> "Transfer";
case Crypto cr -> "Crypto";
};
}
Record patterns - destructure a record's components in place, including nested records:
record Point(int x, int y) {}
record Line(Point start, Point end) {}
String describe(Object obj) {
return switch (obj) {
case Line(Point(var x1, var y1), Point(var x2, var y2))
when x1 == x2 -> "vertical line";
case Line l -> "some other line";
default -> "not a line";
};
}
Exhaustiveness with sealed types - the categorize example above has no default, and that's the point. Because PaymentMethod is sealed, the compiler verifies all cases are covered. Add a new permitted subtype, and this switch stops compiling until you handle it.
The interview point to land: this replaces the instanceof-and-cast chains and visitor patterns that used to dominate Java code that dispatches on type. If you're still writing if (x instanceof Foo) { Foo f = (Foo) x; ... } in 2026, the interviewer will notice.
53. Beyond virtual threads: what are structured concurrency and scoped values?
Virtual threads (question 35) solved "threads are expensive." Two follow-on features address "concurrent code is hard to reason about," and interviewers use them to test whether you've kept up with where Java concurrency is heading.
Structured concurrency (still in preview as of recent JDK releases) treats a group of related tasks as one unit with a single entry and exit point:
// Preview API - shape may still change
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> fetchUser(userId));
var order = scope.fork(() -> fetchOrders(userId));
scope.join(); // wait for both; if one fails, the other is cancelled
return new Profile(user.get(), order.get());
}
The guarantee: no task outlives its scope. If fetchUser throws, fetchOrders is cancelled automatically instead of leaking as an orphaned thread - the failure and cancellation semantics live in the structure of the code, not in manual Future bookkeeping.
Scoped values (finalized in Java 25) are the modern replacement for ThreadLocal in the virtual-thread world:
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> {
// CURRENT_USER.get() is visible here and in anything this calls,
// including forked subtasks in a structured scope
handleRequest();
});
Unlike ThreadLocal, a scoped value is immutable, automatically bounded to the dynamic scope of the run call, and cheap to inherit across the millions of virtual threads a server might create - which fixes exactly the ThreadLocal leak-in-a-pool problem from question 34.
The honest interview answer: most codebases aren't using these yet, and structured concurrency's API is still settling. What you're signaling by knowing them is that you understand the trajectory - virtual threads made concurrency cheap, and these make it correct by construction.
Bonus Tier 2: Common Java Interview Questions for 2026 - Modern JVM, Scenarios and Coding Rounds
Everything above is what a Java interview has asked for a decade. This tier is what changed. Interviewers in 2026 assume Java 17 as the floor and Java 21 as the norm, they ask scenario questions before trivia in senior and backend loops, and a growing share of Java screens end with a short coding task. Questions 54-64 cover each of those, in the order a loop tends to reach them.
54. Which garbage collector should you pick in 2026, and why?
Know the five collectors in the current JDK and the one-line reason to choose each:
- Serial - single-threaded, smallest footprint. The JVM picks it automatically on a machine (or container) with fewer than two CPUs or under about 1.8 GB of memory, which surprises people running small sidecars. Fine for tiny heaps; wrong for anything latency-sensitive.
- Parallel - multi-threaded stop-the-world collector that maximises throughput and does not care about pause length. Still the right answer for batch jobs and offline processing where total CPU time matters and nobody is waiting on a response.
- G1 (Garbage-First) - the default since Java 9. Region-based, mostly concurrent, targets a pause goal (
-XX:MaxGCPauseMillis, default 200 ms). The sensible default for services with heaps from a few hundred MB to tens of GB. - ZGC - concurrent, sub-millisecond pauses independent of heap size, scales to multi-terabyte heaps. Generational ZGC is the default ZGC mode since Java 23 and the non-generational mode was removed in Java 24, so "ZGC" now means generational ZGC. Pick it for latency-sensitive services where you can spend some extra CPU and memory to buy pause predictability.
- Shenandoah - OpenJDK's other low-pause collector, historically strongest in Red Hat builds; a generational mode arrived as experimental in Java 24 and matured in Java 25. Functionally a peer of ZGC; which one you reach for is usually a matter of which JDK build you run.
The interview answer is a decision rule, not a list: G1 unless you have a measured reason; ZGC (or Shenandoah) when p99 latency is the SLO and you have headroom; Parallel when it is a batch job; Serial when it is a tiny container. Then say how you would verify - -Xlog:gc* plus a JFR recording, compared before and after, because a GC swap without a measurement is a guess. Bonus point: -XX:+UseStringDeduplication (question 42) works with G1, ZGC and Shenandoah, not Parallel.
55. Records: what do interviewers probe beyond the basics?
Question 15 covered the syntax. The follow-ups are where records get interesting.
Validation in a compact constructor. The compact form runs before the fields are assigned, so it is the place to validate and normalise:
public record Money(BigDecimal amount, String currency) {
public Money {
Objects.requireNonNull(amount);
if (amount.scale() > 2) throw new IllegalArgumentException("max 2 decimal places");
currency = currency.toUpperCase(); // reassigns the parameter, not the field
}
}
Shallow immutability. A record's fields are final, but a List component is still a mutable list. Defensive-copy in the compact constructor (items = List.copyOf(items)) or the "immutable" record has a back door.
Records as map keys. equals and hashCode are generated from all components, which makes records ideal HashMap keys - as long as every component is itself immutable. A record holding an array uses reference equality for that array, which is a classic gotcha.
Records and JPA. A record cannot be a JPA entity - entities need a no-arg constructor and mutable state for the persistence context to manage. Records are the right shape for DTOs and query projections (Spring Data supports record-based projections directly), which is exactly the separation a good backend interview wants you to draw.
Records vs Lombok @Value. Same goal, but a record is a language feature with a guaranteed contract, no annotation processor, and full pattern-matching support (question 52). The honest nuance: Lombok still wins when you need a builder or a mutable class, which records deliberately refuse to be.
56. What changed between Java 17, 21 and 25 that interviewers actually ask about?
Java 25 (September 2025) is the current long-term-support release, so the three LTS versions you are likely to meet in production are 17, 21 and 25. Know one sentence per feature:
Java 17 (2021) - sealed classes and interfaces finalised; records (16) and pattern matching for instanceof (16) are effectively "17 features" in practice; text blocks; the strong encapsulation of JDK internals that broke old reflection hacks.
Java 21 (2023) - virtual threads finalised; pattern matching for switch and record patterns finalised; sequenced collections (getFirst(), getLast(), reversed() on List, Deque, LinkedHashSet, LinkedHashMap) so you stop writing list.get(list.size() - 1); generational ZGC arrived (experimental); the string-templates preview that was later withdrawn - do not cite it as a current feature.
Java 25 (2025) - scoped values finalised (the ThreadLocal replacement from question 53); module import declarations (import module java.base;); compact source files and instance main methods finalised, so void main() { IO.println("hello"); } is a complete program; flexible constructor bodies (statements are allowed before super(...) as long as they do not touch this); compact object headers as a supported option (-XX:+UseCompactObjectHeaders, smaller objects and better cache behaviour); AOT profiling and command-line ergonomics for faster startup; primitive types in patterns still in preview; structured concurrency still in preview.
The senior-level framing: 17 modernised the type system, 21 modernised concurrency and dispatch, 25 is about startup, footprint and ergonomics. If you are asked which to target for a new service in 2026, the answer is 21 or 25 with a reason - 25 if your frameworks and observability agents have caught up (Spring Boot 3.x and 4 both run on it), 21 if you need the broadest tooling support today.
57. Sequenced collections: what problem did Java 21 solve?
Before 21, "first element" and "last element" had six different spellings depending on the collection type, and LinkedHashSet had no way at all to get its last element. Java 21 added three interfaces - SequencedCollection, SequencedSet, SequencedMap - retrofitted onto every collection with a defined encounter order:
List<String> names = new ArrayList<>(List.of("a", "b", "c"));
names.getFirst(); // "a"
names.getLast(); // "c"
names.reversed(); // a live reversed *view*, not a copy
names.addFirst("z");
LinkedHashMap<String, Integer> m = new LinkedHashMap<>();
m.putLast("k", 1);
m.firstEntry(); m.lastEntry(); m.pollFirstEntry();
The gotcha interviewers like: reversed() is a view, so writes through it mutate the original. And HashSet/HashMap are not sequenced - they have no encounter order to sequence.
58. Scenario: a Spring Boot service's p99 latency spikes every few minutes. Walk me through it.
This is the most common senior Java opener in 2026. The interviewer wants a method, not a guess. A strong answer moves from cheapest signal to most expensive:
- Correlate the spikes with GC first.
-Xlog:gc*or a JFR recording. Periodic spikes every few minutes with the same amplitude are the signature of old-generation collections on G1 or of humongous allocations. If the pause length matches the latency spike, the fix conversation is heap sizing, allocation rate, or a collector change (question 54), in that order. - If GC is clean, look at thread pools. A pool sized at 200 with a slow downstream call fills up, requests queue, latency climbs until the downstream recovers - a sawtooth, not a spike, but it is described as spikes.
jstackor JFR thread dumps during a spike show hundreds of threads parked on the same socket read. The fix is a timeout and a bulkhead, not a bigger pool. - Then connection pools. HikariCP at its default of 10 connections is enough until it isn't; a burst of slow queries exhausts it and everything waits on
getConnection(). Hikari'sconnectionTimeoutand the pool-wait metrics tell you instantly. - Then the JIT. A spike shortly after deploy that disappears is warmup - C2 compiling hot paths (question 39). Periodic spikes on a long-running service are not the JIT, but deoptimisation storms after a class-loading event can look like it; JFR shows them.
- Then the box. CPU throttling in a container with a low
cpu.limit, a noisy neighbour, a log rotation that blocks on disk.kubectl topand the throttling counters in cgroup stats.
Land the meta-point: you are naming which measurement rules out which cause, and you are not changing anything until one of them points somewhere.
59. Scenario: the service leaks memory in production and dies with OutOfMemoryError once a day. How do you find it?
- Capture the evidence first.
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumpsso the next crash hands you the heap. In the meantime,jcmd <pid> GC.class_histograma few times an hour apart shows which classes are growing. - Read the dump with a dominator tree, not a class list. Eclipse MAT or VisualVM: the dominator tree tells you which single object is keeping the memory reachable, which is usually one static map, one cache without eviction, or one listener list (question 40).
- The usual suspects in Spring apps: an unbounded
@Cacheablecache with no TTL, aThreadLocalin a filter that never callsremove()on a pooled thread,ClassLoaderleaks on hot redeploy, and aCompletableFuturechain that holds request bodies in closures while it waits on a slow downstream. - Confirm the fix with the same measurement - the class histogram flattens over the same time window where it used to climb.
The point the interviewer is scoring: you reached for a heap dump and a dominator tree, not for a bigger -Xmx.
60. Scenario: a word counter on a ConcurrentHashMap still produces wrong totals. Why?
// Broken - thread-safe map, non-atomic update
Integer old = counts.get(word);
counts.put(word, old == null ? 1 : old + 1);
ConcurrentHashMap makes each operation atomic, not the sequence of a get followed by a put. Two threads read the same old value and both write old + 1. This is check-then-act, the same shape as question 32. The fixes, from simplest to fastest under contention:
counts.merge(word, 1, Integer::sum); // atomic read-modify-write
counts.compute(word, (k, v) -> v == null ? 1 : v + 1); // same, more general
// Highest throughput under heavy contention: a striped counter
Map<String, LongAdder> counts = new ConcurrentHashMap<>();
counts.computeIfAbsent(word, k -> new LongAdder()).increment();
The interviewer is checking that you know the difference between a thread-safe collection and a thread-safe algorithm, and that LongAdder exists for exactly the hot-counter case where AtomicLong becomes a contention point.
61. Scenario: the endpoint that lists orders makes 1,001 SQL queries. What happened and how do you fix it?
The N+1 problem: one query loads 1,000 orders, then a lazy @ManyToOne or @OneToMany fires one query per order when you touch the association. Hibernate is doing exactly what you told it to.
Fixes, in the order you should reach for them:
JOIN FETCHin the JPQL for a specific query:select o from Order o join fetch o.customer where ....@EntityGraphon a Spring Data repository method when you want to declare the fetch plan without rewriting the query.@BatchSize(size = 50)orhibernate.default_batch_fetch_sizeto turn N queries into N/50INqueries when a join would explode the row count (collections of collections).- A DTO projection (a record, per question 55) when the endpoint does not need entities at all - one query, no persistence-context bookkeeping, and it is usually the right answer for a read-only list endpoint.
And the configuration point that separates seniors: turn off open-session-in-view (spring.jpa.open-in-view=false). Its default of true is what lets a lazy load fire from the controller or the JSON serialiser, which is why the N+1 was invisible in tests and only appeared in production logs.
62. What has changed in Spring Boot that interviewers ask about in 2026?
- The Jakarta namespace and the Java 17 baseline - Spring Boot 3 (on Spring Framework 6) moved every
javax.*import tojakarta.*and requires Java 17+. Spring Boot 4 (November 2025, on Spring Framework 7) keeps that baseline. If a candidate's Spring knowledge is alljavax.servletandWebSecurityConfigurerAdapter, it shows. - Virtual threads with one property -
spring.threads.virtual.enabled=trueputs the embedded server and@Asyncon virtual threads. The interview follow-up is when it helps (I/O-bound request handling with many concurrent requests) and when it does not (CPU-bound work, or code that pins the carrier thread insidesynchronizedblocks - largely fixed in Java 24, but still worth knowing about for older JDKs). RestClientreplacesRestTemplatefor synchronous HTTP - a fluent API on the same infrastructure asWebClient, without dragging in the reactive stack.RestTemplateis in maintenance mode.- Declarative HTTP interfaces -
@HttpExchangeon an interface, and Spring generates the client, the same shape Feign popularised. - Observability through Micrometer -
ObservationAPI, traces and metrics from the same instrumentation, OpenTelemetry export out of the box. "How would you trace a request across three services?" now has a one-paragraph Spring answer. - GraalVM native images -
./mvnw -Pnative native:compileproduces a binary with millisecond startup. Know the trade-off: build time and reflection configuration in exchange for startup and footprint, which matters for serverless and scale-to-zero and rarely matters for a long-running service. - Security config as a lambda DSL -
SecurityFilterChainbeans (question 48) replaced the adapter class; theand()chaining style is gone.
63. Low-latency Java: what do the trading and ad-tech loops ask?
If the job description says "low latency", expect a different Java interview. The questions are about not using the parts of Java everyone else relies on:
- Allocation is the enemy. Every allocation is future GC work. Hot paths use primitives, pre-sized arrays, object pools and flyweights; they avoid autoboxing (question 4), streams, lambdas that capture, and string concatenation.
-XX:+PrintCompilationand JFR's allocation profiling find the offenders. - Choose the collector for the tail. ZGC for services that still allocate; for the extreme case, size the heap so the process never collects during the trading day and restart nightly, or use Epsilon, the no-op collector, to prove a path is allocation-free (it crashes the moment it is not).
- Warm the JIT deliberately. Replay representative traffic at startup so C2 has compiled the hot paths before real traffic arrives; know that the first call to anything is interpreted (question 39). Java 25's AOT profiling cache (question 56) is the platform's answer to this.
- False sharing. Two hot fields written by different threads on the same 64-byte cache line ping-pong between cores. Pad them, or use
@Contended(needs-XX:-RestrictContended). This is the question that tells the interviewer whether you have ever profiled at the cache level. - Lock-free structures. Single-producer single-consumer ring buffers (the Disruptor pattern),
VarHandlewith acquire/release semantics instead ofvolatileeverywhere,LongAdderoverAtomicLongfor counters. Know whysynchronizedis wrong here even when it is uncontended (the JIT can inflate it, and it pins virtual threads on older JDKs). - Off-heap and mechanical sympathy. Direct
ByteBuffers or the Foreign Function & Memory API (final since Java 22) for large buffers the GC should not walk; thread affinity so a hot thread never migrates cores;-XX:+AlwaysPreTouchso page faults happen at startup, not on the first order.
You are not expected to have done all of this. You are expected to know that this list exists and why each item is on it.
64. What coding questions come up in a Java interview screen?
Most Java screens end with a 15-20 minute coding task. They are rarely algorithmically hard; they are testing that you write idiomatic, correct Java quickly. The recurring set:
Reverse a string, then do it without StringBuilder.
String reversed = new StringBuilder(s).reverse().toString();
// Without StringBuilder - two-pointer swap on a char array
char[] c = s.toCharArray();
for (int i = 0, j = c.length - 1; i < j; i++, j--) {
char t = c[i]; c[i] = c[j]; c[j] = t;
}
return new String(c);
Are two strings anagrams? Sort both and compare, O(n log n), or count characters in an int[26] (or a Map<Character,Integer> for Unicode), O(n). Say which and why.
First non-repeating character. One pass to count into a LinkedHashMap<Character,Integer> (preserves order), one pass to find the first entry with count 1. The interviewer wants to hear why LinkedHashMap and not HashMap.
Two Sum. A HashMap<Integer,Integer> from value to index; for each element check whether target - x is already present. O(n) time, O(n) space. Practise the follow-up: sorted input means two pointers and O(1) space.
An LRU cache.
class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
LruCache(int capacity) {
super(capacity, 0.75f, true); // accessOrder = true
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
Six lines, and it demonstrates you know LinkedHashMap has an access-order mode (question 18). The follow-up is "now make it thread-safe" - wrap it with Collections.synchronizedMap for the interview, and mention Caffeine for production.
Producer-consumer. A BlockingQueue (question 23) with a poison-pill sentinel to stop the consumers cleanly. Interviewers are checking you do not reach for wait()/notify() first.
A thread-safe singleton. Either an enum (question 12) or the holder idiom:
class Config {
private Config() {}
private static class Holder { static final Config INSTANCE = new Config(); }
static Config get() { return Holder.INSTANCE; }
}
Lazy, thread-safe by the class-loading guarantee, no volatile, no double-checked locking to get wrong.
Word frequency, top N, with streams.
Map<String, Long> freq = words.stream()
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
List<String> topN = freq.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()))
.limit(n)
.map(Map.Entry::getKey)
.toList();
The tie-break on key is what separates a correct answer from a nondeterministic one, and .toList() (Java 16) instead of .collect(Collectors.toList()) signals you have written Java recently.
When you want more of these under a timer, the coding challenges run your solution against hidden test cases, and the coding interview patterns guide covers the pattern behind each one.
Quick-Fire: Java Interview Questions and Answers
The rapid round interviewers use in the first ten minutes to decide how deep to go. One sentence each.
- JDK vs JRE vs JVM - the JVM runs bytecode; the JRE is the JVM plus the standard library; the JDK is the JRE plus the compiler and tools. Since Java 11 there is no separate JRE download - you ship a
jlinked runtime or the whole JDK. - Why is
mainstatic? - so the JVM can call it without constructing an instance of the class. (Java 25's instancemainmethods relax this for small programs; the classic form still works everywhere.) String,StringBuilder,StringBuffer- immutable, mutable and unsynchronised, mutable and synchronised. Use the first two.- Can a
tryblock have afinallywith nocatch? - yes, and if bothtryandfinallyreturn, thefinallyvalue wins and thetryvalue is silently discarded, which is why you neverreturnfromfinally. - What does
transientdo? - excludes a field from default Java serialisation. It has nothing to do with threads;volatiledoes. - Two interfaces with the same default method - which wins? - neither; the class must override it and may call
A.super.method()explicitly. Class implementations always beat interface defaults. Integer a = 127, b = 127; a == b? - true, because of the -128 to 127 cache; at 128 it is false. Use.equals().- How do you compare arrays? -
Arrays.equals(orArrays.deepEqualsfor nested arrays).array1.equals(array2)is reference equality and==on arrays is always wrong. List.of(...)vsArrays.asList(...)-List.ofis fully immutable and rejectsnull;Arrays.asListis a fixed-size view over the array that allowssetbut notadd.- Can you use a checked exception in a lambda passed to
stream().map()? - not directly;Functiondoes not declare one. Wrap it, use a helper that sneaky-throws, or restructure. Interviewers ask this to see if you have hit it. var- local type inference (Java 10). The type is still static and fixed at compile time;varis notdynamic.hashCodeof a record - generated from all components, consistent with the generatedequals. Do not override one without the other.String.intern()- returns the canonical pooled instance. Almost never the right tool since G1's string deduplication (question 42) exists.- Marker interface - an interface with no methods (
Serializable,Cloneable) that exists to be tested withinstanceof. Annotations are the modern replacement. instanceofvsgetClass()inequals-instanceofallows subclasses to be equal to parents and breaks symmetry;getClass()is strict. Records andfinalclasses avoid the question entirely.Iterator.remove()vslist.remove()in a loop - the first is safe during iteration; the second throwsConcurrentModificationException(question 21).removeIfis the modern one-liner.- What is a daemon thread? - a thread that does not keep the JVM alive. Virtual threads are always daemon threads, which is the trick answer.
Objectmethods everyone should know -equals,hashCode,toString,getClass,wait/notify/notifyAll,clone(protected),finalize(deprecated for removal).
How to Approach Java Interviews
Knowing the answers is only half the battle. Here's how to actually perform well:
Think out loud. Interviewers want to see your reasoning process, not just the final answer. Walk through your thought process.
Know the trade-offs. "It depends" is often the right answer - as long as you can explain what it depends on. ArrayList vs LinkedList, synchronized vs ConcurrentHashMap, microservices vs monolith - always discuss trade-offs.
Write code, not paragraphs. When explaining something like HashMap internals or a concurrency pattern, sketch the code. It demonstrates real understanding.
Go deep on a few topics. It's better to have deep knowledge of collections, concurrency, and Spring than shallow knowledge of everything. Interviewers follow up, and surface-level answers fall apart under follow-ups.
Practice under pressure. Reading about these concepts is different from explaining them in a live interview. Practice answering out loud, on a whiteboard, or in an AI mock interview.
If you're looking for a place to practice Java questions in an interview-like setting, check out our practice questions. You can work through Java fundamentals, data structures, and backend concepts with instant feedback and track your progress over time. When you're ready to write actual code under time pressure, our coding challenges run your solutions against real test cases in the browser. For algorithm prep, our DSA cheat sheet for coding interviews pairs well with this guide.
Keep going
- Drill it under pressure: the Java question bank is timed multiple-choice on exactly this material, the free daily challenge gives you one question a day with no account, and the chat-based AI mock interview runs the follow-up questions a real interviewer would.
- Keep the references open: the concurrency essentials cheat sheet for Tier 3 and questions 60 and 63, the Big-O complexity cheat sheet for the collections trade-offs, and the REST API design cheat sheet for the Spring questions.
- Round out a backend loop: Top 50 database interview questions for the JPA and SQL follow-ups, Top 30 system design interview questions for the architecture round, Top 50 microservices interview questions and Top 50 Kafka interview questions for the distributed-systems questions, and Top 50 Kotlin and Android interview questions if the role touches mobile. Most Java backend loops also include an on-call or operations round, and Top 50 Bash and shell scripting interview questions covers the shell debugging that shows up there.
Good luck out there. You've got this.