Java interview questions have a reputation for being a vocabulary test: name the four pillars of OOP, list the collection interfaces, say what final means. The first round sometimes is. After that, the questions change shape. Someone shows you a class used as a HashMap key, a counter shared by two threads or a stream with a side effect, and asks what happens. The answer is never in the syntax. It is in what the language and the JVM guarantee.
That is the pattern behind almost every Java loop for backend roles in 2026. Five areas cover nearly all of it:
- Objects, equality and collections -
==versusequals,hashCode, how aHashMapfinds a key, records - Lambdas, streams and Optional - laziness, functional interfaces and side effects
- Exceptions and resources - checked versus unchecked, try-with-resources and
finally - Concurrency - executors, futures, locks,
volatile, atomics and virtual threads - The JVM: memory and garbage collection - stack versus heap, collectors and leaks
Take the quiz before reading on. Five questions are enough to show which of those areas you know from habit and which you know from the rules.
Start here: a 5-question self-check
Five questions from the Java set on squizzu, one per area. Each answer comes with a short explanation, and a longer one sits under it if you want the detail. No account needed.
The Callable question catches more experienced developers than you would expect, because most code submits work to an executor without thinking about which interface the lambda became. The Optional question is the other common miss: Optional.of and Optional.ofNullable look interchangeable until a null arrives. If either cost you a point, read areas 2 and 4 first.
What a Java interview looks like in 2026
Java is the backend language of banks, marketplaces, logistics and a large share of enterprise software, so the interview usually sits inside a backend loop alongside system design and SQL. Three kinds of question recur.
Predict the behaviour. A short snippet and the question "what does this print?" or "what goes wrong?". These check the rules: string identity, integer caching, equals without hashCode, an exception thrown from finally, a volatile counter.
Write idiomatic code. A small task such as grouping orders by customer or de-duplicating events, where the interviewer watches whether you reach for records, streams, Map.merge and Collectors.groupingBy, or write loops and mutable state out of habit.
Explain a production problem. A service is slow under load, a heap keeps growing, a scheduled job occasionally double-processes records. These are the senior questions, and they draw on areas 4 and 5.
What changed is the baseline. Most teams now run Java 21 or Java 25, the two most recent long-term-support releases, so interviewers expect records, sealed interfaces, pattern matching in switch and text blocks to be part of your everyday vocabulary. Virtual threads, final since Java 21, are now a standard concurrency question rather than a curiosity.
Area 1: Objects, equality and collections
Almost every question here follows from one distinction: == compares references, equals compares whatever the class defines as equal. For primitives, == compares values and there is nothing else to know. For objects, == asks whether two variables point at the same object.
Strings are where this bites first. String literals are interned, so two literals with the same text usually refer to one pooled object and == happens to return true. A string built at runtime, read from a request or created with new String(...), is a different object, and == returns false even when the characters match. Code that "works in tests" with literals and fails in production with real input is the classic outcome, and the rule is simply to compare strings with equals.
Wrapper types have the same trap with a twist. Integer.valueOf caches the values from -128 to 127. Two boxed Integer objects holding 100 are therefore usually the same object, while two holding 1000 are not:
Integer a = 100, b = 100;
Integer c = 1000, d = 1000;
System.out.println(a == b); // true - cached
System.out.println(c == d); // false - two objects
equals and hashCode are a pair. A HashMap uses hashCode to choose a bucket and then equals to find the entry inside it. The contract is that equal objects must return equal hash codes. Override equals without hashCode and two logically equal keys land in different buckets, so get returns null and a HashSet happily stores "duplicates". The reverse is allowed: different objects may share a hash code, which only costs performance.
A related question: what happens if you change a field used by hashCode after the object is already a key? The entry stays in the bucket chosen by the old hash, and lookups with the new state look somewhere else. Keys should be immutable.
Records are the modern answer to both problems. A record declares its components once, and the compiler generates a constructor, accessors, equals, hashCode and toString from them. Its fields are final, which makes records natural map keys and DTOs. The immutability is shallow, though: a record holding a List still exposes a mutable list unless you copy it with List.copyOf in the constructor.
The collections questions that remain are mostly about choosing. ArrayList is backed by an array and is the default list; LinkedList rarely wins in practice despite its cheap insertions. HashMap allows one null key, ConcurrentHashMap allows none, and TreeMap keeps keys sorted by compareTo, which is why compareTo should agree with equals. List.of and Map.of return unmodifiable collections that reject nulls, and calling add on one throws UnsupportedOperationException.
What the interviewer is testing: whether you can predict how a collection behaves from the contracts its elements follow. Common follow-up: "Why is String immutable?" Because it is used for keys, class names, file paths and security checks, and caching and sharing it is only safe if nobody can change it.
Area 2: Lambdas, streams and Optional
A lambda is an implementation of a functional interface, an interface with a single abstract method. The ones in java.util.function come up constantly: Function<T, R> takes a value and returns one, Consumer<T> takes a value and returns nothing, Supplier<T> takes nothing and returns a value, and Predicate<T> returns a boolean. A lambda can read local variables from the enclosing method only if they are effectively final, because it captures their value, not the variable.
Streams are lazy. Intermediate operations such as filter and map only describe the pipeline. Nothing runs until a terminal operation like toList, collect, count or findFirst asks for a result. Then elements flow through the whole chain one at a time, rather than each stage processing the entire collection before the next, which is why a findFirst after a filter can stop after the first match. Two consequences are regular interview material:
- A stream can be consumed once. Calling a second terminal operation on the same stream throws
IllegalStateException. Keep the collection or aSupplier<Stream<T>>if you need to traverse twice. - Side effects are a smell. A
mapthat writes to a shared list, or aforEachthat modifies the source collection, breaks the model. Modifying the source typically throwsConcurrentModificationException, and on a parallel streamforEachdoes not even preserve encounter order. Collect results with a collector instead.
Stream.toList(), added in Java 16, returns an unmodifiable list and is the idiomatic replacement for collect(Collectors.toList()) when you do not need to mutate the result.
Optional exists to make "there might be no value" part of a method's return type. It has two factories that look interchangeable and are not. Optional.of(value) insists on a non-null argument and throws NullPointerException immediately if it gets null; use it when null would be a bug. Optional.ofNullable(value) returns an empty Optional for null; use it at the boundary with code that may return null. Consume an Optional with map, orElse, orElseGet or orElseThrow rather than calling get after isPresent, which is a null check in disguise. The design guidance interviewers look for: Optional is for return types, not fields, parameters or collection elements.
Modern Java adds two tools that often replace chains of if and instanceof. Sealed interfaces list exactly which classes may implement them, and pattern matching in switch can then check that every case is handled:
sealed interface Payment permits Card, Transfer {}
record Card(String last4) implements Payment {}
record Transfer(String iban) implements Payment {}
String describe(Payment p) {
return switch (p) {
case Card c -> "card ending " + c.last4();
case Transfer t -> "transfer from " + t.iban();
}; // no default needed: the compiler knows every case
}
What the interviewer is testing: whether you write declarative Java without hidden state. Common follow-up: "When would you not use a stream?" For simple loops with early exits and checked exceptions, or hot paths where a plain loop is clearer and measurably faster.
Area 3: Exceptions and resources
Java splits exceptions into checked ones, which the compiler forces you to catch or declare, and unchecked ones: RuntimeException and its subclasses, plus Error. Checked exceptions model conditions a caller can reasonably handle, such as a missing file. Unchecked exceptions model programming errors, such as a null where none was allowed. A common design question is what to do with a checked exception you cannot recover from. The usual answer is to wrap it in an unchecked exception and pass the original as the cause, so the stack trace keeps the root problem.
try-with-resources is the standard way to release files, sockets, database connections and locks. Any object whose class implements AutoCloseable can be declared in the try header, and Java calls its close method when the block exits, normally or by exception. Several resources are closed in reverse order of declaration:
try (var conn = dataSource.getConnection();
var stmt = conn.prepareStatement(sql)) {
// use stmt
} // stmt.close(), then conn.close(), even if the body throws
finally is where exceptions get lost. A finally block runs whether the try block returns, completes or throws, and that is its job. But if code inside finally throws, its exception replaces the one already propagating, and the original error disappears from the logs. try-with-resources handles this better: if the body throws and close also throws, the body's exception is the one you see, and the exception from close is attached to it as a suppressed exception, available through getSuppressed(). That difference is a favourite senior question, and it is the reason manual finally cleanup is now considered a code smell.
The best-practice questions are about restraint. Catch the most specific exception you can handle. Do not catch Throwable or Error: an OutOfMemoryError or StackOverflowError means the JVM is in trouble, and application code rarely has a sensible response. Do not swallow an exception with an empty catch, and do not log it and rethrow it at every layer, which prints the same stack trace five times.
What the interviewer is testing: whether failures in your code leave the system in a known state and leave a trail you can debug. Common follow-up: "An overriding method wants to throw a new checked exception. Can it?" No. It may throw the same checked exceptions as the method it overrides, narrower ones or none, so that callers written against the parent type still compile.
Area 4: Concurrency
Java concurrency questions start with how work is submitted. Very little code creates a Thread directly any more. Instead, tasks go to an ExecutorService, which manages a pool of threads, and the task comes in two shapes:
Runnablehas arun()method that returns nothing and cannot throw checked exceptions.Callable<V>has acall()method that returns a value of typeVand may throw checked exceptions.
Submitting a Callable returns a Future<V>, a handle to the pending result. future.get() blocks until the task finishes and, if the task threw, rethrows the failure wrapped in an ExecutionException. For composing several asynchronous steps without blocking, CompletableFuture is the usual follow-up.
Shared mutable state is where the real questions are. synchronized gives mutual exclusion and visibility: only one thread at a time holds an object's monitor, and whatever a thread wrote before releasing the lock is visible to the next thread that takes it. A synchronized instance method locks this, while a synchronized static method locks the Class object, so the two do not block each other. In library code, locking on a private final object rather than this stops outside code from interfering with your lock.
volatile is not a lock. It guarantees that a write to the field is visible to any thread that reads it afterwards, and it stops the compiler and CPU from reordering around it. It does not make a read-modify-write atomic:
private volatile int count;
void hit() { count++; } // read, add, write - two threads can lose an update
volatile is right for a flag that one thread sets and others read, such as a shutdown signal. For counters, use AtomicInteger or, under heavy contention, LongAdder. For invariants that span several fields, use a lock. The formal rule underneath all three is the Java Memory Model's happens-before relation: without one between a write and a read, the reading thread is allowed to see a stale value.
Virtual threads change the scaling question rather than the correctness one. A virtual thread is scheduled by the JVM onto a small number of platform threads, and when it blocks on I/O it unmounts, so a server can run a hundred thousand concurrent blocking requests without a hundred thousand operating-system threads. Executors.newVirtualThreadPerTaskExecutor() creates one virtual thread per task, and since executors implement AutoCloseable, a try-with-resources block waits for all of them to finish. Three points make the answer complete. Virtual threads do not speed up CPU-bound work. They should not be pooled, because they are cheap to create. And before Java 24, blocking inside a synchronized block pinned the virtual thread to its carrier thread, a limitation that release removed.
What the interviewer is testing: whether you know what each tool guarantees, so you pick the smallest one that is correct. Common follow-up: "How do you prevent deadlock?" Acquire locks in a consistent global order, hold them briefly, and prefer tryLock with a timeout from ReentrantLock where ordering cannot be guaranteed.
Area 5: The JVM: memory and garbage collection
Every thread has its own stack. Each method call pushes a frame holding that call's parameters and local variables, and the frame disappears when the method returns. Local variables of primitive types hold their values directly; local variables of reference types hold only the reference. The objects themselves, and all arrays, live on the heap, which every thread shares and the garbage collector manages. Deep or infinite recursion exhausts a thread's stack and throws StackOverflowError; too many live objects exhaust the heap and throw OutOfMemoryError.
Garbage collection is based on reachability. An object can be collected once no chain of references leads to it from a GC root, such as a local variable on some thread's stack or a static field. Becoming unreachable makes an object eligible; it does not say when collection will happen. System.gc() is only a request, and the JVM is free to ignore it, which is why code that depends on it is wrong.
Most collectors are generational, because most objects die young. New objects are allocated in the young generation, which is collected often and cheaply, and objects that survive several collections are promoted to the old generation. G1 is the default collector in HotSpot and balances throughput with predictable pauses. ZGC targets very short pauses on large heaps and has run in generational mode by default since Java 23. Interviewers rarely expect tuning flags; they expect you to know that the choice is a trade-off between throughput, latency and memory overhead, and that GC logs are where the evidence lives.
Memory leaks still happen in Java. They are objects that are still reachable but no longer needed. Typical sources are a static Map used as a cache with no eviction, listeners registered and never removed, and a ThreadLocal left set on a pooled thread. The garbage collector cannot free them because, from its point of view, they are in use. The usual diagnosis is a heap dump taken when memory is high, opened in a tool such as Eclipse MAT to see which objects dominate the heap and what keeps them reachable.
finalize() is the historical wrong answer to cleanup. It was never guaranteed to run, is deprecated for removal, and has been replaced by try-with-resources for deterministic cleanup and Cleaner for the rare cases where a safety net is needed.
What the interviewer is testing: whether you can reason about a production memory problem from first principles. Common follow-up: "The heap grows slowly for days, then the service falls over. What do you do?" Confirm the trend in GC logs or metrics, take heap dumps at two points in time, compare which object types grew, and follow their references back to the GC root holding them.
A one-week Java interview plan
For developers who write Java regularly and want to answer from the rules rather than from memory.
- Day 1 - Equality. Write a small class, use it as a
HashMapkey withequalsbut nohashCode, and watch the lookup fail. Then turn it into a record and repeat. Test==on strings and boxed integers at the edges of the cache. - Day 2 - Streams. Rewrite three loop-heavy methods with streams and collectors such as
groupingByandtoMap. Put apeekwith a print in a lazy pipeline and confirm when it runs. - Day 3 - Modern types. Model a small domain with a sealed interface, records and a
switchwith patterns. Remove one case and read the compiler error. - Day 4 - Exceptions. Throw from both a
trybody and afinallyblock, then do the same with try-with-resources and inspectgetSuppressed(). - Day 5 - Concurrency. Increment a shared counter from two threads with a plain
int, avolatile intand anAtomicInteger, and compare the totals. SubmitCallabletasks and handle anExecutionException. - Day 6 - Virtual threads and memory. Run ten thousand blocking tasks on a fixed pool and on a virtual-thread executor and time both. Then write a deliberate leak with a static map, take a heap dump and find it.
- Day 7 - Rehearse. Explain out loud, in two minutes each: the
equalsandhashCodecontract, whatvolatiledoes and does not guarantee, and how you would investigate a growing heap.
Keep practising
Five questions cover a small slice of the language. The full Java set has more than 150 reviewed questions across OOP, core APIs, streams, exceptions, concurrency, memory, modules and testing.
Work through the Java questions on squizzu. Every answer comes with an explanation of why the wrong options are wrong, which is what short predict-the-output questions reward.
Backend loops rarely stop at the language. The SQL interview questions for 2026 cover the database round that usually sits next to a Java interview, and the system design round is covered separately. If you also deploy what you build, DevOps interview questions for 2026 follows a change from commit to production, and Python interview questions for 2026 is the same guide for the other common backend language.
