OOP and SOLID, records and sealed types, collections internals, generics, streams, concurrency and virtual threads, JVM memory and GC, and Java 8 to 25.
AMCompiled by Aditya Mishra · Technical Lead, BNP Paribas
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 literala == c; // false: new heap objecta == c.intern(); // truea.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.
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.
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().
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.