Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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