Java · cheat sheet

Java

OOP and SOLID, records and sealed types, collections internals, generics, streams, concurrency and virtual threads, JVM memory and GC, and Java 8 to 25.

Core Java for interviews on one page: language rules, collections internals, streams, concurrency and the JVM, current to Java 25.

OOP & SOLID

  • Four pillars: encapsulation (hide state), abstraction (hide how), inheritance (one extends, many implements), polymorphism (one call, many behaviors).
  • Overloading is compile-time: same name, different parameters (return type alone is not enough). Overriding is runtime: same signature, covariant return allowed, no narrower visibility, no broader checked exceptions.
  • static, private and final methods are not overridden (a same-signature static method hides the parent’s). Fields are never polymorphic.
  • Prefer composition over inheritance. Immutable class: final class, private final fields, no setters, defensive copies.
Principle Means Smell it fixes
Single responsibility one reason to change god classes
Open/closed extend by adding code, not editing it switch on type everywhere
Liskov substitution subtypes honor the parent’s contract Square extends Rectangle
Interface segregation small, focused interfaces stub methods that throw
Dependency inversion depend on abstractions, inject them new of concrete services

Types & modifiers

public record Money(BigDecimal amount, String currency) {
  public Money {                                  // compact canonical constructor
    if (amount.signum() < 0) throw new IllegalArgumentException("negative");
  }
}
java
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}

double area(Shape s) {
  return switch (s) {                             // exhaustive, no default (21)
    case Circle c -> Math.PI * c.r() * c.r();
    case Square(double side) -> side * side;      // record pattern
  };
}
java
  • Record (16): implicitly final; gets private final fields, accessors amount() (not getAmount()), equals, hashCode, toString. No extra instance fields; immutability is shallow.

  • Sealed (17): permitted subclasses must be final, sealed or non-sealed, in the same module (or package).

  • Enum: private constructors, fields, methods, per-constant bodies; values(), valueOf(), ordinal(); compare with ==; pair with EnumMap/EnumSet.

  • Interface: fields are public static final; default and static methods (8), private methods (9).

  • Access, narrowest first: private (class), no keyword (package), protected (package + subclasses), public (everyone). Top-level classes are public or package-private.

  • Abstract class vs interface: an abstract class has state and constructors and allows one extends; an interface has only constants, allows many implements, and models a capability.

equals, hashCode & Strings

@Override public boolean equals(Object o) {
  if (this == o) return true;
  if (!(o instanceof Point p)) return false;      // or getClass() != o.getClass()
  return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
java
  • equals is reflexive, symmetric, transitive, consistent, and x.equals(null) is false.
  • Equal objects must have equal hash codes; unequal ones may collide. Override both, or HashSet keeps “duplicates”.
  • Comparable is the natural order; Comparator is external: comparing(User::age).thenComparing(User::name). Never compare with a - b (overflow).
  • Strings are immutable with a cached hash. Literals are interned in the string pool (on the heap since Java 7).
String a = "hi", b = "hi", c = new String("hi");
a == b;           // true: same pooled literal
a == c;           // false: new heap object
a == c.intern();  // true
a.equals(c);      // true: always compare with equals
java
  • String is immutable; StringBuilder is mutable and unsynchronized (use it in loops); StringBuffer is its synchronized legacy twin.

Exceptions

  • Throwable splits into Error (OutOfMemoryError, StackOverflowError: don’t catch) and Exception.
  • Exception subclasses are checked (IOException, SQLException, InterruptedException) except RuntimeException and its subclasses, which are unchecked (NullPointerException, IllegalArgumentException, IllegalStateException).
try (var in = Files.newBufferedReader(src);
     var out = Files.newBufferedWriter(dst)) {       // any AutoCloseable
  in.transferTo(out);
} catch (IOException e) {                            // close() failures: e.getSuppressed()
  throw new UncheckedIOException("copy failed", e);  // keep the cause
}
java
  • Resources close in reverse order, before catch and finally run. Multi-catch: catch (IOException | SQLException e); catch subclasses first.
  • finally runs even after return, but not after System.exit(). A return inside finally swallows the exception.
  • Checked for recoverable conditions the caller must handle; unchecked for programming errors.

Collections

Implementation Lookup Add / remove Order Nulls
ArrayList O(1) by index O(1) amortized at end, O(n) middle insertion yes
LinkedList O(n) O(1) at ends insertion yes
ArrayDeque ends only O(1) amortized at ends FIFO / LIFO no
HashMap / HashSet O(1) avg O(1) avg none one null key
LinkedHashMap O(1) O(1) insertion or access yes
TreeMap / TreeSet O(log n) O(log n) sorted no null keys
PriorityQueue peek O(1) offer/poll O(log n) heap, not sorted no
ConcurrentHashMap O(1) avg O(1) avg none no
CopyOnWriteArrayList O(1) O(n), copies the array insertion yes
  • Use ArrayDeque for stacks and queues. ArrayList starts at capacity 10 and grows about 1.5x.
  • List.of/Map.of are unmodifiable and reject null; Arrays.asList is fixed-size but writable; Collections.unmodifiableList is a read-only view.
  • Sequenced collections (21) add getFirst(), getLast() and reversed().
  • Iterators are fail-fast (ConcurrentModificationException); concurrent collections are weakly consistent. Remove while looping with iterator.remove() or removeIf.

HashMap & ConcurrentHashMap

  • An array of buckets whose length is a power of two: default capacity 16, load factor 0.75, so it doubles past 12 entries.
  • hash = h ^ (h >>> 16), bucket = (n - 1) & hash; put compares hash, then ==/equals, and replaces or appends.
  • Java 8+: a bucket past 8 entries becomes a red-black tree (once the table has 64 buckets; before that it resizes), so the worst case is O(log n).
  • Mutating a field used by hashCode after put makes the entry unreachable.
  • ConcurrentHashMap (8+): no segments; CAS into empty buckets, synchronized on the bucket head, lock-free reads. No null keys or values.
  • Use its atomic methods (putIfAbsent, computeIfAbsent, merge(w, 1, Integer::sum)): check-then-put is a race.
class Lru<K, V> extends LinkedHashMap<K, V> {
  private final int max;
  Lru(int max) { super(16, 0.75f, true); this.max = max; }   // accessOrder = true
  @Override protected boolean removeEldestEntry(Map.Entry<K, V> e) { return size() > max; }
}
java

Generics & PECS

static <T> void copy(List<? super T> dst, List<? extends T> src) {
  for (T t : src) dst.add(t);   // read from extends, write into super
}
java
  • PECS: Producer extends, Consumer super. You can read Number from List<? extends Number> but add nothing except null.
  • Generics are invariant: List<Integer> is not a List<Number>. Arrays are covariant, so Object[] o = new String[1]; o[0] = 1; throws ArrayStoreException.
  • Type erasure removes type arguments at runtime: no new T(), no new T[n], no obj instanceof List<String>, no List<int>, and m(List<String>) can’t overload m(List<Integer>).

Lambdas & functional interfaces

Interface Method Example
Supplier<T> T get() ArrayList::new
Consumer<T> void accept(T) System.out::println
Function<T,R> R apply(T) String::length
BiFunction<T,U,R> R apply(T, U) map.merge(k, 1, Integer::sum)
UnaryOperator<T> T apply(T) String::trim
BinaryOperator<T> T apply(T, T) Integer::max
Predicate<T> boolean test(T) String::isBlank
Runnable void run() () -> log.info("tick")
Callable<V> V call() throws Exception () -> fetch(url)
  • A functional interface has exactly one abstract method; @FunctionalInterface only enforces it.
  • Method references: static Integer::parseInt, bound System.out::println, unbound String::toUpperCase, constructor ArrayList::new.
  • Lambdas capture only effectively final locals, and this is the enclosing instance (unlike anonymous classes).
  • Compose with andThen, compose, and, negate, Predicate.not(...) (11). IntPredicate, ToIntFunction and friends avoid boxing.

Streams & Optional

Map<Dept, List<String>> names = staff.stream()
    .filter(e -> e.salary() > 50_000)
    .collect(Collectors.groupingBy(Employee::dept,
             Collectors.mapping(Employee::name, Collectors.toList())));
java
  • Intermediate ops are lazy: filter, map, flatMap, distinct, sorted, limit, skip, takeWhile (9), peek (debug only).
  • Terminal ops run the pipeline: collect, toList() (16, unmodifiable), reduce, count, anyMatch, findFirst. A stream is single-use.
Need Collector
Map toMap(k, v) throws IllegalStateException on duplicate keys; add a merge (a, b) -> a
Group groupingBy(f), groupingBy(f, TreeMap::new, counting())
Split in two partitioningBy(pred) gives Map<Boolean, List<T>>
Aggregate counting(), summingInt, averagingDouble, joining(", "), maxBy
Downstream mapping, filtering (9), flatMapping (9), collectingAndThen
Two at once teeing(c1, c2, merger) (12)
  • Keep lambdas stateless. Parallel streams share ForkJoinPool.commonPool(): large CPU-bound work only, never blocking I/O.
  • Optional is for return types, not fields or parameters. Optional.of(null) throws; use ofNullable.
  • orElse(x) always evaluates x; orElseGet(() -> x) is lazy. Prefer orElseThrow() (10) to bare get(). Also map, flatMap, filter, ifPresentOrElse (9).

Concurrency basics

  • Thread states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED. start() runs on a new thread; run() runs on the caller.
  • synchronized gives mutual exclusion and visibility and is reentrant; static synchronized locks the Class object.
  • wait()/notifyAll() need the monitor and a while loop (spurious wakeups). wait releases the lock; sleep keeps it.
  • Happens-before: unlock before the next lock of that monitor, volatile write before read, start() before the thread body, the body before join() returns.
  • CountDownLatch waits for N events (one-shot), CyclicBarrier makes N threads meet (reusable), Semaphore allows N at once.
  • Deadlock needs mutual exclusion, hold-and-wait, no preemption and circular wait. Prevent it with a global lock order or tryLock timeouts.
  • On InterruptedException, rethrow or restore the flag: Thread.currentThread().interrupt().
Visibility Atomic count++ Blocks
volatile yes no no
synchronized / ReentrantLock yes yes yes
AtomicInteger (CAS) yes yes, one variable no
if (lock.tryLock(1, TimeUnit.SECONDS)) {   // ReentrantLock: timed, interruptible, fair
  try { balance -= amount; }
  finally { lock.unlock(); }                // always unlock in finally
}
java
  • ReentrantReadWriteLock suits read-heavy data; LongAdder beats AtomicLong under contention. Call ThreadLocal.remove() in pooled threads or it leaks.

Executors & virtual threads

Factory Behavior Watch out
newFixedThreadPool(n) n threads, unbounded queue queue can grow until OOM
newCachedThreadPool() threads on demand, idle 60 s unbounded thread count
newScheduledThreadPool(n) delayed and periodic tasks an exception cancels the periodic task
newVirtualThreadPerTaskExecutor() (21) one virtual thread per task never pool virtual threads
  • ThreadPoolExecutor fills core threads, then the queue, then grows to max, then rejects: AbortPolicy (default) throws; CallerRunsPolicy gives backpressure.
  • submit() keeps exceptions in the Future (lost if nobody calls get()); execute() hands them to the uncaught handler. shutdown() drains, shutdownNow() interrupts.
CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> api.user(id), pool);
CompletableFuture<List<Order>> orders = CompletableFuture.supplyAsync(() -> api.orders(id), pool);

Profile p = user.thenCombine(orders, Profile::new)   // wait for both
    .orTimeout(2, TimeUnit.SECONDS)                  // Java 9
    .exceptionally(ex -> Profile.empty())            // fallback
    .join();                                         // unchecked CompletionException
java
  • thenApply maps, thenCompose flat-maps, thenCombine zips, allOf/anyOf fan in; handle sees result and error. get() throws checked ExecutionException.
  • Async stages default to the common pool: pass your own executor for blocking calls.
  • Virtual threads (21): Thread.ofVirtual().start(task). The JVM unmounts them from their carrier thread while they block, so millions are cheap. They help I/O-bound code, not CPU-bound work; cap concurrency with a Semaphore.
  • Pinning: in 21 to 23, blocking inside synchronized pins the carrier (use ReentrantLock); Java 24 fixed it (JEP 491). Scoped values (final in 25) are the lighter alternative to ThreadLocal for passing context.

JVM memory & GC

Area Holds Scope When full
Heap: young (Eden + 2 survivors), old objects, arrays, string pool shared OutOfMemoryError: Java heap space
Metaspace (native; replaced PermGen in 8) class metadata shared OutOfMemoryError: Metaspace
JVM stack frames: locals, operand stack per thread StackOverflowError
Code cache JIT-compiled code shared JIT compilation stops
  • New objects go to Eden; minor GCs copy survivors and promote long-lived objects to old gen.
  • GC traces reachability from roots (stacks, static fields, JNI refs), so cycles are collected. Soft refs are cleared before OOM, weak refs at the next GC.
  • Class loaders delegate parent-first: Bootstrap, Platform, Application.
Collector Flag Notes
Serial -XX:+UseSerialGC single thread; tiny heaps, 1-CPU containers
Parallel -XX:+UseParallelGC throughput; default in Java 8
G1 -XX:+UseG1GC default since 9; regions, -XX:MaxGCPauseMillis goal (200 ms)
ZGC -XX:+UseZGC sub-ms pauses; generational since 21, only mode since 24
Shenandoah -XX:+UseShenandoahGC low pause; not in Oracle JDK builds
Epsilon -XX:+UseEpsilonGC no-op, experimental
  • Sizing: -Xms/-Xmx heap, -Xss stack, -XX:MaxRAMPercentage=75 in containers (the default max heap is usually 25% of RAM).
  • Debugging: jcmd <pid> Thread.print for hangs and deadlocks, jcmd <pid> GC.heap_dump plus Eclipse MAT for leaks, -Xlog:gc* for pauses, JFR for profiling, -XX:+HeapDumpOnOutOfMemoryError in production.

Version timeline

Version Year Headline features
8 LTS 2014 lambdas, streams, Optional, default methods, java.time, CompletableFuture
9 to 10 2017 to 2018 modules, JShell, List.of, G1 default; var (10)
11 LTS 2018 HttpClient, String.isBlank/strip/repeat, Files.readString, java App.java
12 to 16 2019 to 2021 switch expressions (14), text blocks (15), records and instanceof patterns (16)
17 LTS 2021 sealed classes, strong encapsulation of JDK internals
21 LTS 2023 virtual threads, record patterns, switch patterns, sequenced collections
22 to 24 2024 to 2025 unnamed variables _ (22), FFM API (22), stream gatherers (24)
25 LTS 2025 scoped values, import module, void main() source files, statements before super(), compact object headers (opt-in)

Note

Structured concurrency is still a preview in Java 25 (--enable-preview).

Design patterns in one line

Pattern One line Seen in
Singleton one instance; enum or holder class (volatile for double-checked locking) Spring beans
Factory method a method picks the class to create List.of
Builder step-by-step construction of immutable objects HttpRequest.newBuilder()
Adapter make one interface fit another InputStreamReader
Decorator wrap to add behavior, same interface BufferedInputStream
Proxy stand-in that controls access Spring AOP, lazy loading
Strategy swap algorithms at runtime Comparator
Observer notify subscribers of changes event listeners
Template method fixed skeleton, subclasses fill steps AbstractList
Flyweight share immutable instances Integer.valueOf cache

Quick answers

  • JDK vs JRE vs JVM? The JVM runs bytecode, the JRE adds the libraries, the JDK adds javac and tools.
  • Pass by value or reference? Always by value; for objects, the value is a copy of the reference.
  • == vs equals? Reference (or primitive) comparison vs content comparison.
  • Why is String immutable? Safe pooling, cached hash for map keys, thread safety, and security.
  • final vs finally vs finalize? Can’t change or override; always-run block; deprecated GC hook.
  • Checked vs unchecked? Checked must be caught or declared; RuntimeException and Error need not be.
  • HashMap vs ConcurrentHashMap vs Hashtable? Not thread-safe, allows nulls; thread-safe with fine-grained locking; legacy with one lock.
  • volatile vs synchronized? Visibility only vs visibility plus atomicity.
  • Fail-fast vs fail-safe iterators? Throw ConcurrentModificationException vs iterate a snapshot or weakly consistent view.
  • Memory leak in Java? Reachable but unused objects: static maps, listeners, ThreadLocal in pools, unclosed resources.

Gotchas & traps

  • Integer a = 127, b = 127; a == b is true, but false for 128: Integer.valueOf caches -128 to 127. Compare boxes with equals.
  • Unboxing null throws NPE, including in a ternary that mixes Integer and int.
  • On a List<Integer>, list.remove(1) removes index 1; list.remove(Integer.valueOf(1)) removes the value.
  • List.of(...).add(x) and Arrays.asList(...).add(x) throw UnsupportedOperationException.
  • new BigDecimal(0.1) is inexact: use BigDecimal.valueOf(0.1). equals checks scale (2.0 vs 2.00); use compareTo.
  • "a.b".split(".") returns an empty array, since split takes a regex; use "\\.".
  • int overflow is silent: Math.abs(Integer.MIN_VALUE) is negative. Use Math.addExact or long.
  • SimpleDateFormat is not thread-safe; DateTimeFormatter is.
  • A non-static inner class holds a reference to its outer instance: a classic leak.
esc