Java Interview Cheat Sheet
The facts worth having exact for a Java interview: collection complexity and memory, the equals/hashCode contract, JVM memory areas, GC flags, and the commands that diagnose a production incident.
Collections — complexity
- ArrayList
get O(1) by index, add O(1) amortised, remove O(n). ~4 bytes overhead per element. Grows 1.5x with a full array copy.
- LinkedList
get O(n), add O(1) at the ends, remove O(1) via iterator. ~40 bytes per element. Loses to ArrayList almost everywhere on cache locality.
- HashMap
O(1) average for get/put/remove. Load factor 0.75, doubles on resize, a bucket treeifies at 8 entries if the table has 64+ slots.
- TreeMap
O(log n) — a red-black tree. The only one that answers range queries: floorEntry, ceilingKey, subMap.
- LinkedHashMap
O(1), plus a linked list for order. accessOrder=true and removeEldestEntry gives an LRU cache in ten lines.
- ArrayDeque
O(1) at both ends, no per-node allocation. Beats LinkedList as a queue and Stack as a stack.
- EnumSet / EnumMap
A bit vector and an ordinal-indexed array. One instruction per operation. Always use these for enum keys.
Collections — memory per element
- int[]
4 bytes. The floor.
- ArrayList<Integer>
~20 bytes — a 4-byte reference plus a 16-byte Integer object.
- HashMap entry
~48 bytes before the key and value. A million entries is ~48MB of pure overhead.
- TreeMap entry
~64 bytes — left, right, parent and colour on top of the key and value.
- Object header
12 bytes with compressed oops (8 mark + 4 class), padded to 8. Minimum object size is 16 bytes.
- Compressed oops
References are 4 bytes up to a 32GB heap, 8 bytes above it. A 32GB heap can hold less than a 31GB one.
Core Java contracts
- equals/hashCode
hashCode picks the bucket, equals picks the entry inside it. Equal objects MUST hash equally; equal hashes need not be equal.
- Mutable key
Mutating a field used in hashCode after insertion makes the entry permanently unreachable — still counted in size(), never findable.
- Comparable contract
Antisymmetric, transitive, consistent. Breaking it throws "Comparison method violates its general contract" from TimSort.
- a - b comparator
Overflows. Use Integer.compare(a, b).
- try-with-resources
Closes in reverse order. A close() failure is attached to the body exception as suppressed, not thrown over it.
- finally + return
A return or throw in finally discards whatever the try block was doing, including a pending exception.
- Type erasure
T becomes its leftmost bound and casts are inserted. No new T(), no new T[], no instanceof on a parameterised type.
- PECS
? extends T to read (producer), ? super T to write (consumer), plain T when you do both.
Java 8 and modern
- orElse vs orElseGet
orElse evaluates its argument ALWAYS. orElseGet only when empty. The cause of caches that query the database on every hit.
- thenApply vs thenCompose
map versus flatMap. Use thenCompose when the function itself returns a CompletableFuture.
- Stream laziness
Intermediate operations record; the terminal operation executes. Elements flow one at a time all the way down, which is why short-circuiting works.
- toMap duplicate key
Throws IllegalStateException without a merge function, and NullPointerException on a null value. Always pass the third argument.
- Parallel streams
Use the shared common ForkJoinPool, sized cores-1. Never put blocking I/O in one.
- Lambda internals
invokedynamic plus a private static method. Non-capturing lambdas are singletons; capturing ones allocate per evaluation.
Concurrency
- volatile
Visibility and ordering. NOT atomicity — count++ still loses updates.
- happens-before
Unlock before lock on the same monitor; volatile write before volatile read; start() before the thread; the thread before join().
- Thread pool sizing
CPU-bound: ~cores. I/O-bound: cores x utilisation x (1 + wait/service). Then check the downstream can absorb it.
- newFixedThreadPool
Unbounded LinkedBlockingQueue. Never rejects, never grows, accumulates until OutOfMemoryError. Build a ThreadPoolExecutor instead.
- CallerRunsPolicy
The rejection policy that gives you backpressure for free — the submitting thread runs the task.
- Virtual threads
Do not pool them. Limit concurrency with a Semaphore. Pinning inside synchronized is the Java 21 gotcha.
- Deadlock defence
Consistent lock ordering by a stable key, or tryLock with a timeout and jittered backoff.
JVM memory and GC
- Per thread
Stack (~1MB, -Xss), program counter, native method stack.
- Shared
Heap (young + old), Metaspace (native, since Java 8), code cache.
- Process memory
heap + metaspace + code cache + (threads x stack) + GC structures + direct buffers. Typically 25-50% above -Xmx.
- Container sizing
-XX:MaxRAMPercentage=70 rather than -Xmx, plus MaxMetaspaceSize and MaxDirectMemorySize.
- G1
Default. Regions, concurrent marking, mixed collections. MaxGCPauseMillis is a target — 100-200ms is realistic, 10ms backfires.
- ZGC
Sub-millisecond pauses at any heap size, ~10-15% throughput cost. Use -XX:+ZGenerational.
- Leak signature
Heap occupancy AFTER each Full GC keeps rising. A sawtooth returning to the same floor is healthy.
- OOMKilled (137)
The kernel, not the JVM. No stack trace, no heap dump. Non-heap memory was unaccounted for in the container limit.
Production flags to always set
- GC logging
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20M
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20M - Heap dump on OOM
Costs nothing until it fires, and without it you are guessing.
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/ - Continuous JFR
A rolling buffer means you have the recording from before the incident.
-XX:StartFlightRecording=name=cont,settings=profile,maxsize=512m,maxage=1h,disk=true - Container memory
Adapts when the pod spec changes, unlike a hard-coded -Xmx.
-XX:MaxRAMPercentage=70.0 -XX:MaxMetaspaceSize=256m - Native memory tracking
The only reliable way to find where non-heap memory went.
-XX:NativeMemoryTracking=summary
Incident commands
- Hot thread
Find the thread burning CPU, then convert its id to hex.
top -H -p $PID then printf '%x\n' <tid> - Thread dump
Match nid=0x<hex> from the previous step. Take three, five seconds apart.
jcmd $PID Thread.print | grep -A 30 nid=0x3072 - GC at a glance
Watch the O column across full collections — rising means a leak.
jstat -gcutil $PID 1000 10 - Heap dump
Pauses the JVM seconds per GB. Drain the node first.
jcmd $PID GC.heap_dump /tmp/heap.hprof - Native memory
Breaks the process down by category.
jcmd $PID VM.native_memory summary - Dump the JFR buffer
Everything from the last hour, if continuous recording is on.
jcmd $PID JFR.dump name=cont filename=/tmp/incident.jfr - Thread names by count
Instantly shows which pool is creating threads without bound.
jcmd $PID Thread.print | grep '^"' | sed 's/".*//' | sort | uniq -c | sort -rn
Spring gotchas
- Self-invocation
Calling a @Transactional method through this bypasses the proxy — no transaction, no retry, no caching. Same for @Async and @Cacheable.
- Rollback rules
@Transactional rolls back on unchecked exceptions only. A checked exception COMMITS unless you set rollbackFor.
- Network call in a transaction
Holds a database connection and locks for the duration. The most common cause of pool exhaustion.
- Prototype in a singleton
Injected once, reused forever. Use ObjectProvider or a scoped proxy.
- Debug auto-configuration
Prints the conditions evaluation report — why a bean was or was not created.
java -jar app.jar --debug