I’ve sat on both sides of the Java interview table enough times to notice a pattern: candidates who can recite definitions often freeze the moment you ask “why” or “what happens if.” This guide is organized the way interviews actually flow — core language fundamentals, then collections, then concurrency, then the Streams/Optional questions that trip up even experienced developers, and finally the Spring basics that show up in almost every Java backend role today. Each question includes a real, runnable example and a note on the specific way candidates get it wrong, because the wrong answer is usually more instructive than the right one.
Core Java
Q1: What is the difference between == and .equals()?
== compares references for objects (whether two variables point to the same memory location) and values for primitives. .equals() compares logical equality, as defined by the class. The classic trap is String interning:
String a = "hello";
String b = "hello";
String c = new String("hello");
System.out.println(a == b); // true - both point to the same interned literal
System.out.println(a == c); // false - c is a new heap object
System.out.println(a.equals(c)); // true - same characters
Why this trips people up: candidates memorize “use .equals() for Strings” without understanding the string pool, so when asked to predict output for new String("hello") they guess instead of reasoning through it. Interviewers ask this specifically to see if you understand the pool, not just the rule.
Q2: Why are Strings immutable in Java?
Three reasons interviewers want to hear: string pool safety (mutable pooled strings would let one reference corrupt all others sharing that literal), thread safety without synchronization, and security (class loader paths, network connections, and reflection arguments often pass through Strings — if they were mutable, validated values could change after the check).
Q3: Checked vs unchecked exceptions — what’s the actual design intent?
Checked exceptions (subclasses of Exception excluding RuntimeException) represent recoverable conditions the caller is expected to handle — a file not existing, a network call failing. Unchecked exceptions (RuntimeException and its subclasses, plus Error) represent programming bugs or unrecoverable states — a null dereference, an out-of-bounds index.
| Aspect | Checked Exception | Unchecked Exception |
|---|---|---|
| Base class | Exception (not RuntimeException) | RuntimeException or Error |
| Compiler enforcement | Must declare with throws or catch | No compiler requirement |
| Examples | IOException, SQLException | NullPointerException, IllegalArgumentException |
| Typical cause | External failure (I/O, network, parsing) | Programming error / invalid state |
| Recovery expectation | Caller is expected to recover or retry | Usually indicates a bug to fix, not catch |
| Common criticism | Encourages catch-and-ignore or wrapping noise | Easy to let propagate unnoticed until runtime |
Why this trips people up: candidates say “checked exceptions must be caught” which is wrong — they must be either caught or declared. They also rarely mention that modern API design (Spring, most reactive libraries) leans heavily toward unchecked exceptions because checked exceptions don’t compose well with lambdas — a Function<T, R> can’t declare throws IOException.
Q4: What is the difference between final, finally, and finalize()?
- final — a modifier for variables (constant reference), methods (can’t override), or classes (can’t extend).
- finally — a block that always executes after try/catch, used for cleanup.
- finalize() — a deprecated
Objectmethod the GC used to call before reclaiming an object; deprecated since Java 9 and slated for removal because its timing is non-deterministic. Usetry-with-resources orCleanerinstead.
Collections
Q5: HashMap vs ConcurrentHashMap — when does it actually matter?
This is one of the most common interview questions, and I’ve written a full HashMap vs ConcurrentHashMap interview guide covering internals, but the short version for an interview answer:
| Feature | HashMap | ConcurrentHashMap |
|---|---|---|
| Thread safety | Not thread-safe | Thread-safe via bucket-level locking (Java 8+) |
| Null keys/values | One null key, multiple null values allowed | Neither null keys nor null values allowed |
| Iterator behavior | Fail-fast (ConcurrentModificationException) | Weakly consistent, never throws CME |
| Performance (single thread) | Faster — no locking overhead | Slightly slower due to lock striping |
| Performance (concurrent) | Requires external sync, contention-prone | Scales well, only contended buckets lock |
Why this trips people up: candidates jump straight to “ConcurrentHashMap is thread-safe” without explaining the locking granularity. A strong answer mentions that pre-Java 8 used segment locking and Java 8+ synchronizes only the head node of a contended bucket — that one detail signals you’ve actually read the source, not just a blog post about it.
Q6: ArrayList vs LinkedList — and why the “LinkedList is faster for inserts” answer is usually wrong
The textbook answer is “LinkedList has O(1) insertion, ArrayList has O(n).” In practice, LinkedList loses on raw insertion benchmarks too, once you account for cache locality — each node is a separate heap allocation with two pointers, while ArrayList is a contiguous backing array that the CPU prefetches efficiently. LinkedList only wins when you’re inserting/removing from the middle of a list you’re already iterating with an Iterator (O(1) removal at the cursor vs O(n) for ArrayList’s array shift).
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() == 3) {
it.remove(); // safe O(1)-amortized removal during iteration
}
}
System.out.println(list); // [1, 2, 4, 5]
Q7: What does it mean for a class to be Comparable vs Comparator?
Comparable<T> is implemented by the class itself to define its single “natural” ordering via compareTo(). Comparator<T> is a separate strategy object that defines an ordering externally, letting you sort the same class multiple different ways without modifying it.
record Employee(String name, int salary) {}
List<Employee> employees = new ArrayList<>(List.of(
new Employee("Dana", 95000),
new Employee("Alex", 72000)
));
// Comparator: external, composable, no change to Employee needed
employees.sort(Comparator.comparing(Employee::salary).reversed());
System.out.println(employees);
// [Employee[name=Dana, salary=95000], Employee[name=Alex, salary=72000]]
Concurrency
Q8: What’s the difference between synchronized and a ReentrantLock?
synchronized is a JVM-managed intrinsic lock — simple, automatically released even on exception, but inflexible. ReentrantLock (from java.util.concurrent.locks) gives you tryLock() with timeouts, interruptible lock acquisition, and fairness policies, at the cost of requiring you to manually unlock in a finally block.
private final ReentrantLock lock = new ReentrantLock();
public void updateBalance(int amount) {
lock.lock();
try {
balance += amount;
} finally {
lock.unlock(); // forgetting this finally is the #1 ReentrantLock bug
}
}
Why this trips people up: candidates forget that synchronized releases the lock automatically if an exception is thrown inside the block, but a manually acquired ReentrantLock does not — if you skip the finally, you’ve created a permanent deadlock for any other thread waiting on that lock.
Q9: What is a race condition, and how do you actually demonstrate one in an interview?
A race condition occurs when the correctness of a result depends on the timing/interleaving of concurrent operations. The classic demo is a non-atomic increment:
class Counter {
private int count = 0;
public void increment() { count++; } // read-modify-write: NOT atomic
public int get() { return count; }
}
// With 1000 threads each calling increment() once, the final count
// is frequently less than 1000 because two threads can read the
// same value before either writes back the incremented result.
The fix is AtomicInteger, a synchronized method, or a ReentrantLock — know all three and when each is appropriate (AtomicInteger for a single counter, synchronized/lock for multi-field invariants).
Streams and Optional
Q10: Why shouldn’t you call Optional.get() without checking isPresent()?
Because get() throws NoSuchElementException on an empty Optional — it’s just as dangerous as a raw null dereference, except now it gives a false sense of safety. The idiomatic fix is to never call get() directly; use orElse(), orElseGet(), orElseThrow(), or map()/ifPresent() instead.
Optional<String> maybeName = Optional.empty();
// Bad: defeats the purpose of Optional entirely
if (maybeName.isPresent()) {
System.out.println(maybeName.get());
}
// Idiomatic
String name = maybeName.orElse("Unknown");
maybeName.ifPresent(System.out::println);
String required = maybeName.orElseThrow(() ->
new IllegalStateException("name must be present"));
Why this trips people up: interviewers specifically ask this to see if you treat Optional as documentation for “this might be absent, handle it functionally” or just as a fancy null wrapper you unwrap immediately — the latter defeats the entire point.
Q11: What’s the difference between map() and flatMap() in Streams?
map() transforms each element 1-to-1. flatMap() transforms each element into a stream and flattens all resulting streams into one — essential when each input maps to zero, one, or many outputs.
List<List<String>> nested = List.of(
List.of("a", "b"),
List.of("c"),
List.of("d", "e", "f")
);
// map() would give you Stream<List<String>> (still nested)
// flatMap() flattens to Stream<String>
List<String> flat = nested.stream()
.flatMap(List::stream)
.toList();
System.out.println(flat); // [a, b, c, d, e, f]
For a deeper dive into Collectors patterns like groupingBy and partitioningBy, see my Java Streams API Deep Dive + Collectors Cookbook.
Q12: Are Streams lazy? Prove it.
Yes. Intermediate operations (filter, map) don’t execute until a terminal operation (collect, forEach, count) is invoked. You can prove it with a side-effecting peek():
Stream<Integer> stream = Stream.of(1, 2, 3)
.peek(n -> System.out.println("saw " + n))
.filter(n -> n % 2 == 0);
System.out.println("Stream built, nothing printed yet");
long count = stream.count(); // now "saw 1", "saw 2", "saw 3" print
Spring Basics
Q13: What is dependency injection, and why constructor injection over field injection?
Dependency injection means a class declares what it needs and an external container (Spring’s ApplicationContext) supplies it, instead of the class constructing its own dependencies. Constructor injection is preferred over @Autowired field injection because it makes dependencies explicit and final, fails fast at startup if a bean is missing, and makes the class testable without reflection hacks (you can just call the constructor in a unit test with mocks).
@Service
public class OrderService {
private final PaymentClient paymentClient;
private final OrderRepository orderRepository;
// Constructor injection: Spring 4.3+ doesn't even need @Autowired
// here if there's exactly one constructor.
public OrderService(PaymentClient paymentClient, OrderRepository orderRepository) {
this.paymentClient = paymentClient;
this.orderRepository = orderRepository;
}
}
Q14: What’s the default Spring bean scope, and when would you change it?
Default is singleton — one instance per ApplicationContext, shared across the application. You’d switch to prototype for stateful, non-thread-safe beans that need a fresh instance per injection point, or to request/session scope (web-aware contexts only) for data that must be isolated per HTTP request or user session.
Why this trips people up: candidates forget that singleton scope means the bean itself must be stateless or thread-safe, since every request shares it — putting a mutable instance field on a @Service bean without synchronization is a bug, not a style choice.
Q15: What’s the difference between @Component, @Service, @Repository, and @Controller?
Functionally, they’re all @Component meta-annotations — Spring’s classpath scanner treats them identically for bean registration. The difference is semantic documentation and a few targeted behaviors: @Repository enables automatic exception translation (vendor-specific JDBC exceptions become Spring’s DataAccessException hierarchy), and @Controller is what Spring MVC’s request-mapping infrastructure scans for. @Service has no special behavior — it’s purely a convention signaling “business logic lives here.”
FAQs
Should I memorize Big-O complexities for every collection?
Know the common ones cold: ArrayList get O(1), LinkedList get O(n), HashMap get/put O(1) average, TreeMap O(log n). Interviewers care more about whether you can explain why than whether you can recite a table.
Do interviewers still ask about Java 8 features, or has the bar moved to newer versions?
Java 8 (Streams, Optional, lambdas) is still the baseline most interviews test, since it’s the most widely deployed LTS in legacy enterprise codebases. But it’s increasingly common to get asked about records (Java 16+), pattern matching for switch (Java 21), and virtual threads (Java 21+) if the role touches anything built in the last two years.
Is it bad to say “I don’t know” in a technical interview?
No — it’s far better than confidently guessing wrong. The strongest answer I’ve heard to a question I expected nobody to fully know was “I haven’t used that directly, but based on how X works I’d expect Y, let me reason through it” followed by actually reasoning through it correctly.
See Also
- Java HashMap vs ConcurrentHashMap: Complete Interview Guide
- Java Generics: The Complete Developer’s Reference
- Java Streams API Deep Dive + Collectors Cookbook
- Java Records: The Complete Guide to Immutable Data Classes
Conclusion
None of these questions are designed to catch you out for sport — each one maps to a real production bug somebody has shipped at some point (a mutable singleton bean, a forgotten lock.unlock(), a checked exception swallowed in a catch block). If you can explain not just the answer but the failure mode it prevents, you’ve already separated yourself from most candidates who stop at the definition.