Java interviews at mid-to-senior level expect you to reason about the JVM, not just write syntax. The questions below cover what interviewers actually probe - from garbage collection and the Java Memory Model to the nuances of the collections framework and the Streams API.
JVM and garbage collection
The JVM is the foundation every Java question rests on. Understanding how memory is laid out and how objects are reclaimed explains a large class of performance and correctness issues.
| Question | Sharp answer |
|---|---|
| What are the JVM memory areas? | Heap (object instances and arrays, GC-managed), Metaspace (class metadata, replaces PermGen since Java 8), Stack (one per thread - frames for method calls, local variables, operand stack), PC register, and native method stack. |
| How does generational GC work? | Most objects die young (weak generational hypothesis). The heap is split into Young (Eden + two Survivor spaces) and Old (Tenured). Minor GC collects Young quickly; Major/Full GC collects Old. Objects that survive several Minor GCs are promoted to Old. |
| What is G1 GC? | Garbage-First divides the heap into equal-sized regions rather than fixed generational areas. It collects the regions with the most garbage first, which lets it meet soft pause-time goals. Default since JDK 9. |
| What triggers an OutOfMemoryError? | Heap exhaustion (too many live objects), Metaspace exhaustion (too many loaded classes, common with reflection or class-loader leaks), or a StackOverflowError from deep/infinite recursion (technically a different error but often conflated). |
| What is the difference between the stack and the heap? | Stack memory is thread-local, holds primitive local variables and references (not the objects they point to), and is freed automatically when a frame pops. Heap memory holds all object instances and is shared across threads; it is GC-managed. |
Collections framework
Choosing the wrong collection is a red flag. Know the concrete implementations, their complexity guarantees, and the trade-offs.
| Type | Implementation | When to use |
|---|---|---|
| List | ArrayList | Random access, end-appends. O(1) get/set, O(n) insert-at-index. |
| List | LinkedList | Frequent insert/remove at head or interior. O(1) at known node, O(n) index lookup. Rarely the right call in practice. |
| Map | HashMap | O(1) average get/put. No ordering guarantee. Allows one null key. |
| Map | LinkedHashMap | Insertion-order or access-order iteration over a HashMap. Useful for LRU caches. |
| Map | TreeMap | O(log n) operations, keys sorted by natural order or Comparator. Use when you need floorKey/ceilingKey/subMap. |
| Set | HashSet | O(1) average contains/add. No ordering. Backed by a HashMap. |
| Set | TreeSet | O(log n), sorted. Backed by a TreeMap. |
| Queue | PriorityQueue | Min-heap by default. O(log n) offer/poll, O(1) peek. Pass a Comparator for max-heap or custom ordering. |
| Deque | ArrayDeque | O(1) amortised push/pop at both ends. Preferred over Stack and LinkedList for stack/queue use. |
| Map (concurrent) | ConcurrentHashMap | Thread-safe, higher concurrency than Hashtable (segment-level locking in Java 7, CAS in Java 8+). Never use Hashtable in new code. |
Concurrency
Java concurrency questions range from threading fundamentals to the Java Memory Model. This is a deep area; know the vocabulary and the modern APIs.
| Question | Sharp answer |
|---|---|
| What does synchronized do? | Acquires a monitor lock on the object (or class for static methods) before entering the block/method, ensuring mutual exclusion and establishing a happens-before relationship for memory visibility. |
| What is volatile? | Guarantees visibility: reads/writes go directly to main memory, not a CPU cache. It does NOT make compound actions (check-then-act, increment) atomic. Use AtomicInteger or synchronized for compound operations. |
| What is the Java Memory Model? | The JMM defines which memory operations are visible to which threads. It specifies happens-before relationships: a write that happens-before a read guarantees the read sees the write. Actions that establish happens-before: synchronized unlock/lock, volatile write/read, thread start, thread join. |
| ExecutorService vs raw threads? | ExecutorService decouples task submission from thread management. It maintains a thread pool, handles reuse, and exposes Future for results. Always prefer it over new Thread() - raw threads leak easily and lack backpressure. |
| What is a race condition vs a deadlock? | Race condition: outcome depends on thread scheduling - typically from unsynchronized reads/writes to shared mutable state. Deadlock: two or more threads each hold a lock and wait for the other, blocking forever. Prevent deadlock by acquiring locks in a consistent global order. |
| ReentrantLock vs synchronized? | ReentrantLock from java.util.concurrent.locks offers tryLock (non-blocking acquisition), timed lock, fairness policy, and multiple Condition variables. synchronized is syntactically simpler and sufficient for most cases; use ReentrantLock when you need those extras. |
equals / hashCode contract
Violating this contract silently breaks HashMap, HashSet, and any collection that relies on hashing. Interviewers test it because many engineers get it wrong.
| Rule | Why it matters |
|---|---|
| If a.equals(b) then a.hashCode() == b.hashCode() | HashMap uses hashCode to find the bucket, then equals to find the key. If equal objects have different hashCodes, the map will not find a previously stored key. |
| The converse is NOT required | Two unequal objects may have the same hashCode (collision). HashMap handles this with chaining or tree bins (Java 8+). A function that returns 42 for everything is a valid but terrible hashCode. |
| Override both or neither | The default Object.equals is identity (==); the default Object.hashCode is based on memory address. If you override equals to compare by fields, you must override hashCode to maintain the contract. |
| equals must be reflexive, symmetric, transitive, consistent, and handle null | x.equals(null) must return false (not throw). Consistency means the result must not change between calls unless a field used in comparison is mutated. |
Generics and type erasure
| Question | Sharp answer |
|---|---|
| What is type erasure? | Generic type parameters are removed at compile time and replaced with their bounds (Object if unbounded). At runtime, List<String> and List<Integer> are both just List. This preserves backwards compatibility with pre-generics bytecode. |
| Why can't you do new T()? | Because T is erased at runtime - the JVM doesn't know what type T is. Workaround: pass a Class<T> parameter and call clazz.getDeclaredConstructor().newInstance(), or use a factory lambda. |
| What is bounded wildcarding? When do you use ? extends vs ? super? | PECS - Producer Extends, Consumer Super. Use List<? extends Animal> when you only read from the list (producer). Use List<? super Dog> when you only write to it (consumer). A list of both reads and writes needs a concrete type parameter. |
| Can you check instanceof against a generic type at runtime? | No. obj instanceof List<String> is a compile error because List<String> is erased. You can check obj instanceof List<?> but not the element type. |
Checked vs unchecked exceptions
| Aspect | Checked | Unchecked |
|---|---|---|
| Superclass | Exception (but not RuntimeException) | RuntimeException or Error |
| Compile-time enforcement | Must catch or declare in throws | No requirement |
| Typical use | Recoverable external failures: IOException, SQLException | Programming errors: NullPointerException, IllegalArgumentException, IndexOutOfBoundsException |
| Streams / lambdas | Cannot be thrown from a lambda without wrapping - a common pain point | Propagate freely from lambdas |
| Modern trend | Libraries (Spring, Hibernate) increasingly wrap checked exceptions in unchecked ones to avoid API pollution | Dominant in modern code |
Streams API
The Streams API (Java 8+) is tested in coding rounds as well as language questions. Know both the semantics and the performance traps.
| Question | Sharp answer |
|---|---|
| What is a Stream? | A lazily evaluated pipeline over a sequence of elements. It is not a data structure - it does not store elements. A pipeline has a source, zero or more intermediate operations (map, filter, sorted), and one terminal operation (collect, reduce, forEach). Intermediate operations are lazy; the terminal operation triggers execution. |
| map vs flatMap? | map applies a function 1-to-1, producing Stream<Stream<T>> if the function returns a collection. flatMap applies the function and flattens one level - each element maps to a Stream and those streams are concatenated. |
| When are parallel streams appropriate? | Only when: the data set is large (thousands of elements minimum), the per-element work is CPU-bound and stateless, and the source splits well (ArrayList yes, LinkedList no). The overhead of the ForkJoinPool makes parallel streams slower than sequential for small or I/O-bound workloads. |
| collect vs reduce? | reduce folds elements into a single immutable result using an identity and a BinaryOperator. collect is a mutable reduction that accumulates into a container (List, Map, StringBuilder) using a Collector. Use collect for building collections; reduce for computing a scalar. |
| Can a Stream be reused? | No. A Stream is consumed once. Attempting a second terminal operation throws IllegalStateException. If you need to traverse the source again, create a new Stream. |
For the algorithm side of Java interviews, the patterns in LeetCode Patterns apply equally in Java - knowing when to reach for a PriorityQueue or a HashMap is just as important as knowing their internal details.
Sources & further reading
- 1The Java Language Specification, SE 21 — Oracle / OpenJDK
- 2java.util.concurrent package documentation — Oracle Java SE 21 API
- 3Collections framework overview — Oracle Java Tutorials
- 4JEP 444: Virtual Threads — OpenJDK