Skip to content

Core Java โ€” Senior Engineer Study Guide โ€‹

๐Ÿ“ Quiz ยท ๐Ÿƒ Flashcards

Companion to INTERVIEW_PREP.md ยง1. This guide is the teaching layer: concepts explained from first principles, code examples, gotchas, and anchor examples (a legacy MQ microservice, cross-team schema standardization, JAXB migration, ArgoCD rollouts).

Scope: Core Java language + JVM internals + modern features up through Java 25 LTS. Concurrency is intentionally a refresher here (JMM / volatile / synchronized / ThreadLocal basics) โ€” full depth lives in a future CONCURRENCY.md.

How to use: Skim the coverage matrix (ยงQ-Map) to jump to the section answering a specific INTERVIEW_PREP question. For open study, walk ยง1 โ†’ ยง22 in order. Morning-of-interview: ยง21 Rapid-Fire.


Table of Contents โ€‹

  1. Java Platform & Language Basics
  2. Object-Oriented Java
  3. Strings & Text
  4. Immutability
  5. Exception Handling
  6. Generics
  7. Collections Framework
  8. Functional Java โ€” Lambdas, Streams, Optional
  9. Modern Java Features (Java 11 โ†’ 25)
  10. JVM Internals & Memory
  11. Garbage Collection
  12. Concurrency Refresher
  13. I/O & NIO
  14. Reflection & Annotations
  15. Serialization
  16. Java Modules (JPMS)
  17. Null Safety
  18. Common Gotchas & Tricky Outputs
  19. Build & Ecosystem
  20. Connect to Your Experience
  21. Rapid-Fire Review
  22. Practice Exercises

Interview-Question Coverage Matrix โ€‹

Maps each INTERVIEW_PREP.md ยง1 question (1โ€“30) to the section(s) in this guide that answer it.

Q#TopicSection
1== vs .equals()ยง1
2hashCode/equals contractยง1
3ArrayList/LinkedList/Vectorยง7
4HashMap/LinkedHashMap/TreeMap/ConcurrentHashMapยง7
5HashMap internals (Java 8+)ยง7
6Checked vs unchecked exceptionsยง5
7try-with-resources / AutoCloseableยง5
8Immutability recipeยง4
9String vs StringBuilder vs StringBufferยง3
10String pool / new String("abc") vs "abc"ยง3
11Type erasureยง6
12Covariance / contravariance / PECSยง6
13Optional โ€” when NOT to use itยง8
14Streams โ€” intermediate vs terminal, lazy evalยง8
15map vs flatMapยง8
16parallelStream() pitfallsยง8
17Functional interfacesยง8
18Recordsยง4, ยง9
19Sealed classesยง9
20Pattern matching (instanceof / switch)ยง9
21Text blocksยง3, ยง9
22Virtual threads & pinningยง9, ยง12
23JVM memory model โ€” heap/stack/metaspace/gensยง10
24StackOverflowError vs OutOfMemoryErrorยง10
25G1 vs ZGC vs Shenandoahยง11
26Memory leak analysis in a running JVMยง10, ยง11
27final vs finally vs finalizeยง1, ยง5
28Serializable โ€” why dangerousยง15
29Java Modules (JPMS)ยง16
30Null safety without Optional-everywhereยง17

1. Java Platform & Language Basics โ€‹

JDK vs JRE vs JVM โ€‹

  • JVM โ€” the spec + runtime that executes .class bytecode. Multiple implementations (HotSpot, OpenJ9, GraalVM). Platform-specific.
  • JRE โ€” JVM + core class libraries. Run-only. (Bundled into the JDK since Java 11; no separate download.)
  • JDK โ€” JRE + developer tools (javac, jar, javadoc, jlink, jpackage, jcmd).

Write-once-run-anywhere = javac produces portable bytecode; a platform-specific JVM executes it. Cross-platform native code (via JNI / Panama) breaks this guarantee.

Primitives vs wrappers โ€‹

Java has 8 primitives: byte, short, int, long, float, double, char, boolean. Each has a wrapper (Integer, Long, etc.) for use in generics / collections / nullable fields.

Autoboxing (primitive โ†’ wrapper) and unboxing (wrapper โ†’ primitive) happen implicitly:

java
Integer boxed = 42;       // autoboxing: Integer.valueOf(42)
int unboxed = boxed;      // unboxing: boxed.intValue()

Integer cache gotcha (โ€“128 to 127): Integer.valueOf(int) caches boxed values in that range. So:

java
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b); // true  (same cached instance)
System.out.println(c == d); // false (two different Integer objects)

Always use .equals() on boxed types. The cache is an optimization, not a contract you should rely on.

Null-unbox NPE:

java
Integer x = null;
int y = x; // NullPointerException โ€” unboxing null

This is insidious in ternaries: boolean flag ? 1 : null compiles but unboxes the null.

Pass-by-value โ€” always โ€‹

Java is always pass-by-value. For object arguments, the "value" is the reference. So a method can mutate the object the reference points to, but cannot reassign the caller's reference.

java
void reassign(List<String> l) { l = new ArrayList<>(); } // caller's list unchanged
void mutate(List<String> l)   { l.add("x"); }            // caller's list gets "x"

== vs .equals() โ€‹

  • == โ€” reference identity for objects; value equality for primitives.
  • .equals() โ€” logical equality. Default Object.equals is ==. Override it for value-like classes.
java
String a = new String("hi"), b = new String("hi");
a == b;         // false โ€” two distinct heap objects
a.equals(b);    // true  โ€” logical equality

equals / hashCode contract โ€‹

Five rules for equals:

  1. Reflexive โ€” x.equals(x) is true.
  2. Symmetric โ€” x.equals(y) โ‡” y.equals(x).
  3. Transitive โ€” if x.equals(y) and y.equals(z), then x.equals(z).
  4. Consistent โ€” repeated calls return the same result so long as objects don't change.
  5. Null-safe โ€” x.equals(null) is false.

The hashCode contract: if a.equals(b), then a.hashCode() == b.hashCode(). The reverse is not required.

What breaks if you violate it?

  • Override equals without hashCode โ†’ objects that are "equal" land in different buckets of a HashMap / HashSet. map.put(key, v) then map.get(equalKey) returns null. Duplicates appear in HashSet.
  • Mutate a field used in hashCode after the object is a map key โ†’ the object is effectively "lost" โ€” it's in a bucket it shouldn't be in anymore, and lookup goes to the new bucket, where it isn't.

Canonical implementation:

java
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User u)) return false;  // pattern matching (Java 16+)
    return Objects.equals(id, u.id);
}

@Override
public int hashCode() { return Objects.hash(id); }

Better yet: use record (ยง4), which generates both correctly.

final, finally, finalize โ€‹

Three unrelated concepts despite the name overlap:

KeywordWhat it does
finalPrevents reassignment (final var), overriding (final method), or subclassing (final class).
finallyBlock after try that always executes (except JVM death / killed thread).
finalize()Legacy Object method called by GC before reclamation. Deprecated in Java 9, removal underway. Use AutoCloseable + try-with-resources or Cleaner.

static โ€‹

  • Belongs to the class, not an instance.
  • Static fields are initialized once when the class is loaded (see ยง10 Class Loading).
  • Static init blocks run top-to-bottom in source order:
java
class Config {
    static int a = 1;
    static { System.out.println(a); a = 2; }
    static int b = a; // b = 2
}
  • Static methods can't be overridden (they're hidden). Don't call static methods through an instance reference โ€” confuses readers.

Interview Qs covered โ€‹

ยง1 addresses INTERVIEW_PREP Qs: 1 (==/equals), 2 (hashCode contract), 27 (final/finally/finalize).


2. Object-Oriented Java โ€‹

The four pillars โ€” Java-specific โ€‹

PillarJava mechanism
EncapsulationPrivate fields + public methods. Lombok shortens this.
InheritanceSingle-class inheritance (extends) + multi-interface implementation (implements). No multiple class inheritance; diamond problem is avoided at the type level but returns with default interface methods (see below).
PolymorphismRuntime dispatch on overridden methods. Compile-time via overloading.
AbstractionAbstract classes + interfaces.

Abstract class vs interface โ€‹

Abstract classInterface
Instance stateFields + constructors allowedOnly static final constants
Multiple inheritanceOne per classMulti-implement
Method bodiesYes (any access)default + static + private (Java 8 / 9)
Use casePartial implementation with statePure contract / capability

Rule of thumb: reach for interfaces unless you need shared instance state.

Default / static / private interface methods โ€‹

  • Java 8: default (instance default body) + static (namespace-scoped helper).
  • Java 9: private (share logic among default methods without leaking it to implementers).

Diamond with defaults: if a class inherits conflicting default methods from two interfaces, the class must override and explicitly pick:

java
interface A { default String greet() { return "A"; } }
interface B { default String greet() { return "B"; } }
class C implements A, B {
    @Override public String greet() { return A.super.greet(); }
}

Access modifiers โ€‹

ModifierSame classSame pkgSubclass (diff pkg)Anywhere
privateโœ“
(package-private)โœ“โœ“
protectedโœ“โœ“โœ“
publicโœ“โœ“โœ“โœ“

Package-private (no keyword) is underused โ€” great for internal helpers you want unit-testable but not part of the public API.

Overloading vs overriding โ€‹

  • Overloading โ€” same name, different parameter list, resolved at compile time. add(int) vs add(String).
  • Overriding โ€” subclass re-implements an inherited instance method with the same signature. Resolved at runtime (virtual dispatch). Always use @Override; the compiler catches signature drift.

Covariant return types (Java 5+): an override can return a subtype of the superclass's return type.

java
class Animal { Animal clone() { return new Animal(); } }
class Dog extends Animal { @Override Dog clone() { return new Dog(); } }

Nested classes โ€‹

KindHolds outer this?Use
Static nestedNoHelper class logically grouped with outer (e.g., Map.Entry)
Inner (non-static)Yes โ€” implicit Outer.thisRarely needed; enables tight coupling to outer state
LocalYes (effectively final captures)One-off inside a method
AnonymousYes (same)Pre-Java 8, for SAM impls. Now largely replaced by lambdas.

Memory leak warning: non-static inner and anonymous classes hold a reference to the enclosing instance. If the inner outlives the outer (event listener, cached callback), you pin the outer. Prefer static nested when possible.

Composition > inheritance โ€‹

Inheritance creates a strong compile-time coupling and is easy to misuse. Prefer composition (+ delegation) for reuse; prefer interfaces for polymorphism. Mark classes final unless you've designed them for extension โ€” see Effective Java Item 19 ("Design and document for inheritance or else prohibit it").


3. Strings & Text โ€‹

Immutability and why it matters โ€‹

String is immutable (backed by a final byte[] since Java 9 โ€” compact strings). Reasons:

  • Security โ€” strings are used as file paths, URLs, SQL. If mutable, a checker-then-user race could change the validated string.
  • Thread-safety โ€” no synchronization needed to share a String across threads.
  • Hashing โ€” String.hashCode() is cached (Java can do this because the bytes never change), making maps keyed by strings fast.
  • Interning โ€” allows the string pool.

The String pool โ€‹

Located in the heap (since Java 7; it was in PermGen before). String literals are interned: the same literal always resolves to the same object.

java
String a = "hi";
String b = "hi";
a == b; // true โ€” same interned instance

String c = new String("hi");
a == c; // false โ€” c is a new heap allocation
c.intern() == a; // true โ€” intern returns the pool entry

Gotcha: new String("hi") creates two objects โ€” one in the pool (from the literal) and one on the heap (from new). Don't use new String unless you specifically need a separate instance.

String vs StringBuilder vs StringBuffer โ€‹

ClassMutableThread-safePerf
StringNoInherentlyConcat = new object each time
StringBuilderYesNo~4x faster than StringBuffer
StringBufferYesYes (synchronized)Legacy; almost never needed

Rule: use StringBuilder for loops that concat. The compiler may convert + in loops to StringBuilder, but it sometimes can't see through method boundaries โ€” don't rely on it.

Since Java 9, simple concat (a + b) uses invokedynamic (StringConcatFactory) and is usually faster than hand-rolled StringBuilder for a small number of parts.

Text blocks (Java 15) โ€‹

java
String sql = """
    SELECT id, name
    FROM users
    WHERE active = true
    """;

String json = """
    {"habit":"run","done":true}
    """;
  • Indentation is normalized: the compiler strips the common leading whitespace.
  • Use \ at the end of a line to suppress the newline, \s for trailing space.
  • Great for embedded JSON/SQL/XML. Hugely improves readability in tests.

Formatting โ€‹

  • String.format("Hello %s, age %d", name, age)
  • "Hello %s".formatted(name) (Java 15+) โ€” fluent.
  • printf for stdout.
  • Always specify a Locale for user-facing number/date formatting โ€” the default Locale bites on machines where , vs . flips.

Unicode & equality caveats โ€‹

  • "a".equalsIgnoreCase("A") โ€” true.
  • But some Unicode characters have no uppercase equivalent or have multi-char folding. For user-facing comparison, normalize with Normalizer.normalize(s, Form.NFC) first.

Interview Qs covered โ€‹

ยง3 addresses Qs: 9 (String/Builder/Buffer), 10 (string pool), 21 (text blocks).


4. Immutability โ€‹

The recipe โ€‹

To make a class truly immutable:

  1. Declare the class final (or use a sealed hierarchy โ€” ยง9).
  2. All fields private final.
  3. No setters, no mutators.
  4. Defensive-copy mutable constructor arguments on the way in and on the way out.
  5. Don't leak this during construction (no this::method registration before the constructor finishes).
java
public final class Event {
    private final String id;
    private final Instant timestamp;
    private final List<String> tags;

    public Event(String id, Instant timestamp, List<String> tags) {
        this.id = Objects.requireNonNull(id);
        this.timestamp = Objects.requireNonNull(timestamp);
        this.tags = List.copyOf(tags); // defensive copy + unmodifiable
    }

    public List<String> getTags() {
        return tags; // already unmodifiable; no need to recopy
    }
}

Why immutable types are inherently thread-safe: no state change means no race. They also safely cache hashCode.

Records (Java 14+) โ€‹

A record is a concise syntax for immutable data carriers. The compiler generates:

  • A canonical constructor with all components.
  • Final private fields.
  • Accessors named after the components (no get prefix).
  • equals, hashCode, toString based on all components.
java
public record Point(int x, int y) {}

Point p = new Point(1, 2);
p.x();             // 1
p.equals(new Point(1, 2)); // true

Compact constructor for validation / normalization โ€” runs before field assignment:

java
public record Rate(int value) {
    public Rate {                              // compact: no parameter list
        if (value < 0) throw new IllegalArgumentException();
    }
}

Canonical constructor (full form) lets you customize field assignment:

java
public record Name(String first, String last) {
    public Name(String first, String last) {
        this.first = first == null ? "" : first.strip();
        this.last  = last  == null ? "" : last.strip();
    }
}

When NOT to use records:

  • You need mutable state.
  • You need to extend another class (records implicitly extend java.lang.Record).
  • A framework demands a no-arg constructor + setters (old Hibernate, older Jackson without Parameter Names module).
  • You care about hiding fields โ€” records expose them by accessor.

Records can implement interfaces and hold static methods/fields. They cannot declare instance fields beyond the components.

Immutable collections โ€‹

  • List.of(...), Set.of(...), Map.of(...) / Map.ofEntries(...) โ€” Java 9+. Truly immutable; throw on mutation attempts; reject nulls.
  • List.copyOf(other) โ€” Java 10+. Returns other if already immutable; otherwise copies.
  • Collections.unmodifiableList(x) โ€” a view, not a copy. If you keep a reference to x and mutate it, the "unmodifiable" view reflects the mutation. Trap.

Interview Qs covered โ€‹

ยง4 addresses Qs: 8 (immutability), 18 (records).


5. Exception Handling โ€‹

Hierarchy โ€‹

Throwable
โ”œโ”€โ”€ Error                    โ† serious JVM issues; don't catch
โ”‚   โ””โ”€โ”€ OutOfMemoryError, StackOverflowError, โ€ฆ
โ””โ”€โ”€ Exception                โ† CHECKED (except RuntimeException)
    โ”œโ”€โ”€ IOException, SQLException, โ€ฆ   โ† checked
    โ””โ”€โ”€ RuntimeException                โ† UNCHECKED
        โ””โ”€โ”€ NullPointerException, IllegalArgumentException, โ€ฆ

Checked vs unchecked โ€‹

  • Checked (extends Exception, not RuntimeException) โ€” compiler forces you to catch or declare (throws).
  • Unchecked (extends RuntimeException or Error) โ€” no compile-time mandate.

Philosophy:

  • Checked was intended for recoverable conditions; unchecked for programmer errors. In practice this split didn't age well.
  • Modern frameworks lean unchecked. Spring translates JDBC's SQLException (checked) to its own unchecked DataAccessException hierarchy so your repository methods aren't noise.
  • Checked exceptions don't compose with lambdas (most java.util.function types don't declare throws), which makes them awkward in streams.

try / catch / finally โ€‹

java
try {
    riskyOp();
} catch (IOException | SQLException e) {      // multi-catch (Java 7+)
    log.error("op failed", e);
    throw new AppException("op failed", e);   // exception chaining
} finally {
    cleanup();
}
  • Multi-catch โ€” combine handlers when the logic is identical. The caught variable is effectively final inside the block.
  • Chaining โ€” pass cause to new Exception(msg, cause). Preserves root cause in stack traces.

try-with-resources & AutoCloseable โ€‹

java
try (var in = Files.newBufferedReader(path);
     var out = Files.newBufferedWriter(other)) {
    in.transferTo(out);
}
  • Resources are closed in reverse order of declaration, even if an exception is thrown.
  • The resource must implement AutoCloseable (or Closeable, which extends it and restricts to IOException).
  • If both the try body and close() throw, the close() exception becomes a suppressed exception on the primary. Retrievable via e.getSuppressed().

Rethrow patterns โ€‹

  • Wrap and rethrow with cause โ€” standard.
  • Catch-log-rethrow (both log and throw) โ€” makes logs noisy (one error, two stack traces). Log OR throw, not both. Let the top-level handler log.
  • Swallow-and-continue โ€” occasionally right (best-effort cleanup), usually wrong. Add a comment.

finally subtleties โ€‹

  • Runs even when you return from try. finally can override the return:
java
int f() {
    try { return 1; }
    finally { return 2; } // returns 2; don't do this
}
  • Does not run if JVM exits (System.exit), the thread is killed, or the process is SIGKILLed.

Interview Qs covered โ€‹

ยง5 addresses Qs: 6 (checked vs unchecked), 7 (try-with-resources / AutoCloseable), 27 (finally).


6. Generics โ€‹

Why generics โ€‹

Compile-time type safety + removal of casts. Before Java 5:

java
List names = new ArrayList();
names.add("Alice");
String n = (String) names.get(0); // cast + hope

With generics, the cast is inserted by the compiler and type errors are caught at compile time.

Type erasure โ€‹

At compile time, the compiler checks types. At runtime, the <T> is erased to its bound (Object by default, or the upper bound if declared). Consequences:

  • You cannot do new T(). At runtime, T doesn't exist.
    • Workaround: pass Class<T> and call clazz.getDeclaredConstructor().newInstance().
  • You cannot new T[10]. Generic array creation is banned.
    • Workaround: @SuppressWarnings("unchecked") T[] a = (T[]) new Object[10]; โ€” but arrays are covariant and generics are invariant, so this is fragile.
  • instanceof T fails (except with wildcards: x instanceof List<?>).
  • Bridge methods are synthesized to preserve polymorphism in the erased signatures โ€” usually invisible, sometimes show in stack traces.
  • List<String> and List<Integer> are the same Class at runtime (List.class).

Bounded types โ€‹

java
<T extends Number> double sumOf(List<T> xs) { ... }      // upper bound
<T extends Number & Comparable<T>> T max(List<T> xs) { } // multiple bounds (class first)

Wildcards โ€” ?, ? extends T, ? super T โ€‹

  • List<?> โ€” a list of unknown type. You can read Object; you cannot add (except null).
  • List<? extends Number> โ€” a list of Number or any subtype. You can read as Number; you cannot add (since the exact subtype is unknown).
  • List<? super Integer> โ€” a list of Integer or any supertype. You can add Integer (or subtype); reading yields Object.

PECS โ€” Producer Extends, Consumer Super โ€‹

If the collection produces values for you to read โ†’ ? extends T. If the collection consumes values you write โ†’ ? super T.

Copy example from Collections:

java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}
  • src produces Ts (read) โ†’ extends.
  • dest consumes Ts (write) โ†’ super.

Reifiable vs non-reifiable โ€‹

  • Reifiable โ€” runtime carries full type info (primitives, raw types, unbounded wildcards, arrays of reifiable types).
  • Non-reifiable โ€” parameterized types like List<String>. Hence no instanceof List<String>, no generic arrays, heap pollution warnings.

Interview Qs covered โ€‹

ยง6 addresses Qs: 11 (type erasure, no new T()), 12 (covariance / contravariance / PECS).


7. Collections Framework โ€‹

Hierarchy โ€‹

Iterable
โ””โ”€โ”€ Collection
    โ”œโ”€โ”€ List       (ordered, indexed, allows duplicates)
    โ”œโ”€โ”€ Set        (no duplicates)
    โ””โ”€โ”€ Queue/Deque (FIFO / LIFO)
Map                (not a Collection โ€” keyed access)

List โ€‹

ImplBackingRandom accessInsert at endInsert in middleThread-safe
ArrayListResizable arrayO(1)amortized O(1)O(n)No
LinkedListDoubly-linked nodesO(n)O(1)O(1) if you have node ref, O(n) to findNo
VectorResizable arrayO(1)O(1)O(n)Yes (every method synchronized) โ€” legacy
CopyOnWriteArrayListArray, copy on writeO(1)O(n) (full copy)O(n)Yes โ€” reads lock-free

Practical advice:

  • Default to ArrayList.
  • LinkedList sounds attractive for "inserts in the middle" but almost never wins in practice โ€” cache misses on each pointer chase dominate. Benchmark before reaching for it.
  • Vector โ€” don't, ever. If you need thread-safety wrap with Collections.synchronizedList or use CopyOnWriteArrayList (read-heavy).
  • CopyOnWriteArrayList โ€” perfect for listener lists: infrequent writes, frequent iteration without sync.

Set โ€‹

  • HashSet โ€” hash-based, O(1) ops, no order.
  • LinkedHashSet โ€” insertion-order.
  • TreeSet โ€” sorted (by Comparable or Comparator), O(log n); NavigableSet methods (floor, ceiling, etc.).
  • EnumSet โ€” bitwise-packed, extremely efficient for enum keys.

Map โ€‹

  • HashMap โ€” hash bucket array; no order; O(1) average.
  • LinkedHashMap โ€” insertion or access order; constructor flag accessOrder=true enables LRU behavior (see ยง22).
  • TreeMap โ€” red-black tree; sorted keys; NavigableMap.
  • WeakHashMap โ€” keys are weak references; entries auto-removed when key is GCed. Great for caches tied to object lifecycle.
  • IdentityHashMap โ€” uses == not equals; for graph-traversal algorithms.
  • EnumMap โ€” array-backed; very fast for enum keys.
  • Hashtable โ€” legacy synchronized; don't use.
  • ConcurrentHashMap โ€” modern concurrent; see below.

HashMap internals (Java 8+) โ€‹

  • Backed by Node<K,V>[] table โ€” power-of-two size, default capacity 16, load factor 0.75.
  • Hash spread: (h = key.hashCode()) ^ (h >>> 16) โ€” mixes high bits into low bits so poor hashers (e.g., integer IDs) distribute better.
  • Bucket index: hash & (table.length - 1).
  • On collision: chain as a singly-linked list. Once a bucket reaches 8 nodes (TREEIFY_THRESHOLD), it's converted to a red-black tree โ€” provided table.length >= 64; otherwise the table is resized first. Tree lookup degrades to O(log n) worst-case instead of O(n) for adversarial / poor-hashing keys.
  • Untreeify threshold 6 โ€” during removal, tree shrinks back to a list at 6 nodes.
  • Resize โ€” doubles capacity when size > loadFactor * capacity. Each entry is rehashed. Expensive; size your map if you know the target.
  • null key allowed (goes in bucket 0); null values allowed.

ConcurrentHashMap internals (Java 8+) โ€‹

  • Same bucket layout as HashMap.
  • Pre-Java-8 used "segments" (16 by default). Java 8 replaced that with:
    • Lock-free reads via volatile semantics on the node array.
    • CAS on empty-bucket insertions.
    • synchronized on the head node of a non-empty bucket for inserts/updates. Fine-grained: different buckets contend only when hashing to the same slot.
  • No null keys, no null values โ€” avoids ambiguity with "key not present" in concurrent reads.
  • Atomic compound ops: computeIfAbsent, merge, putIfAbsent.
  • size() is an approximation under contention; use mappingCount() for a long.

Hashtable vs synchronizedMap vs ConcurrentHashMap โ€‹

  • Hashtable โ€” entire map synchronized; single lock; legacy.
  • Collections.synchronizedMap(hashMap) โ€” also single lock, wrapped; iteration still needs manual synchronized(map) { for (...) }.
  • ConcurrentHashMap โ€” per-bucket fine-grained locking, weakly-consistent iterators (no ConcurrentModificationException). Use this.

Queue & Deque โ€‹

  • ArrayDeque โ€” default stack + deque. Beats Stack (legacy, synchronized).
  • PriorityQueue โ€” min-heap by default; provide a Comparator for max-heap or custom order.
  • BlockingQueue family: ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue, DelayQueue, LinkedTransferQueue. Producer-consumer foundation (see ยง12).

Comparable vs Comparator โ€‹

  • Comparable<T> โ€” "natural order" (String, Integer). One per type.
  • Comparator<T> โ€” external ordering strategy. Multiple orders per type possible.
java
users.sort(Comparator.comparing(User::lastName)
                      .thenComparing(User::firstName)
                      .reversed());

Fail-fast vs fail-safe iterators โ€‹

  • Fail-fast (ArrayList, HashMap): detect structural modification during iteration and throw ConcurrentModificationException. Best-effort, not guaranteed.
  • Fail-safe (CopyOnWriteArrayList, ConcurrentHashMap): iterate over a snapshot / weakly-consistent view; never throw CME. Iterators may not see concurrent updates.

Interview Qs covered โ€‹

ยง7 addresses Qs: 3 (ArrayList/LinkedList/Vector), 4 (Map impls), 5 (HashMap internals).


8. Functional Java โ€” Lambdas, Streams, Optional โ€‹

Functional interfaces โ€‹

A functional interface has exactly one abstract method (SAM). @FunctionalInterface documents intent and lets the compiler enforce it.

Core types in java.util.function:

InterfaceMethodShape
Function<T,R>R apply(T)T โ†’ R
BiFunction<T,U,R>R apply(T,U)T,U โ†’ R
Predicate<T>boolean test(T)T โ†’ bool
Consumer<T>void accept(T)T โ†’ ()
Supplier<T>T get()() โ†’ T
UnaryOperator<T>T apply(T)T โ†’ T
BinaryOperator<T>T apply(T,T)T,T โ†’ T

Primitive specializations avoid boxing: IntFunction, ToIntFunction, IntPredicate, IntStream, etc.

Lambdas and captures โ€‹

Lambdas capture variables that are effectively final โ€” the variable isn't reassigned after capture. (Not required to be declared final, just not reassigned.)

java
int base = 10;
Function<Integer, Integer> add = x -> x + base; // base is effectively final
// base = 20; // would break the above โ€” compile error

Captured instance fields are not subject to this (they're accessed through this), but mutating them from a lambda on another thread is still a data race.

Method references โ€‹

FormExample
Static methodInteger::parseInt
Instance method of a specific objectsb::append
Instance method of an arbitrary object of a given typeString::length
ConstructorArrayList::new

Streams โ€‹

A stream is a pipeline of operations over a data source. Three stages:

  1. Source โ€” collection, array, I/O channel, Stream.of(...), Stream.generate(...).
  2. Intermediate ops โ€” lazy, return another stream (filter, map, flatMap, sorted, distinct, peek, limit, skip).
  3. Terminal op โ€” triggers execution (collect, forEach, reduce, count, findFirst, toList, toArray, min, max, anyMatch, allMatch, noneMatch).

Lazy evaluation: intermediate ops don't run until a terminal op is invoked. The terminal op drives the pipeline element-by-element (short-circuiting where possible).

java
list.stream()
    .filter(this::isActive)   // lazy
    .map(User::email)         // lazy
    .findFirst();              // terminal โ€” pull starts here, short-circuits

map vs flatMap โ€‹

  • map(fn) โ€” Stream<T> โ†’ Stream<R>. One-to-one.
  • flatMap(fn) โ€” Stream<T> โ†’ Stream<R> where fn: T โ†’ Stream<R>. One-to-many, flattened.
java
List<List<String>> tagLists = ...;
List<String> allTags = tagLists.stream()
                                .flatMap(List::stream)
                                .toList();

Collectors โ€” the common ones โ€‹

  • toList() / toUnmodifiableList() / toSet()
  • toMap(keyFn, valueFn, mergeFn) โ€” always supply a merge function if duplicate keys are possible; otherwise you get IllegalStateException.
  • groupingBy(fn) โ€” returns Map<K, List<V>>.
  • groupingBy(fn, counting()) / groupingBy(fn, mapping(extractor, toList())) โ€” downstream collectors.
  • partitioningBy(pred) โ€” Map<Boolean, List<V>>.
  • joining(", ", "[", "]") โ€” strings.
  • summarizingInt(fn) โ€” IntSummaryStatistics (count, sum, min, max, avg in one pass).

parallelStream() โ€” when it helps and when it hurts โ€‹

Runs on the ForkJoin common pool. Pitfalls:

  • Shared mutable state across pipeline โ†’ data race.
  • Tasks that do blocking I/O monopolize the common pool โ€” affects every parallel stream in the JVM. Use a custom pool or don't go parallel.
  • Ordering โ€” preserved by default for ordered sources but at a sync cost. Use .unordered() to drop the guarantee.
  • Small N โ€” overhead swamps gains. Rule of thumb: worth considering above ~10k elements of non-trivial work.
  • With virtual threads, blocking pipelines still misbehave โ€” virtual threads help with blocking in the task-per-thread model, not in the ForkJoin common pool.

Default: use sequential streams. Reach for parallel only with data you own, CPU-bound work, and benchmarks.

Optional โ€‹

  • Return type only. Don't use as:
    • Instance field โ€” use null (with @Nullable) or default value.
    • Method parameter โ€” forces callers to wrap.
    • Collection element โ€” Optional<T> in a List<Optional<T>> is almost always wrong.
  • Use ifPresent, orElse, orElseThrow, map, flatMap.
  • Don't optional.get() without isPresent() โ€” defeats the point.
  • Don't wrap when it could be null: use Optional.ofNullable(x) not Optional.of(x).
java
// Good
repo.findById(id).map(User::email).orElseThrow(() -> new NotFound(id));

// Bad
Optional<Optional<String>> nope = ...;

Interview Qs covered โ€‹

ยง8 addresses Qs: 13 (Optional), 14 (streams lazy eval), 15 (map vs flatMap), 16 (parallelStream), 17 (functional interfaces).


9. Modern Java Features (Java 11 โ†’ 25) โ€‹

Release timeline (LTS in bold) โ€‹

VersionReleasedHighlights
82014Lambdas, streams, Optional, default methods
11 (LTS)2018var (10), HTTP client, String enhancements
142020Records (preview), helpful NPEs, switch expressions standard
152020Text blocks standard, sealed classes (preview)
162021Records standard, pattern matching for instanceof standard
17 (LTS)2021Sealed classes standard
21 (LTS)2023Virtual threads, pattern matching for switch, record patterns, sequenced collections
232024Unnamed classes / instance main (preview)
25 (LTS)2025-09Flexible constructor bodies, compact object headers, scoped values, module imports, primitive patterns (preview)

var (local variable type inference, Java 10) โ€‹

java
var users = new ArrayList<User>();   // inferred ArrayList<User>
var stream = list.stream();

Rules:

  • Only for local variables with an initializer (or for-loop variables).
  • Not for fields, method parameters, return types.
  • Not with null initializer, not with a lambda (no target type to infer).
  • Prefer when the RHS makes the type obvious. Don't use it to hide complex types.

Records (Java 14, standard 16) โ€” see ยง4. โ€‹

Sealed classes/interfaces (Java 17) โ€‹

Restrict who can extend/implement:

java
public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double a, double b, double c) implements Shape {}

Subclasses must be final, sealed, or non-sealed. Enables exhaustive switches:

java
double area(Shape s) {
    return switch (s) {
        case Circle c     -> Math.PI * c.r() * c.r();
        case Square sq    -> sq.side() * sq.side();
        case Triangle t   -> heron(t);
        // no default โ€” compiler verifies exhaustiveness
    };
}

Pattern matching for instanceof (Java 16) โ€‹

java
if (obj instanceof User u) {     // binding variable 'u'
    send(u.email());
}

Cleaner than cast-after-check. The binding is flow-scoped โ€” in the negative branch (if (!(obj instanceof User u))), u would be available after an early return.

Pattern matching for switch (Java 21) โ€‹

java
String describe(Object o) {
    return switch (o) {
        case Integer i when i < 0 -> "negative int";
        case Integer i            -> "non-negative int";
        case String s             -> "string: " + s;
        case null                 -> "null";
        default                   -> "other";
    };
}

Switch expressions also give you:

  • Arrow syntax โ€” no fallthrough.
  • yield โ€” return a value from a block branch.
  • Exhaustiveness โ€” on sealed types / enums, no default needed.

Record patterns (Java 21) โ€‹

Destructure records in patterns:

java
String formatShape(Shape s) {
    return switch (s) {
        case Circle(double r)            -> "circle r=" + r;
        case Square(double side)         -> "square " + side;
        case Triangle(double a, double b, double c) -> "tri " + a + "," + b + "," + c;
    };
}

Nested record patterns let you pull out sub-fields in one expression.

Text blocks โ€” see ยง3. โ€‹

Virtual threads (Java 21) โ€” quick intro (full depth in CONCURRENCY.md) โ€‹

  • What they are: lightweight threads scheduled by the JVM on top of a small pool of carrier platform threads. You can create millions. Thread.ofVirtual().start(r) or Executors.newVirtualThreadPerTaskExecutor().
  • When they help: blocking I/O at scale โ€” thread-per-request servers without the cost. A virtual thread blocked on read() unmounts from its carrier, freeing it for another virtual thread.
  • Pinning: if a virtual thread enters a synchronized block or a native frame (JNI), it's pinned to its carrier and cannot unmount. Before Java 24 this pinned the carrier for the duration; Java 24+ largely lifts this for synchronized. Still: prefer ReentrantLock for long-held locks.
  • When they don't help: CPU-bound work โ€” you still need roughly one thread per core. ForkJoinPool or a sized executor is better.

Structured concurrency (Java 25 incubator / preview depending on final status) โ€‹

Treats a group of tasks as a single unit so that cancellation propagates and errors fail fast:

java
try (var scope = StructuredTaskScope.open()) {
    var userTask  = scope.fork(() -> userService.load(id));
    var orderTask = scope.fork(() -> orderService.load(id));
    scope.join();           // wait for all
    return new View(userTask.get(), orderTask.get());
}   // auto-closed: any outstanding task is cancelled

Scoped values (Java 25) โ€‹

Immutable per-thread context that plays well with virtual threads (where ThreadLocal has pain around millions of threads, pinning on synchronized ops, and leaks):

java
static final ScopedValue<User> CURRENT = ScopedValue.newInstance();

ScopedValue.where(CURRENT, user).run(this::handleRequest);
// inside handleRequest: CURRENT.get()

Sequenced collections (Java 21) โ€‹

SequencedCollection, SequencedSet, SequencedMap give you first/last access uniformly across implementations:

java
list.getFirst(); list.getLast(); list.reversed();
linkedHashMap.firstEntry(); linkedHashMap.lastEntry();

Flexible constructor bodies (Java 25) โ€‹

Code before the super(...) / this(...) call is now allowed, provided it doesn't reference this:

java
public class Employee extends Person {
    public Employee(String name, double salary) {
        if (salary < 0) throw new IllegalArgumentException();  // before super()
        super(name);
        this.salary = salary;
    }
}

Compact object headers (Java 25) โ€‹

JEP that reduces the object header from 96 to 64 bits on 64-bit VMs, cutting heap footprint 10โ€“20% for object-heavy workloads. Opt-in via -XX:+UseCompactObjectHeaders.

Unnamed variables and patterns (Java 21) โ€‹

Use _ for values you don't care about:

java
for (var _ : list) count++;             // loop without binding
switch (obj) {
    case Point(int x, int _) -> ...;    // destructure but ignore y
}

Simpler void main and implicit classes (Java 25) โ€‹

A program without explicit class declaration and with a simple void main():

java
void main() {
    println("Hello");
}

Useful for scripts/tutorials. Production code still uses full class form.

Interview Qs covered โ€‹

ยง9 addresses Qs: 18 (records), 19 (sealed), 20 (pattern matching), 21 (text blocks), 22 (virtual threads).


10. JVM Internals & Memory โ€‹

Architecture โ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Classloader Subsystem                           โ”‚
โ”‚   Bootstrap โ†’ Platform โ†’ Application [โ†’ custom] โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    โ”‚
                    โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Runtime Data Areas                              โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚   โ”‚ Heap   โ”‚ โ”‚Metaspace โ”‚ โ”‚ Stacks (1/thread) โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”‚
โ”‚   โ”‚PC reg  โ”‚ โ”‚Native Method Stackโ”‚              โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                    โ”‚
                    โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Execution Engine                                โ”‚
โ”‚   Interpreter + JIT (C1/C2) + GC                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Runtime data areas โ€‹

AreaShared?Contents
HeapAll threadsObjects, arrays, class data
MetaspaceAll threadsClass metadata (method bytecode, constant pool, field/method info). Replaced PermGen in Java 8. Uses native memory, not heap.
StackPer threadMethod frames: local vars, operand stack, return address
PC registerPer threadCurrently executing instruction address
Native method stackPer threadFor JNI calls

Heap subdivisions (generational GCs):

Young Generation
โ”œโ”€โ”€ Eden      โ† new objects
โ”œโ”€โ”€ Survivor 0
โ””โ”€โ”€ Survivor 1
Old Generation โ† promoted after N young collections
  • New allocations go to Eden.
  • Minor GC copies survivors Eden โ†’ S0, flips on next cycle. Objects surviving N cycles are tenured to the old gen.
  • Major / full GC compacts old gen.
  • ZGC / Shenandoah use different layouts (region-based, no fixed gen split for ZGC generational mode in newer JDKs).

Class loading โ€‹

Three phases, each split further:

  1. Loading โ€” the classloader reads the .class file and creates a Class<?> object in metaspace. Requested via Class.forName or implicit (first new X, first static access).
  2. Linking
    • Verify โ€” bytecode safety checks (stack maps, types).
    • Prepare โ€” allocate memory for static fields, set to default values (0 / null).
    • Resolve โ€” symbolic references โ†’ direct references (lazy by default).
  3. Initialization โ€” static initializers + static field assignments run.

Classloader hierarchy:

Bootstrap (native) โ€” loads java.base etc.
    โ”‚ parent of
Platform (formerly "Extension") โ€” java.xml, java.sql, ...
    โ”‚ parent of
Application (classpath) โ€” your code
    โ”‚ parent of
Custom (OSGi, web app, Spring Boot fat jar)

Parent-delegation model: a classloader first asks its parent to find the class; only if the parent fails does it try itself. This prevents your code from shadowing java.lang.String with a malicious one.

Static vs instance initialization order โ€‹

For new Subclass():

  1. Static blocks/fields of Subclass (if not already initialized โ€” once per classloader).
  2. Static blocks/fields of Superclass โ€” actually run first, since the superclass must be initialized before its subclass.
  3. Instance init of Superclass (fields + init blocks + constructor body).
  4. Instance init of Subclass.

String pool location โ€‹

  • Pre-Java 7: PermGen.
  • Java 7+: heap. Hence OOM from too many interned strings now manifests as regular heap OOM, and the pool is subject to GC.

StackOverflowError vs OutOfMemoryError โ€‹

ErrorCauseWhereFix
StackOverflowErrorDeep / infinite recursionPer-thread stack (default ~512KBโ€“1MB)Refactor to iteration, or -Xss2m
OutOfMemoryError: Java heap spaceHeap fullHeapFind leak (see below); raise -Xmx; tune GC
OutOfMemoryError: MetaspaceLoaded too many classesMetaspaceLeaking classloaders (hot-reload app servers); -XX:MaxMetaspaceSize
OutOfMemoryError: Direct buffer memoryOff-heap NIO buffersNativeNetty/NIO leak; -XX:MaxDirectMemorySize
OutOfMemoryError: GC overhead limit exceeded>98% time in GC, <2% heap recoveredHeap pressureFix leak or raise heap

Top memory-leak sources โ€‹

  1. ThreadLocal in a thread pool โ€” threads are pooled, never die, their ThreadLocalMap keeps growing. Always threadLocal.remove() at the end of the request / task.
  2. Static caches without eviction โ€” static Map<K,V> grows forever. Use Caffeine with size/time-based eviction.
  3. Listener / callback registries โ€” register but never unregister. Use WeakReference or explicit dispose hooks.
  4. Inner / anonymous classes holding outer this โ€” worst when the inner is stored in a long-lived collection. Prefer static nested.
  5. Classloader leaks โ€” app servers / hot-reload environments. A single reference from an old classloader's static field to a long-lived object pins the whole classloader, preventing metaspace reclamation.
  6. Unclosed resources โ€” streams, PreparedStatement, DB connections. try-with-resources fixes this.
  7. Growing log context / MDC without cleanup on pooled threads.

Heap dump workflow โ€‹

  1. Trigger:
    • Auto: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof.
    • Manual: jcmd <pid> GC.heap_dump /tmp/heap.hprof (preferred) or jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>.
  2. Transfer off the pod (if K8s: kubectl cp).
  3. Open in Eclipse MAT or VisualVM / JFR (for newer flight-recorder-based analysis).
  4. In MAT: look at Dominator Tree (who retains the most memory) and Leak Suspects report. Cross-ref by class to narrow.
  5. Find the GC root path: why can't this object be collected?

Also useful: jcmd <pid> GC.class_histogram, jcmd <pid> Thread.print.

Interview Qs covered โ€‹

ยง10 addresses Qs: 23 (JVM memory), 24 (SOE vs OOM), 26 (leak analysis).


11. Garbage Collection โ€‹

Generational hypothesis โ€‹

Most objects die young.

GCs exploit this by keeping new allocations in a small, fast-collected young region. Survivors promote to the old gen, which is collected less often.

Algorithm families โ€‹

  • Mark-Sweep โ€” mark reachable, sweep garbage. Fragmentation.
  • Mark-Sweep-Compact โ€” + compaction. Moves live objects, reduces fragmentation at cost of pause time.
  • Copying โ€” split region into two halves; move live to the other half. Used in young gen.

The collectors โ€‹

GCSTW pausesHeap size sweet spotUse case
SerialLong<100MBTiny single-core apps, containers with 1 CPU
Parallel (default pre-9)MediumUp to GBThroughput-focused batch / ETL
CMSShort young + concurrent oldGBRemoved in Java 14; replaced by G1/ZGC
G1 (default since 9)~10โ€“200ms4 GB โ€“ 64 GBBalanced general-purpose
ZGCSub-ms, mostly concurrentGB to TBLow-latency services; large heaps
ShenandoahSub-msGB to TBSame goals as ZGC; OpenJDK / RedHat origin

When to pick โ€‹

  • G1 โ€” default. Good starting point for most services. -XX:MaxGCPauseMillis=200 tunes young region size.
  • ZGC โ€” when you need consistent sub-millisecond pauses, or you have a huge heap (>32GB). As of Java 21, ZGC is generational โ€” the tradeoff vs G1 narrowed. -XX:+UseZGC.
  • Shenandoah โ€” similar to ZGC; pick based on distro support. -XX:+UseShenandoahGC.
  • Parallel โ€” max throughput batch jobs where pauses don't matter.
  • Serial โ€” container with 1 CPU, <100MB heap.

Key flags (cheat sheet) โ€‹

-Xms4g -Xmx4g                        # heap (set equal in prod to avoid resize pauses)
-XX:+UseG1GC                         # pick collector
-XX:MaxGCPauseMillis=100             # G1 soft target
-Xlog:gc*:file=/var/log/gc.log:time  # unified GC logging (Java 9+)
-XX:+HeapDumpOnOutOfMemoryError      # auto dump on OOM
-XX:MaxDirectMemorySize=256m         # cap off-heap
-XX:+UseContainerSupport             # on by default 10+; honor cgroups
-XX:MaxRAMPercentage=75.0            # % of container RAM for heap
-XX:+UseCompactObjectHeaders         # Java 25+: smaller headers

Stop-the-world vs concurrent โ€‹

  • STW โ€” all application threads paused during a GC phase.
  • Concurrent โ€” GC work runs alongside the app.

G1 has short STW pauses for young collection + mixed collections, with some concurrent phases (marking). ZGC and Shenandoah aim for almost all concurrent with only tiny STW phases (< 1ms).

Reference types โ€‹

TypeReachabilityCollected whenUse case
StrongNormal referenceNever until unreachableDefault
Soft (SoftReference<T>)Softly reachableUnder memory pressureLarge optional caches
Weak (WeakReference<T>)Weakly reachableNext GC cycleWeakHashMap, canonicalizing maps, cached metadata
Phantom (PhantomReference<T>)After finalizationNever .get()s the referent; enqueued when clearedCleanup callbacks via Cleaner (Java 9+)

System.gc() โ€‹

  • A hint. JVMs may ignore or honor.
  • Under G1/ZGC usually triggers a full GC โ€” disruptive.
  • Don't call in production unless you really know why. -XX:+DisableExplicitGC is common in prod.

Interview Qs covered โ€‹

ยง11 addresses Qs: 25 (G1 vs ZGC vs Shenandoah), 26 (leak analysis).


12. Concurrency Refresher โ€‹

This section is a light touch. For ExecutorService, Locks, CompletableFuture, ForkJoinPool, virtual-thread deep dive, BlockingQueue patterns, and the full JMM formal model โ€” see the planned CONCURRENCY.md.

The Java Memory Model โ€” what you need for Core Java โ€‹

The JMM defines when one thread's writes become visible to another and what reorderings the compiler / CPU may perform.

Happens-before is the central relation. If action A happens-before action B, A's effects are visible to B. The JMM establishes happens-before via:

  • Program order within a single thread.
  • Monitor lock โ€” an unlock happens-before every subsequent lock on the same monitor (so synchronized publishes writes).
  • Volatile โ€” a write to a volatile field happens-before every subsequent read of that field.
  • Thread start/join โ€” Thread.start() happens-before the new thread's first action; a thread's actions happen-before another thread's return from join().
  • final field freezes โ€” a correctly published final field is safely visible after the constructor finishes.

volatile โ€‹

  • Guarantees: visibility (all threads see the latest write) + prevents certain reorderings.
  • Does NOT guarantee atomicity of compound ops. volatile int counter; counter++; is still a race.
  • Use: flags (volatile boolean running), "publish" of an immutable snapshot pointer, double-checked locking (requires volatile to work correctly).

synchronized โ€‹

  • Monitor lock per object (or per class for static synchronized).
  • Reentrant (same thread can re-enter a block it already holds).
  • On entry: acquire lock โ†’ memory barriers make prior unlock's writes visible. On exit: release + flush.
  • Prefer synchronize on a private final object, not on this, to avoid external code contending on your lock.
java
private final Object lock = new Object();
void tx() { synchronized (lock) { โ€ฆ } }

Atomic* classes โ€‹

AtomicInteger, AtomicLong, AtomicReference, AtomicBoolean, LongAdder, etc. Backed by CAS (Compare-And-Swap). Give you atomic compound ops:

java
AtomicInteger n = new AtomicInteger();
n.incrementAndGet();
n.compareAndSet(5, 6);
n.updateAndGet(x -> x + 1);

LongAdder trades exact count for lower contention by keeping per-thread counters. Great for hot counters; read with .sum().

ThreadLocal โ€‹

Each thread sees its own value. Classic uses: per-request context (user, trace ID), date formatters (pre-Java-8; now use DateTimeFormatter which is thread-safe).

Leak risk in pools: the thread outlives the request. If you don't remove(), values accumulate in the pool's threads โ€” unbounded leak. Framework filters (Spring's RequestContextFilter, Logback's MDC) call remove() in a finally; if you create your own, do the same.

Virtual threads: each VT has its own locals, which is great except ThreadLocal.remove() hygiene matters even more when you might create millions. Prefer ScopedValue (ยง9) for new code on Java 25.

Covers โ€‹

Minimal overlap with INTERVIEW_PREP ยง3 (Concurrency). The rest belongs in CONCURRENCY.md.


13. I/O & NIO โ€‹

Classic IO โ€” java.io โ€‹

  • Byte streams: InputStream / OutputStream. Decorator pattern: BufferedInputStream(new FileInputStream(...)), DataInputStream for typed reads, ObjectInputStream (โ†’ ยง15 Serialization, avoid).
  • Character streams: Reader / Writer. Always wrap with BufferedReader / BufferedWriter.
  • Always specify charset โ€” new InputStreamReader(in, StandardCharsets.UTF_8). Default charset depends on JVM / locale and has burned everyone.
  • Use try-with-resources.

NIO โ€” java.nio โ€‹

  • Channels (FileChannel, SocketChannel, DatagramChannel) โ€” bidirectional, non-blocking capable.
  • Buffers (ByteBuffer, CharBuffer, โ€ฆ) โ€” fixed-size arrays with position/limit/capacity semantics. Direct buffers are off-heap, faster for I/O at the cost of non-GC'd memory.
  • Selectors โ€” monitor multiple channels for readiness (readable/writable) in one thread. Foundation of reactive servers (Netty). Virtual threads let you revert to simpler blocking code in many cases.

NIO.2 โ€” java.nio.file โ€‹

Modern filesystem API. Prefer over java.io.File.

java
Path path = Path.of("/tmp/data.json");
String text = Files.readString(path, StandardCharsets.UTF_8);
Files.writeString(path, text, StandardOpenOption.CREATE_NEW);
try (var stream = Files.list(dir)) { stream.forEach(System.out::println); }
  • Files.walk, Files.find โ€” recursive traversal; close the stream (try-with-resources).
  • WatchService โ€” native filesystem change notifications.
  • FileSystems.newFileSystem(zipPath, env) โ€” treat a zip like a filesystem.

Performance tips โ€‹

  • Buffer. Never read one byte at a time from a raw FileInputStream.
  • For large files: Files.newBufferedReader, transferTo (channel โ†’ channel), memory-mapped files (FileChannel.map).
  • Always close. Always specify charset.

14. Reflection & Annotations โ€‹

Reflection โ€‹

APIs in java.lang.reflect: Class, Method, Field, Constructor, Modifier, Parameter.

java
Class<?> c = Class.forName("com.example.User");
Object obj = c.getDeclaredConstructor().newInstance();
Field f = c.getDeclaredField("email");
f.setAccessible(true);
f.set(obj, "a@b.com");
  • Powers Spring DI, JPA, Jackson, JUnit.
  • Costs: slower than direct calls (JIT handles most cases now, but cold), bypasses compile-time checks, and triggers JPMS warnings / errors (--add-opens).
  • MethodHandles / VarHandles โ€” modern alternatives with better JIT inlining.

Annotations โ€‹

Built-in:

  • @Override โ€” catches signature drift.
  • @Deprecated(since, forRemoval) โ€” mark obsolete APIs.
  • @SuppressWarnings("unchecked") โ€” opt out of specific warnings.
  • @FunctionalInterface โ€” enforce single abstract method.
  • @SafeVarargs โ€” assert heap-pollution-free varargs use.

Meta-annotations (on your custom annotations):

  • @Retention(RUNTIME | CLASS | SOURCE) โ€” how long it's kept. RUNTIME required for reflection-driven frameworks.
  • @Target โ€” where it can be placed (TYPE, METHOD, FIELD, PARAMETER, ANNOTATION_TYPE, โ€ฆ).
  • @Documented โ€” included in Javadoc.
  • @Inherited โ€” subclasses inherit the annotation (class-level only).
  • @Repeatable โ€” multiple instances on one element.

Custom annotations โ€‹

java
@Retention(RUNTIME)
@Target(METHOD)
public @interface Timed {
    String value() default "";
}

Processed at runtime (reflection) or compile time (annotation processor โ€” javax.annotation.processing.Processor, how Lombok works โ€” though Lombok hooks deeper).

How frameworks use them โ€‹

  • Spring โ€” @Component, @Autowired, @Transactional, @RequestMapping. Runtime reflection + proxies (JDK dynamic or CGLIB).
  • JPA / Hibernate โ€” @Entity, @Id, @OneToMany. Runtime; may generate bytecode for lazy proxies.
  • Jackson โ€” @JsonProperty, @JsonIgnore. Runtime reflection.
  • Lombok โ€” @Data, @Builder. Compile-time bytecode weaving; annotations vanish after compile.

15. Serialization โ€‹

Java built-in serialization โ€” Serializable โ€‹

java
public class Event implements Serializable {
    private static final long serialVersionUID = 1L;
    private String id;
    private transient Secret s;  // not serialized
}
  • serialVersionUID โ€” explicit version key for the serialized shape. Always declare it; otherwise the compiler computes one from the class structure, and minor changes (adding a method!) can break deserialization.
  • transient โ€” skip this field.
  • Hooks: private void writeObject(ObjectOutputStream) and readObject(ObjectInputStream) for custom logic; readResolve() to canonicalize (singletons).

Why Serializable is dangerous โ€‹

Deserialization invokes constructors / methods based on the input stream. An attacker-controlled stream can instantiate classes available on your classpath with attacker-chosen state. Combined with "gadget chains" (classes that trigger useful side effects in readObject / finalize / etc.), this enables remote code execution.

Famous incidents: the Apache Commons Collections gadget chain (2015) enabled RCE in WebLogic, JBoss, Jenkins, and many others simply by deserializing untrusted input.

Mitigations (in order of preference):

  1. Don't deserialize untrusted input with Java serialization. Use JSON/Protobuf/Avro.
  2. If forced, use ObjectInputFilter (Java 9+, JEP 290) to allowlist classes:
    java
    ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.myapp.*;!*");
    in.setObjectInputFilter(filter);
  3. Audit dependencies for known gadgets (OWASP Dependency Check).

Modern alternatives โ€‹

  • Jackson (JSON) โ€” default for REST APIs. Text-based; verbose but debuggable.
  • Avro โ€” compact binary, schema-registry integrated, great for Kafka. Worth discussing in interviews: schema evolution, backward/forward compatibility, naming strategies.
  • Protobuf โ€” Google-origin, strong schema, gRPC's wire format. Similar properties to Avro; less Kafka-friendly by default.
  • Kryo โ€” high-perf Java-only; not cross-language.

Interview Qs covered โ€‹

ยง15 addresses Q 28 (Serializable dangers).


16. Java Modules (JPMS) โ€‹

module-info.java โ€‹

java
module com.example.habits {
    requires spring.boot;
    requires transitive com.example.common;  // consumers also see this

    exports com.example.habits.api;          // public to all
    exports com.example.habits.internal      // public to specific module only
        to com.example.admin;

    opens com.example.habits.domain          // deep reflection access (JPA, Jackson)
        to org.hibernate.orm.core, com.fasterxml.jackson.databind;

    provides com.example.spi.Plugin
        with com.example.habits.impl.MyPlugin;

    uses com.example.spi.AuthProvider;
}

Terms:

  • requires โ€” compile- and run-time dependency.
  • requires transitive โ€” propagates to my consumers (implied readability).
  • exports โ€” public API (. paths, not classes). Optional to clause for targeted exports.
  • opens โ€” allows deep (setAccessible) reflection. Frameworks need this.
  • provides/uses โ€” ServiceLoader SPI.

Module kinds โ€‹

  • Named module โ€” has module-info.java.
  • Automatic module โ€” a plain jar placed on the module path. Name derived from Automatic-Module-Name manifest entry or the jar filename. Implicitly requires everything, opens everything. The bridge during migration.
  • Unnamed module โ€” jars on the classpath (not module path). Reads everything, exports everything. Legacy.

The practical pain โ€‹

Spring Boot fat jars predate strong encapsulation and rely heavily on reflection. Common flags:

--add-opens java.base/java.lang=ALL-UNNAMED
--add-exports java.base/sun.nio.ch=ALL-UNNAMED

JDK 16+ deny setAccessible on JDK internals by default. Expect friction when upgrading.

When JPMS actually shines:

  • Library authors who want to prevent consumers from using internal packages.
  • Building custom runtime images with jlink.
  • Strong encapsulation of plugin SPIs.

Most app developers touch JPMS only via --add-opens flags.

Interview Qs covered โ€‹

ยง16 addresses Q 29 (JPMS).


17. Null Safety โ€‹

The default stance โ€‹

null is a value for every reference type. NullPointerException is the single most common bug in enterprise Java. Your job: minimize the surface where null can sneak in or out.

Validate at boundaries โ€‹

Objects.requireNonNull โ€” fail fast with a clear message at constructor / method entry:

java
public Event(String id, Instant ts) {
    this.id = Objects.requireNonNull(id, "id");
    this.ts = Objects.requireNonNull(ts, "ts");
}

requireNonNullElse(x, fallback) โ€” supply a default.

Annotations โ€‹

Several ecosystems; none universal:

  • JSR-305 โ€” @Nullable, @Nonnull. Widely used but JSR itself was abandoned.
  • JetBrains โ€” org.jetbrains.annotations.Nullable / NotNull. IntelliJ picks these up for nullability warnings.
  • Spring Framework โ€” @Nullable, @NonNull in org.springframework.lang.
  • Checker Framework โ€” heavier, includes a compile-time type checker.
  • JSpecify โ€” the nascent consolidation effort.

Use a consistent one per project. Treat IntelliJ / SonarQube warnings as errors.

When NOT to use Optional โ€‹

  • Field โ€” adds memory per instance; use null + annotation.
  • Method parameter โ€” forces callers to wrap; allow null explicitly or provide overloads.
  • Collection element โ€” List<Optional<T>> almost always means: filter out the nothings.

Helpful NPE messages โ€‹

Since Java 14 (on by default): the exception points to which variable was null in a chain:

a.b.c.d  โ†’  Cannot invoke "Y.d()" because the return value of "X.c()" is null

Flag: -XX:+ShowCodeDetailsInExceptionMessages.

Interview Qs covered โ€‹

ยง17 addresses Q 30 (null safety without Optional-everywhere).


18. Common Gotchas & Tricky Outputs โ€‹

Predict-the-output drills. Expect 1โ€“2 of these in a live interview.

1. Integer cache โ€‹

java
Integer a = 127, b = 127;     // a == b โ†’ true  (cached)
Integer c = 128, d = 128;     // c == d โ†’ false (new instances)

2. Mutating a HashMap key โ€‹

java
List<String> k = new ArrayList<>(List.of("a"));
Map<List<String>, Integer> m = new HashMap<>();
m.put(k, 1);
k.add("b");                    // hashCode changed
m.get(k);                      // likely null โ€” wrong bucket

Never use a mutable object as a map key.

3. switch fallthrough โ€‹

java
switch (x) {
    case 1: System.out.println("one");
    case 2: System.out.println("two");    // both print if x=1
}

Prefer arrow-syntax switch (Java 14+), which never falls through.

4. Floating-point equality โ€‹

java
System.out.println(0.1 + 0.2 == 0.3); // false

Use BigDecimal for money; Math.abs(a - b) < eps for approximate compare.

5. Autoboxing in ternary โ€‹

java
Integer x = false ? 1 : null;  // null
int y = false ? 1 : null;      // NullPointerException โ€” forces unbox

6. finally overrides return โ€‹

java
int f() {
    try { return 1; } finally { return 2; }   // returns 2
}

7. Concurrent modification during iteration โ€‹

java
List<Integer> l = new ArrayList<>(List.of(1,2,3));
for (Integer i : l) if (i == 2) l.remove(i); // CME

Use Iterator.remove() or List.removeIf(...).

8. Diamond defaults โ€‹

Two interfaces provide the same default method; compiler forces the class to override.

9. String.split("") โ€‹

java
"abc".split("");        // ["a","b","c"]  (Java 8+)
"abc".toCharArray();    // ['a','b','c']

10. Arrays.asList โ€‹

java
List<Integer> l = Arrays.asList(1,2,3);
l.add(4);                                  // UnsupportedOperationException โ€” fixed-size wrapper
List<Integer> l2 = new ArrayList<>(Arrays.asList(1,2,3)); // OK

11. Array covariance + generics invariance โ€‹

java
Object[] arr = new String[3];
arr[0] = 1;                                // ArrayStoreException at runtime

List<Object> list = new ArrayList<String>(); // compile error

12. == on boxed types โ€‹

Any two boxed types compared with == outside the Integer cache range โ†’ identity compare, not value.

13. HashMap iteration order โ€‹

Not guaranteed. Never rely on it. Use LinkedHashMap if order matters.

14. Stream reuse โ€‹

A stream can be consumed once. Calling another terminal op on a finished stream throws.

15. parallelStream on ConcurrentModificationException-prone sources โ€‹

Parallel streams over a HashMap.entrySet() are safe only if no one mutates the map. Stick to ConcurrentHashMap or snapshot.


19. Build & Ecosystem โ€‹

Maven vs Gradle โ€‹

MavenGradle
Build filepom.xml (XML, verbose but predictable)build.gradle(.kts) (Groovy/Kotlin DSL)
ConventionStrongStrong + flexible
SpeedFine, not incrementalIncremental, daemon, parallel โ€” faster for large repos
Learning curveShallowSteeper (esp. plugins)

For most Spring Boot apps, Maven is enough. Gradle wins in polyglot / monorepo / custom-tooling contexts.

Maven dependency scopes โ€‹

ScopeCompileTestRuntimePackaged in WAR/JAR
compile (default)โœ“โœ“โœ“โœ“
providedโœ“โœ“
runtimeโœ“โœ“โœ“ (e.g., JDBC driver)
testโœ“
systemlegacy, avoid
importfor BOMs only

BOMs โ€‹

A Bill Of Materials pins versions for a set of related deps. Spring Boot's starter parent / spring-boot-dependencies BOM manages versions for hundreds of libraries โ€” you declare without <version> and get coherent versions.

Lombok โ€‹

Generates boilerplate at compile time via an annotation processor.

AnnotationGenerates
@Getter/@SetterAccessors
@RequiredArgsConstructorConstructor for final + @NonNull fields
@AllArgsConstructor / @NoArgsConstructorAs named
@Data@Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor
@ValueImmutable @Data (all final)
@BuilderFluent builder
@Slf4jprivate static final Logger log = ...

Gotchas:

  • Debugging: stack traces point to synthetic methods; IDE plugin required.
  • @Data on JPA entities: equals/hashCode include all fields, including lazy relations โ†’ performance disaster. Prefer @Getter/@Setter + hand-written equals/hashCode based on ID.
  • Jackson and Lombok mostly cooperate; occasionally need @JsonPOJOBuilder for @Builder.
  • In Java 17+ with records, you often don't need Lombok for DTOs.

Logging โ€‹

Stack of choice: SLF4J API + Logback or Log4j2 binding.

java
private static final Logger log = LoggerFactory.getLogger(MyService.class);

log.info("processed {} records", count);    // parameterized โ€” no string build if level off
log.error("processing failed", ex);          // pass exception as LAST arg; don't concatenate

MDC (Mapped Diagnostic Context) โ€” per-thread key-value store for log context:

java
MDC.put("traceId", traceId);
try { โ€ฆ } finally { MDC.remove("traceId"); }

With virtual threads, MDC works via ThreadLocal so the same hygiene applies. Scoped values are a future replacement.

Core libraries โ€‹

LibUse
JacksonJSON (de)serialization. Ship by default.
CaffeineIn-memory cache. Window-TinyLFU eviction, size/time-based. Default pick over legacy Guava Cache.
GuavaMisc (immutable collections pre-Java-9, hashing, preconditions). Less essential now.
Apache Commons Lang/IOStringUtils, IOUtils. Useful but smaller surface with modern Java.
MapStructCompile-time bean mapping. Much better than ModelMapper's reflection.
Resilience4jCircuit breaker, retry, bulkhead.

20. Connect to Your Experience โ€‹

Stories to anchor abstract topics. Drop these specifics into behavioral / technical answers.

Anchor example: legacy MQ microservice (10k+ tx/day) โ€‹

  • Thread sizing โ€” tune listener container concurrency vs downstream DB pool. Size the pool via Little's Law (concurrency = rate ร— latency).
  • Memory leak investigation โ€” capture a heap dump in prod under pressure; a common find is a growing MDC on a pooled thread, fixed by MDC.clear() in the listener's finally.
  • Backpressure โ€” throttle consumer concurrency when downstream latency spikes to avoid cascading OOM.
  • GC tuning โ€” move from Parallel to G1 with -XX:MaxGCPauseMillis=100 after pause-related SLO misses.

Anchor example: cross-team schema standardization โ€‹

  • Schema evolution โ€” enforce backward compatibility via schema registry; block breaking changes at CI.
  • Serialization internals โ€” Avro's compactness vs JSON's 10ร— overhead on a 10k-tx/day feed.
  • Records as DTOs โ€” use Java records for the generated domain types' container classes.
  • Pairs directly with ยง15 Serialization.

Anchor example: JAXB migration from Thymeleaf โ€‹

  • XML parsing + XXE โ€” harden XMLInputFactory (XMLConstants.FEATURE_SECURE_PROCESSING, disable DTDs and external entities). See ยง15.
  • Generics in generated code โ€” JAXBElement<T> wrapper and ObjectFactory patterns.
  • Classloader quirks โ€” JAXB moved out of the JDK in Java 11; add jakarta.xml.bind dependency explicitly.

Anchor example: ArgoCD rollouts (99% deploy success rate) โ€‹

  • Containerized JVM tuning โ€” -XX:+UseContainerSupport (on by default 10+), -XX:MaxRAMPercentage=75.0 over hard -Xmx so pod resizing stays coherent.
  • Liveness/readiness โ€” tie startup probe delays to actual app init time; use Spring Actuator /health/liveness and /health/readiness.
  • Rollback drills โ€” argocd app rollback vs git revert; practice both.

Anchor example: mentoring a ~20-person intern cohort โ€‹

  • The exact topics juniors trip on: == vs equals, mutability, HashMap with mutable keys, ThreadLocal leaks, Optional.get() without isPresent.
  • Build a small katalog (immutable class, thread-safe cache, streaming transform) โ€” these are the problems to drill on.

21. Rapid-Fire Review โ€‹

One-liners for each of the 30 INTERVIEW_PREP ยง1 questions. Morning-of.

  1. == vs .equals() โ€” == is reference identity; .equals() is logical equality. Override equals + hashCode together.
  2. hashCode/equals contract โ€” equal objects must have equal hashCodes. Break it โ†’ HashMap.get() returns null on an "equal" key; duplicates in HashSet.
  3. ArrayList vs LinkedList vs Vector โ€” ArrayList default (O(1) index, O(n) insert-middle); LinkedList rarely wins due to cache misses; Vector legacy, synchronized, skip.
  4. HashMap vs LinkedHashMap vs TreeMap vs ConcurrentHashMap โ€” unordered / insertion-order / sorted / thread-safe. CHM uses per-bucket locking + CAS.
  5. HashMap internals (8+) โ€” bucket array, chain โ†’ red-black tree at 8 nodes (and table โ‰ฅ 64), back to list at 6. Load factor 0.75.
  6. Checked vs unchecked โ€” checked = compile-time enforced, for recoverable; unchecked = programmer error. Spring leans unchecked.
  7. try-with-resources โ€” auto-closes AutoCloseable in reverse declaration order; suppressed exceptions on e.getSuppressed().
  8. Immutability โ€” final class + final fields + no setters + defensive copies + no leaking this. Thread-safe by construction.
  9. String vs StringBuilder vs StringBuffer โ€” immutable / mutable unsync / mutable sync. Builder for loops; Buffer legacy.
  10. String pool โ€” literals interned to a single heap instance; new String("x") creates a fresh object. Use .intern() to canonicalize.
  11. Type erasure โ€” generics are compile-time; at runtime T is Object (or bound). Hence no new T(), no T[], no instanceof T.
  12. PECS โ€” Producer Extends, Consumer Super. List<? extends T> read-only-of-T; List<? super T> write-T.
  13. Optional โ€” return types only. Not for fields / params / collection elements.
  14. Streams intermediate vs terminal โ€” intermediate lazy, terminal triggers execution. Short-circuiting ops stop early.
  15. map vs flatMap โ€” 1:1 transform vs 1:many with flattening (Stream<Stream<T>> โ†’ Stream<T>).
  16. parallelStream pitfalls โ€” ForkJoin common pool, shared state races, blocking I/O monopolizes the pool, small N is overhead. Benchmark.
  17. Functional interfaces โ€” Function, Predicate, Consumer, Supplier, BiFunction, UnaryOperator, BinaryOperator.
  18. Records โ€” compiler-generated final fields / constructor / accessors / equals+hashCode+toString. Not for mutable state or inheritance.
  19. Sealed classes โ€” permits list restricts hierarchy; subclasses final/sealed/non-sealed. Enables exhaustive switches.
  20. Pattern matching โ€” obj instanceof User u binds; switch on types and record patterns destructures; when adds guards.
  21. Text blocks โ€” """โ€ฆ"""; common indentation stripped. Great for embedded JSON/SQL/XML.
  22. Virtual threads โ€” lightweight, JVM-scheduled, M:N on carrier threads. Great for blocking I/O at scale. Pinning on synchronized (improved in 24+) and JNI is the main gotcha.
  23. JVM memory โ€” heap (young: Eden+S0+S1, old) + metaspace (class metadata, native) + stack per thread. String pool in heap since 7.
  24. SOE vs OOM โ€” SOE: deep recursion, per-thread stack. OOM: heap / metaspace / direct buffer / GC-overhead. Dump heap + MAT for OOM.
  25. G1 vs ZGC vs Shenandoah โ€” G1 default balanced; ZGC sub-ms on large heaps; Shenandoah similar, Red Hat origin. Pick ZGC for strict latency SLOs.
  26. Leak analysis โ€” jcmd <pid> GC.heap_dump โ†’ MAT dominator tree + leak suspects. Root causes: ThreadLocal in pools, static caches, inner-class outer refs, classloader leaks.
  27. final / finally / finalize โ€” final no-reassign/override/extend; finally always runs (except JVM death); finalize deprecated, use AutoCloseable/Cleaner.
  28. Serializable dangers โ€” gadget chains โ†’ RCE. Use Object Input Filters if forced; prefer JSON/Avro/Protobuf.
  29. JPMS โ€” named modules, exports/requires/opens, strong encapsulation. Most pain is --add-opens in reflection-heavy frameworks.
  30. Null safety โ€” Objects.requireNonNull at boundaries; @Nullable/@NonNull annotations; Optional for return types only. -XX:+ShowCodeDetailsInExceptionMessages (default 14+).

22. Practice Exercises โ€‹

Tier 1 โ€” Code from memory โ€‹

Set a timer; no IDE help.

A. Immutable Event class

  • Fields: String id, Instant timestamp, List<String> tags, Map<String, String> attrs.
  • Truly immutable: final class, defensive copies, equals / hashCode based on id.
  • toString useful for logs.

B. LRU cache with LinkedHashMap

java
public class Lru<K,V> extends LinkedHashMap<K,V> {
    private final int cap;
    public Lru(int cap) { super(cap, 0.75f, true); this.cap = cap; }
    @Override protected boolean removeEldestEntry(Map.Entry<K,V> e) {
        return size() > cap;
    }
}
  • Then rewrite it from scratch using HashMap + doubly-linked list.

C. Thread-safe singleton

  • Eager (preferred):
java
public final class Reg { public static final Reg INSTANCE = new Reg(); private Reg() {} }
  • Enum (Effective Java Item 3): public enum Reg { INSTANCE; }
  • Lazy double-checked locking (for the record):
java
private static volatile Reg instance;
public static Reg get() {
    Reg r = instance;
    if (r == null) synchronized (Reg.class) {
        if ((r = instance) == null) r = instance = new Reg();
    }
    return r;
}

D. Correct equals / hashCode

  • Write one for a class with long id, String name, LocalDate dob. Use Objects.hash(...). Satisfy all five equals rules.

Tier 2 โ€” Predict the output โ€‹

Run through the 15 gotchas in ยง18 without peeking. Target: 12/15 right.

Tier 3 โ€” Mini design โ€‹

A. Memoizing function cache

  • Implement <T,R> Function<T,R> memoize(Function<T,R> fn) thread-safe using ConcurrentHashMap.computeIfAbsent. Handle null returns correctly. Add optional size cap (Caffeine-style) as a stretch.

B. Min-and-max in one stream pass

  • Write a Collector<Integer, ?, int[]> that returns {min, max} in a single pass. Use Collector.of(...). Handle the empty case. Then bonus: support a combiner for parallel streams.

C. Safe ThreadLocal-based request context

  • Build RequestContext with set(ctx), get(), clear(), and a run(ctx, Runnable) helper that always clears in a finally. Write a tiny JUnit test that proves reuse across a FixedThreadPool doesn't leak state.

Spaced-review checklist โ€‹

Mark โœ… / ๐Ÿ” / โŒ per section. Repeat โŒ daily, ๐Ÿ” every other day, โœ… weekly.

  • [ ] ยง1 Basics
  • [ ] ยง2 OOP
  • [ ] ยง3 Strings
  • [ ] ยง4 Immutability
  • [ ] ยง5 Exceptions
  • [ ] ยง6 Generics
  • [ ] ยง7 Collections
  • [ ] ยง8 Functional
  • [ ] ยง9 Modern Java (especially sealed, pattern matching, virtual threads)
  • [ ] ยง10 JVM memory
  • [ ] ยง11 GC
  • [ ] ยง12 Concurrency refresher
  • [ ] ยง13 I/O
  • [ ] ยง14 Reflection / annotations
  • [ ] ยง15 Serialization
  • [ ] ยง16 JPMS
  • [ ] ยง17 Null safety
  • [ ] ยง18 Gotchas (drill 15)
  • [ ] ยง19 Build / ecosystem
  • [ ] ยง20 Experience tie-ins

Further reading โ€‹

  • Java Language Spec (JLS) and JVM Spec โ€” definitive source of truth.
  • Effective Java (Joshua Bloch, 3rd ed.) โ€” items 17 (immutability), 18 (composition over inheritance), 49 (param validation), 80 (executors over threads).
  • Java Concurrency in Practice (Goetz) โ€” the JMM / concurrency reference. Companion to the future CONCURRENCY.md.
  • Oracle JEPs โ€” authoritative notes on every new feature. Cross-reference rather than blog posts.
  • Inside.java โ€” Oracle's technical blog; good for Loom / Amber / Valhalla developments.

Next in the series: CONCURRENCY.md (full depth on executors, locks, CompletableFuture, ForkJoin, virtual threads internals, structured concurrency), then SPRING_BOOT.md.

Last updated: