Appearance
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/ThreadLocalbasics) โ full depth lives in a futureCONCURRENCY.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 โ
- Java Platform & Language Basics
- Object-Oriented Java
- Strings & Text
- Immutability
- Exception Handling
- Generics
- Collections Framework
- Functional Java โ Lambdas, Streams, Optional
- Modern Java Features (Java 11 โ 25)
- JVM Internals & Memory
- Garbage Collection
- Concurrency Refresher
- I/O & NIO
- Reflection & Annotations
- Serialization
- Java Modules (JPMS)
- Null Safety
- Common Gotchas & Tricky Outputs
- Build & Ecosystem
- Connect to Your Experience
- Rapid-Fire Review
- 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# | Topic | Section |
|---|---|---|
| 1 | == vs .equals() | ยง1 |
| 2 | hashCode/equals contract | ยง1 |
| 3 | ArrayList/LinkedList/Vector | ยง7 |
| 4 | HashMap/LinkedHashMap/TreeMap/ConcurrentHashMap | ยง7 |
| 5 | HashMap internals (Java 8+) | ยง7 |
| 6 | Checked vs unchecked exceptions | ยง5 |
| 7 | try-with-resources / AutoCloseable | ยง5 |
| 8 | Immutability recipe | ยง4 |
| 9 | String vs StringBuilder vs StringBuffer | ยง3 |
| 10 | String pool / new String("abc") vs "abc" | ยง3 |
| 11 | Type erasure | ยง6 |
| 12 | Covariance / contravariance / PECS | ยง6 |
| 13 | Optional โ when NOT to use it | ยง8 |
| 14 | Streams โ intermediate vs terminal, lazy eval | ยง8 |
| 15 | map vs flatMap | ยง8 |
| 16 | parallelStream() pitfalls | ยง8 |
| 17 | Functional interfaces | ยง8 |
| 18 | Records | ยง4, ยง9 |
| 19 | Sealed classes | ยง9 |
| 20 | Pattern matching (instanceof / switch) | ยง9 |
| 21 | Text blocks | ยง3, ยง9 |
| 22 | Virtual threads & pinning | ยง9, ยง12 |
| 23 | JVM memory model โ heap/stack/metaspace/gens | ยง10 |
| 24 | StackOverflowError vs OutOfMemoryError | ยง10 |
| 25 | G1 vs ZGC vs Shenandoah | ยง11 |
| 26 | Memory leak analysis in a running JVM | ยง10, ยง11 |
| 27 | final vs finally vs finalize | ยง1, ยง5 |
| 28 | Serializable โ why dangerous | ยง15 |
| 29 | Java Modules (JPMS) | ยง16 |
| 30 | Null safety without Optional-everywhere | ยง17 |
1. Java Platform & Language Basics โ
JDK vs JRE vs JVM โ
- JVM โ the spec + runtime that executes
.classbytecode. 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 =
javacproduces 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 nullThis 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. DefaultObject.equalsis==. 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 equalityequals / hashCode contract โ
Five rules for equals:
- Reflexive โ
x.equals(x)is true. - Symmetric โ
x.equals(y)โy.equals(x). - Transitive โ if
x.equals(y)andy.equals(z), thenx.equals(z). - Consistent โ repeated calls return the same result so long as objects don't change.
- 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
equalswithouthashCodeโ objects that are "equal" land in different buckets of aHashMap/HashSet.map.put(key, v)thenmap.get(equalKey)returnsnull. Duplicates appear inHashSet. - Mutate a field used in
hashCodeafter 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:
| Keyword | What it does |
|---|---|
final | Prevents reassignment (final var), overriding (final method), or subclassing (final class). |
finally | Block 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 โ
| Pillar | Java mechanism |
|---|---|
| Encapsulation | Private fields + public methods. Lombok shortens this. |
| Inheritance | Single-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). |
| Polymorphism | Runtime dispatch on overridden methods. Compile-time via overloading. |
| Abstraction | Abstract classes + interfaces. |
Abstract class vs interface โ
| Abstract class | Interface | |
|---|---|---|
| Instance state | Fields + constructors allowed | Only static final constants |
| Multiple inheritance | One per class | Multi-implement |
| Method bodies | Yes (any access) | default + static + private (Java 8 / 9) |
| Use case | Partial implementation with state | Pure 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 โ
| Modifier | Same class | Same pkg | Subclass (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)vsadd(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 โ
| Kind | Holds outer this? | Use |
|---|---|---|
| Static nested | No | Helper class logically grouped with outer (e.g., Map.Entry) |
| Inner (non-static) | Yes โ implicit Outer.this | Rarely needed; enables tight coupling to outer state |
| Local | Yes (effectively final captures) | One-off inside a method |
| Anonymous | Yes (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
Stringacross 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 entryGotcha: 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 โ
| Class | Mutable | Thread-safe | Perf |
|---|---|---|---|
String | No | Inherently | Concat = new object each time |
StringBuilder | Yes | No | ~4x faster than StringBuffer |
StringBuffer | Yes | Yes (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,\sfor 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.printffor stdout.- Always specify a
Localefor 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:
- Declare the class
final(or use a sealed hierarchy โ ยง9). - All fields
private final. - No setters, no mutators.
- Defensive-copy mutable constructor arguments on the way in and on the way out.
- Don't leak
thisduring construction (nothis::methodregistration 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
getprefix). equals,hashCode,toStringbased 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)); // trueCompact 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+. Returnsotherif already immutable; otherwise copies.Collections.unmodifiableList(x)โ a view, not a copy. If you keep a reference toxand 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, notRuntimeException) โ compiler forces you to catch or declare (throws). - Unchecked (
extends RuntimeExceptionorError) โ 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 uncheckedDataAccessExceptionhierarchy so your repository methods aren't noise. - Checked exceptions don't compose with lambdas (most
java.util.functiontypes don't declarethrows), 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
causetonew 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(orCloseable, which extends it and restricts toIOException). - If both the
trybody andclose()throw, theclose()exception becomes a suppressed exception on the primary. Retrievable viae.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
returnfromtry.finallycan 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 + hopeWith 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,Tdoesn't exist.- Workaround: pass
Class<T>and callclazz.getDeclaredConstructor().newInstance().
- Workaround: pass
- 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.
- Workaround:
instanceof Tfails (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>andList<Integer>are the sameClassat 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 readObject; you cannot add (exceptnull).List<? extends Number>โ a list of Number or any subtype. You can read asNumber; you cannot add (since the exact subtype is unknown).List<? super Integer>โ a list of Integer or any supertype. You can addInteger(or subtype); reading yieldsObject.
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);
}srcproducesTs (read) โextends.destconsumesTs (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 noinstanceof 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 โ
| Impl | Backing | Random access | Insert at end | Insert in middle | Thread-safe |
|---|---|---|---|---|---|
ArrayList | Resizable array | O(1) | amortized O(1) | O(n) | No |
LinkedList | Doubly-linked nodes | O(n) | O(1) | O(1) if you have node ref, O(n) to find | No |
Vector | Resizable array | O(1) | O(1) | O(n) | Yes (every method synchronized) โ legacy |
CopyOnWriteArrayList | Array, copy on write | O(1) | O(n) (full copy) | O(n) | Yes โ reads lock-free |
Practical advice:
- Default to
ArrayList. LinkedListsounds 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 withCollections.synchronizedListor useCopyOnWriteArrayList(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 (byComparableorComparator), O(log n);NavigableSetmethods (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 flagaccessOrder=trueenables 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==notequals; 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. nullkey allowed (goes in bucket 0);nullvalues 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
volatilesemantics on the node array. - CAS on empty-bucket insertions.
synchronizedon the head node of a non-empty bucket for inserts/updates. Fine-grained: different buckets contend only when hashing to the same slot.
- Lock-free reads via
- No
nullkeys, nonullvalues โ avoids ambiguity with "key not present" in concurrent reads. - Atomic compound ops:
computeIfAbsent,merge,putIfAbsent. size()is an approximation under contention; usemappingCount()for along.
Hashtable vs synchronizedMap vs ConcurrentHashMap โ
Hashtableโ entire map synchronized; single lock; legacy.Collections.synchronizedMap(hashMap)โ also single lock, wrapped; iteration still needs manualsynchronized(map) { for (...) }.ConcurrentHashMapโ per-bucket fine-grained locking, weakly-consistent iterators (noConcurrentModificationException). Use this.
Queue & Deque โ
ArrayDequeโ default stack + deque. BeatsStack(legacy, synchronized).PriorityQueueโ min-heap by default; provide aComparatorfor max-heap or custom order.BlockingQueuefamily: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 throwConcurrentModificationException. 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:
| Interface | Method | Shape |
|---|---|---|
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 errorCaptured 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 โ
| Form | Example |
|---|---|
| Static method | Integer::parseInt |
| Instance method of a specific object | sb::append |
| Instance method of an arbitrary object of a given type | String::length |
| Constructor | ArrayList::new |
Streams โ
A stream is a pipeline of operations over a data source. Three stages:
- Source โ collection, array, I/O channel,
Stream.of(...),Stream.generate(...). - Intermediate ops โ lazy, return another stream (
filter,map,flatMap,sorted,distinct,peek,limit,skip). - 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-circuitsmap vs flatMap โ
map(fn)โStream<T>โStream<R>. One-to-one.flatMap(fn)โStream<T>โStream<R>wherefn: 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 getIllegalStateException.groupingBy(fn)โ returnsMap<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 aList<Optional<T>>is almost always wrong.
- Instance field โ use
- Use
ifPresent,orElse,orElseThrow,map,flatMap. - Don't
optional.get()withoutisPresent()โ defeats the point. - Don't wrap when it could be
null: useOptional.ofNullable(x)notOptional.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) โ
| Version | Released | Highlights |
|---|---|---|
| 8 | 2014 | Lambdas, streams, Optional, default methods |
| 11 (LTS) | 2018 | var (10), HTTP client, String enhancements |
| 14 | 2020 | Records (preview), helpful NPEs, switch expressions standard |
| 15 | 2020 | Text blocks standard, sealed classes (preview) |
| 16 | 2021 | Records standard, pattern matching for instanceof standard |
| 17 (LTS) | 2021 | Sealed classes standard |
| 21 (LTS) | 2023 | Virtual threads, pattern matching for switch, record patterns, sequenced collections |
| 23 | 2024 | Unnamed classes / instance main (preview) |
| 25 (LTS) | 2025-09 | Flexible 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
nullinitializer, 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
defaultneeded.
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)orExecutors.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
synchronizedblock 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 forsynchronized. Still: preferReentrantLockfor long-held locks. - When they don't help: CPU-bound work โ you still need roughly one thread per core.
ForkJoinPoolor 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 cancelledScoped 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 โ
| Area | Shared? | Contents |
|---|---|---|
| Heap | All threads | Objects, arrays, class data |
| Metaspace | All threads | Class metadata (method bytecode, constant pool, field/method info). Replaced PermGen in Java 8. Uses native memory, not heap. |
| Stack | Per thread | Method frames: local vars, operand stack, return address |
| PC register | Per thread | Currently executing instruction address |
| Native method stack | Per thread | For 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:
- Loading โ the classloader reads the
.classfile and creates aClass<?>object in metaspace. Requested viaClass.forNameor implicit (firstnew X, first static access). - 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).
- 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():
- Static blocks/fields of
Subclass(if not already initialized โ once per classloader). - Static blocks/fields of
Superclassโ actually run first, since the superclass must be initialized before its subclass. - Instance init of
Superclass(fields + init blocks + constructor body). - 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 โ
| Error | Cause | Where | Fix |
|---|---|---|---|
StackOverflowError | Deep / infinite recursion | Per-thread stack (default ~512KBโ1MB) | Refactor to iteration, or -Xss2m |
OutOfMemoryError: Java heap space | Heap full | Heap | Find leak (see below); raise -Xmx; tune GC |
OutOfMemoryError: Metaspace | Loaded too many classes | Metaspace | Leaking classloaders (hot-reload app servers); -XX:MaxMetaspaceSize |
OutOfMemoryError: Direct buffer memory | Off-heap NIO buffers | Native | Netty/NIO leak; -XX:MaxDirectMemorySize |
OutOfMemoryError: GC overhead limit exceeded | >98% time in GC, <2% heap recovered | Heap pressure | Fix leak or raise heap |
Top memory-leak sources โ
ThreadLocalin a thread pool โ threads are pooled, never die, theirThreadLocalMapkeeps growing. AlwaysthreadLocal.remove()at the end of the request / task.- Static caches without eviction โ
static Map<K,V>grows forever. Use Caffeine with size/time-based eviction. - Listener / callback registries โ register but never unregister. Use
WeakReferenceor explicit dispose hooks. - Inner / anonymous classes holding outer
thisโ worst when the inner is stored in a long-lived collection. Prefer static nested. - 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.
- Unclosed resources โ streams,
PreparedStatement, DB connections.try-with-resourcesfixes this. - Growing log context / MDC without cleanup on pooled threads.
Heap dump workflow โ
- Trigger:
- Auto:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof. - Manual:
jcmd <pid> GC.heap_dump /tmp/heap.hprof(preferred) orjmap -dump:live,format=b,file=/tmp/heap.hprof <pid>.
- Auto:
- Transfer off the pod (if K8s:
kubectl cp). - Open in Eclipse MAT or VisualVM / JFR (for newer flight-recorder-based analysis).
- In MAT: look at Dominator Tree (who retains the most memory) and Leak Suspects report. Cross-ref by class to narrow.
- 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 โ
| GC | STW pauses | Heap size sweet spot | Use case |
|---|---|---|---|
| Serial | Long | <100MB | Tiny single-core apps, containers with 1 CPU |
| Parallel (default pre-9) | Medium | Up to GB | Throughput-focused batch / ETL |
| CMS | Short young + concurrent old | GB | Removed in Java 14; replaced by G1/ZGC |
| G1 (default since 9) | ~10โ200ms | 4 GB โ 64 GB | Balanced general-purpose |
| ZGC | Sub-ms, mostly concurrent | GB to TB | Low-latency services; large heaps |
| Shenandoah | Sub-ms | GB to TB | Same goals as ZGC; OpenJDK / RedHat origin |
When to pick โ
- G1 โ default. Good starting point for most services.
-XX:MaxGCPauseMillis=200tunes 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 headersStop-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 โ
| Type | Reachability | Collected when | Use case |
|---|---|---|---|
| Strong | Normal reference | Never until unreachable | Default |
Soft (SoftReference<T>) | Softly reachable | Under memory pressure | Large optional caches |
Weak (WeakReference<T>) | Weakly reachable | Next GC cycle | WeakHashMap, canonicalizing maps, cached metadata |
Phantom (PhantomReference<T>) | After finalization | Never .get()s the referent; enqueued when cleared | Cleanup 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:+DisableExplicitGCis 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,BlockingQueuepatterns, and the full JMM formal model โ see the plannedCONCURRENCY.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
synchronizedpublishes writes). - Volatile โ a write to a
volatilefield 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 fromjoin(). finalfield freezes โ a correctly publishedfinalfield 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 (requiresvolatileto 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(...)),DataInputStreamfor typed reads,ObjectInputStream(โ ยง15 Serialization, avoid). - Character streams:
Reader/Writer. Always wrap withBufferedReader/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.RUNTIMErequired 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)andreadObject(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):
- Don't deserialize untrusted input with Java serialization. Use JSON/Protobuf/Avro.
- If forced, use ObjectInputFilter (Java 9+,
JEP 290) to allowlist classes:javaObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.myapp.*;!*"); in.setObjectInputFilter(filter); - 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). Optionaltoclause 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-Namemanifest 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-UNNAMEDJDK 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,@NonNullinorg.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
nullexplicitly 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 nullFlag: -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 bucketNever 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); // falseUse 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 unbox6. 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); // CMEUse 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)); // OK11. Array covariance + generics invariance โ
java
Object[] arr = new String[3];
arr[0] = 1; // ArrayStoreException at runtime
List<Object> list = new ArrayList<String>(); // compile error12. == 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 โ
| Maven | Gradle | |
|---|---|---|
| Build file | pom.xml (XML, verbose but predictable) | build.gradle(.kts) (Groovy/Kotlin DSL) |
| Convention | Strong | Strong + flexible |
| Speed | Fine, not incremental | Incremental, daemon, parallel โ faster for large repos |
| Learning curve | Shallow | Steeper (esp. plugins) |
For most Spring Boot apps, Maven is enough. Gradle wins in polyglot / monorepo / custom-tooling contexts.
Maven dependency scopes โ
| Scope | Compile | Test | Runtime | Packaged in WAR/JAR |
|---|---|---|---|---|
compile (default) | โ | โ | โ | โ |
provided | โ | โ | ||
runtime | โ | โ | โ (e.g., JDBC driver) | |
test | โ | |||
system | legacy, avoid | |||
import | for 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.
| Annotation | Generates |
|---|---|
@Getter/@Setter | Accessors |
@RequiredArgsConstructor | Constructor for final + @NonNull fields |
@AllArgsConstructor / @NoArgsConstructor | As named |
@Data | @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor |
@Value | Immutable @Data (all final) |
@Builder | Fluent builder |
@Slf4j | private static final Logger log = ... |
Gotchas:
- Debugging: stack traces point to synthetic methods; IDE plugin required.
@Dataon JPA entities:equals/hashCodeinclude all fields, including lazy relations โ performance disaster. Prefer@Getter/@Setter+ hand-writtenequals/hashCodebased on ID.- Jackson and Lombok mostly cooperate; occasionally need
@JsonPOJOBuilderfor@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 concatenateMDC (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 โ
| Lib | Use |
|---|---|
| Jackson | JSON (de)serialization. Ship by default. |
| Caffeine | In-memory cache. Window-TinyLFU eviction, size/time-based. Default pick over legacy Guava Cache. |
| Guava | Misc (immutable collections pre-Java-9, hashing, preconditions). Less essential now. |
| Apache Commons Lang/IO | StringUtils, IOUtils. Useful but smaller surface with modern Java. |
| MapStruct | Compile-time bean mapping. Much better than ModelMapper's reflection. |
| Resilience4j | Circuit 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'sfinally. - Backpressure โ throttle consumer concurrency when downstream latency spikes to avoid cascading OOM.
- GC tuning โ move from Parallel to G1 with
-XX:MaxGCPauseMillis=100after 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 andObjectFactorypatterns. - Classloader quirks โ JAXB moved out of the JDK in Java 11; add
jakarta.xml.binddependency explicitly.
Anchor example: ArgoCD rollouts (99% deploy success rate) โ
- Containerized JVM tuning โ
-XX:+UseContainerSupport(on by default 10+),-XX:MaxRAMPercentage=75.0over hard-Xmxso pod resizing stays coherent. - Liveness/readiness โ tie startup probe delays to actual app init time; use Spring Actuator
/health/livenessand/health/readiness. - Rollback drills โ
argocd app rollbackvsgit revert; practice both.
Anchor example: mentoring a ~20-person intern cohort โ
- The exact topics juniors trip on:
==vsequals, mutability,HashMapwith mutable keys,ThreadLocalleaks,Optional.get()withoutisPresent. - 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.
==vs.equals()โ==is reference identity;.equals()is logical equality. Overrideequals+hashCodetogether.- hashCode/equals contract โ equal objects must have equal hashCodes. Break it โ
HashMap.get()returns null on an "equal" key; duplicates inHashSet. - ArrayList vs LinkedList vs Vector โ
ArrayListdefault (O(1) index, O(n) insert-middle);LinkedListrarely wins due to cache misses;Vectorlegacy, synchronized, skip. - HashMap vs LinkedHashMap vs TreeMap vs ConcurrentHashMap โ unordered / insertion-order / sorted / thread-safe. CHM uses per-bucket locking + CAS.
- HashMap internals (8+) โ bucket array, chain โ red-black tree at 8 nodes (and table โฅ 64), back to list at 6. Load factor 0.75.
- Checked vs unchecked โ checked = compile-time enforced, for recoverable; unchecked = programmer error. Spring leans unchecked.
- try-with-resources โ auto-closes
AutoCloseablein reverse declaration order; suppressed exceptions one.getSuppressed(). - Immutability โ final class + final fields + no setters + defensive copies + no leaking
this. Thread-safe by construction. - String vs StringBuilder vs StringBuffer โ immutable / mutable unsync / mutable sync. Builder for loops; Buffer legacy.
- String pool โ literals interned to a single heap instance;
new String("x")creates a fresh object. Use.intern()to canonicalize. - Type erasure โ generics are compile-time; at runtime
TisObject(or bound). Hence nonew T(), noT[], noinstanceof T. - PECS โ Producer Extends, Consumer Super.
List<? extends T>read-only-of-T;List<? super T>write-T. - Optional โ return types only. Not for fields / params / collection elements.
- Streams intermediate vs terminal โ intermediate lazy, terminal triggers execution. Short-circuiting ops stop early.
- map vs flatMap โ 1:1 transform vs 1:many with flattening (
Stream<Stream<T>>โStream<T>). - parallelStream pitfalls โ ForkJoin common pool, shared state races, blocking I/O monopolizes the pool, small N is overhead. Benchmark.
- Functional interfaces โ
Function,Predicate,Consumer,Supplier,BiFunction,UnaryOperator,BinaryOperator. - Records โ compiler-generated final fields / constructor / accessors / equals+hashCode+toString. Not for mutable state or inheritance.
- Sealed classes โ
permitslist restricts hierarchy; subclassesfinal/sealed/non-sealed. Enables exhaustive switches. - Pattern matching โ
obj instanceof User ubinds;switchon types and record patterns destructures;whenadds guards. - Text blocks โ
"""โฆ"""; common indentation stripped. Great for embedded JSON/SQL/XML. - 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. - JVM memory โ heap (young: Eden+S0+S1, old) + metaspace (class metadata, native) + stack per thread. String pool in heap since 7.
- SOE vs OOM โ SOE: deep recursion, per-thread stack. OOM: heap / metaspace / direct buffer / GC-overhead. Dump heap + MAT for OOM.
- 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.
- 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. - final / finally / finalize โ
finalno-reassign/override/extend;finallyalways runs (except JVM death);finalizedeprecated, useAutoCloseable/Cleaner. - Serializable dangers โ gadget chains โ RCE. Use Object Input Filters if forced; prefer JSON/Avro/Protobuf.
- JPMS โ named modules, exports/requires/opens, strong encapsulation. Most pain is
--add-opensin reflection-heavy frameworks. - Null safety โ
Objects.requireNonNullat boundaries;@Nullable/@NonNullannotations; 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/hashCodebased on id. toStringuseful 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. UseObjects.hash(...). Satisfy all fiveequalsrules.
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 usingConcurrentHashMap.computeIfAbsent. Handlenullreturns 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. UseCollector.of(...). Handle the empty case. Then bonus: support a combiner for parallel streams.
C. Safe ThreadLocal-based request context
- Build
RequestContextwithset(ctx),get(),clear(), and arun(ctx, Runnable)helper that always clears in afinally. Write a tiny JUnit test that proves reuse across aFixedThreadPooldoesn'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.