← Tutorials 🤖 AI 📓 Architect's Notebook ☁ AWS 📋 Cheatsheet 🗃 Databases 🚀 DevOps 🧩 DSA ❓ FAQ 🖥 Frontend ☕ Java 🍏 MongoDB 🐍 Python 🌿 Spring Boot 🏗 System Design 🏛 TOGAF
Tutorials › Java
★ marked topics are high-priority interview topics explained in depth with diagrams. Other topics give concise, practical summaries.

Java Fundamentals

☕ Java · Fundamentals · 6 min read

Java is a class-based, object-oriented, platform-independent language. Its hallmark — "write once, run anywhere" (WORA) — comes from compiling source to platform-neutral bytecode that runs on any JVM.

Compile & Run Flow

// Main.java public class Main { public static void main(String[] args) { System.out.println("Hello, Java!"); } }
javac Main.java # source .java → bytecode .class java Main # JVM loads & runs the bytecode

Key Characteristics

  • Platform independent — bytecode runs on any OS with a JVM.
  • Object-oriented — everything lives in classes.
  • Statically typed — types checked at compile time.
  • Automatic memory management — garbage collection.
  • Multithreaded & secure — built-in concurrency & sandboxing.
💡 The magic of WORA: javac produces bytecode (not machine code); the JVM JIT-compiles that bytecode to native code at runtime for the host platform.

Data Types

☕ Java · Fundamentals · 5 min read

Java has two categories: primitives (store values directly) and reference types (store references to objects).

The 8 Primitives

TypeSizeExample
byte8-bit-128 to 127
short16-bit±32k
int32-bitdefault integer
long64-bit100L
float32-bit3.14f
double64-bitdefault decimal
char16-bit'A' (Unicode)
boolean1-bittrue/false

Reference types: classes, interfaces, arrays, enums — they default to null.

💡 Primitives live on the stack (or inside objects); their wrapper classes (Integer, Double…) are objects on the heap. Default values: numeric → 0, boolean → false, reference → null.

Variables

☕ Java · Fundamentals · 5 min read

A variable is a named storage location with a type. Java has three kinds based on scope and lifetime.

TypeDeclaredLifetime
LocalInside a method/blockWhile the block runs
InstanceIn a class, non-staticPer object
Static (class)In a class with staticOne per class
public class Counter { static int total; // static — shared by all instances int count; // instance — per object void inc() { int step = 1; // local — only inside inc() count += step; total += step; } }
💡 Use var (Java 10+) for local type inference: var list = new ArrayList<String>(); — still statically typed, just inferred.

Operators

☕ Java · Fundamentals · 4 min read
CategoryOperators
Arithmetic+ - * / %
Relational== != > < >= <=
Logical&& || !
Bitwise& | ^ ~ << >> >>>
Assignment= += -= *= /=
Unary / Ternary++ -- · cond ? a : b
💡 && and || short-circuit (stop evaluating once the result is known); & and | always evaluate both sides. Use short-circuit for null checks: if (obj != null && obj.isValid()).

Control Statements

☕ Java · Fundamentals · 5 min read

Control flow directs program execution: decisions, loops, and branching.

// decisions if (x > 0) { ... } else if (x == 0) { ... } else { ... } // switch expression (Java 14+) String day = switch (n) { case 1, 7 -> "weekend"; default -> "weekday"; }; // loops for (int i = 0; i < 5; i++) { ... } for (String s : list) { ... } // enhanced for while (cond) { ... } do { ... } while (cond);
💡 Modern switch expressions (->, returns a value, no fall-through) are safer than classic switch with break. continue skips an iteration; break exits the loop.

Arrays

☕ Java · Fundamentals · 5 min read

An array is a fixed-size, indexed container of same-type elements stored contiguously. Length is fixed at creation.

int[] nums = {1, 2, 3, 4}; int[] empty = new int[5]; // [0,0,0,0,0] String[][] grid = new String[3][3];// 2D nums[0] = 10; int len = nums.length; // field, not method Arrays.sort(nums); int idx = Arrays.binarySearch(nums, 10); System.out.println(Arrays.toString(nums));
💡 Arrays are fixed-size and zero-indexed; accessing an invalid index throws ArrayIndexOutOfBoundsException. Need dynamic sizing? Use ArrayList (see Collections).

JVM Architecture

☕ Java · JVM & Memory · 7 min read

The JVM (Java Virtual Machine) is the runtime that loads, verifies, and executes bytecode. It has three subsystems: Class Loader, Runtime Data Areas (memory), and the Execution Engine.

1. Class LoaderLoadingLinking (verify/prepare)Initialization 2. Runtime DataHeap (objects)Stacks (per thread)MetaspacePC Registers 3. Execution EngineInterpreterJIT CompilerGarbage Collector
  • Class Loader — loads .class files, verifies bytecode, links & initializes.
  • Runtime Data Areas — Heap, per-thread Stacks, Metaspace, PC registers.
  • Execution Engine — Interpreter + JIT (hot code → native) + GC.
💡 The JIT compiler is why Java is fast: frequently-run ("hot") bytecode is compiled to native machine code and cached, approaching C-like speed after warm-up.

JDK vs JRE vs JVM

☕ Java · JVM & Memory · 4 min read

Three nested layers — a classic interview question.

JDK — Java Development Kit (build + run) JRE — Java Runtime Environment (run only) JVM — executes bytecode+ core libraries
ContainsUse
JVMBytecode executorRuns the program
JREJVM + librariesRun Java apps
JDKJRE + compiler/tools (javac, jar, jdb)Develop Java apps
💡 One line: JDK = JRE + dev tools; JRE = JVM + libraries; JVM runs the bytecode. You install the JDK to develop, only the JRE to run.

JVM Memory Model

☕ Java · JVM & Memory · ★ Interview Topic · 10 min read

The JVM divides memory into well-defined runtime data areas. Some are shared across all threads (Heap, Metaspace); others are per-thread (Stack, PC register). Knowing what lives where is essential for debugging memory issues.

Runtime Data Areas — Visual

Shared (all threads) Heap — all objects & arraysYoung (Eden + Survivors) + Old Genmanaged by GC Metaspaceclass metadata (off-heap, since Java 8) Per-thread JVM Stackframes: locals, refs PC Register Native Method Stack

The Areas

AreaHoldsScope
HeapObjects, arrays, instance fieldsShared
MetaspaceClass metadata, static fieldsShared (off-heap)
StackMethod frames: locals, partial results, refsPer thread
PC RegisterAddress of current instructionPer thread
Native StackNative (JNI) callsPer thread

Happens-Before (the JMM)

The Java Memory Model also defines visibility rules for concurrency — when a write by one thread becomes visible to another (the "happens-before" relationship), established by synchronized, volatile, and thread start/join.

💡 Two errors to know: OutOfMemoryError: Java heap space (heap full — too many live objects) vs StackOverflowError (stack full — usually infinite recursion). Different areas, different causes.

Heap vs Stack

☕ Java · JVM & Memory · ★ Interview Topic · 8 min read

The two memory regions developers reason about most. The Stack holds method frames and local variables (fast, thread-private, auto-managed). The Heap holds all objects (shared, GC-managed).

Where Things Live — Visual

Stack (per thread) main() frame: int x = 5 User u → (ref) ─────┐ primitives + references LIFO · auto freed on return Heap (shared) User object{ name:"Aftab", age:30 } all objects & arrays GC-managed

Comparison

StackHeap
StoresPrimitives, references, method framesObjects, arrays
ScopePer threadShared by all threads
LifetimeUntil method returnsUntil GC reclaims
SpeedVery fast (LIFO)Slower (GC overhead)
ErrorStackOverflowErrorOutOfMemoryError
void demo() { int x = 5; // x on the stack User u = new User("Aftab"); // u (ref) on stack, object on heap } // frame popped; object eligible for GC if no other refs
💡 Interview line: "Primitives and object references live on the stack; the objects themselves live on the heap. The stack is thread-private and auto-managed; the heap is shared and GC-managed."

Garbage Collection

☕ Java · JVM & Memory · ★ Interview Topic · 10 min read

Garbage Collection automatically reclaims heap memory occupied by objects no longer reachable from GC roots — freeing developers from manual free() and preventing most memory leaks.

Generational Heap — Visual

Young Generation (Minor GC, frequent) Edennew objects S0 S1 Old Generationlong-lived objectsMajor GC (rare, costly) promote

How It Works

  • Mark — find all objects reachable from GC roots (stack refs, statics).
  • Sweep/Compact — reclaim unreachable objects; compact to avoid fragmentation.
  • Generational hypothesis — most objects die young, so the Young Gen is collected often (cheap Minor GC); survivors are promoted to Old Gen (rare Major GC).
// you can suggest (not force) GC — rarely needed System.gc(); // a hint to the JVM; don't rely on it
💡 GC reclaims objects that are unreachable, not merely unused. A "leak" in Java = objects still referenced (e.g. in a static collection) but never used again — see Memory Leaks. Collectors: see GC Algorithms.

Garbage Collection Algorithms

☕ Java · JVM & Memory · 6 min read

The JVM offers several collectors, each trading throughput vs latency. You pick one based on your app's needs.

CollectorBest forFlag
SerialSmall heaps, single CPU-XX:+UseSerialGC
Parallel (Throughput)Batch jobs, max throughput-XX:+UseParallelGC
G1 (default since 9)Balanced, large heaps-XX:+UseG1GC
ZGCUltra-low pause (<1ms), huge heaps-XX:+UseZGC
ShenandoahLow pause, concurrent compaction-XX:+UseShenandoahGC
💡 G1 is the modern default — region-based, predictable pauses. For latency-critical services with big heaps, ZGC/Shenandoah give sub-millisecond pauses. For throughput batch work, Parallel wins.

Class Loading Mechanism

☕ Java · JVM & Memory · ★ Interview Topic · 9 min read

Class loading is how the JVM brings .class bytecode into memory, on demand, in three phases — and it follows the parent delegation model for security and consistency.

The Loader Hierarchy — Visual

Bootstrap ClassLoadercore JDK (java.*) Platform ClassLoaderJDK modules Application ClassLoaderyour classpath delegate up first ↑load only ifparent can't

Three Phases

  1. Loading — find & read the .class, create a Class object.
  2. LinkingVerify (bytecode safety) → Prepare (default static values) → Resolve (symbolic refs).
  3. Initialization — run static initializers & assign static fields.

Parent Delegation

A loader first asks its parent to load a class; it only loads the class itself if no ancestor can. This prevents user code from overriding core classes (you can't replace java.lang.String).

💡 Custom ClassLoaders power plugins, hot-reload, and app servers (each web app gets its own loader for isolation). ClassNotFoundException (loading) vs NoClassDefFoundError (was present at compile, missing at runtime) are common interview traps.

JVM Tuning

☕ Java · JVM & Memory · ★ Interview Topic · 8 min read

Tuning the JVM means choosing heap sizes, the GC, and flags to balance throughput, latency, and memory footprint for your workload.

Key Flags

FlagPurpose
-Xms / -XmxInitial / max heap size (set equal in prod to avoid resizing)
-XX:+UseG1GCSelect the collector
-XX:MaxGCPauseMillisTarget pause time (G1)
-XX:MaxRAMPercentageHeap as % of container memory
-XX:+HeapDumpOnOutOfMemoryErrorDump heap on OOM for analysis
-XssThread stack size
java -Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \ -XX:MaxRAMPercentage=75 -XX:+HeapDumpOnOutOfMemoryError \ -jar app.jar

Diagnostic Tools

jstat (GC stats), jmap (heap dump), jstack (thread dump), JFR (Java Flight Recorder), and visualizers like VisualVM / Eclipse MAT.

⚠️ In containers, always set -XX:MaxRAMPercentage (or -Xmx) — modern JVMs are container-aware, but without limits the heap can be sized to host RAM and get OOM-killed (exit 137).

Memory Leaks in Java

☕ Java · JVM & Memory · 6 min read

Even with GC, Java apps leak memory when objects remain reachable but unused — GC can't collect them, so the heap grows until OutOfMemoryError.

Common Causes

  • Static collections that grow forever (a static Map cache with no eviction).
  • Unclosed resources — streams, connections (use try-with-resources).
  • Listeners/callbacks never deregistered.
  • ThreadLocals not removed (esp. in thread pools).
  • Inner classes holding an implicit outer reference.
// classic leak: unbounded static cache static final Map<Key,Val> CACHE = new HashMap<>(); // never evicts → leak // fix: bounded cache (Caffeine / LinkedHashMap LRU) or WeakHashMap
💡 Diagnose with a heap dump (jmap) analyzed in Eclipse MAT — look for the "dominator tree" and unexpectedly large retained sets. The fix is almost always removing a lingering reference.

Performance Optimization

☕ Java · JVM & Memory · ★ Interview Topic · 9 min read

Performance work in Java spans algorithmic choices, memory/GC behaviour, concurrency, and I/O. The cardinal rule: measure first — profile before optimizing.

Optimization Checklist

Algorithms & Data Structures

Right collection for the access pattern (HashMap vs TreeMap), correct Big-O. Biggest wins live here.

Object Allocation

Avoid needless objects in hot loops; reuse buffers; prefer primitives over wrappers to cut GC pressure.

Strings

Use StringBuilder for concatenation in loops; avoid + in tight loops.

I/O

Buffer streams; batch DB calls; use connection pools; async/NIO for high concurrency.

Concurrency

Parallelize CPU-bound work; size thread pools to cores; avoid lock contention.

JVM/GC

Right collector & heap size; reduce allocation rate; cache hot results.

Measure, Don't Guess

  • Profilers — JFR, async-profiler, VisualVM (find the real hotspots).
  • Benchmarks — JMH for micro-benchmarks (avoid JIT/warm-up pitfalls).
  • Metrics — Micrometer + Prometheus for production latency/throughput.
⚠️ "Premature optimization is the root of all evil." Profile to find the actual bottleneck — it's usually one method or one bad query, not where you'd guess. Optimize that, re-measure, repeat.

Strings — String vs StringBuilder vs StringBuffer

☕ Java · Strings · ★ Interview Topic · 9 min read

String is immutable — every modification creates a new object. For heavy modification, use the mutable StringBuilder (fast, not thread-safe) or StringBuffer (thread-safe, synchronized).

The Three Compared

StringStringBuilderStringBuffer
MutableNo (immutable)YesYes
Thread-safeYes (immutable)NoYes (synchronized)
PerformanceSlow for editsFastestSlower (locking)
Since1.01.51.0

Why Immutability Hurts in Loops — Visual

String + in loop (each += makes a NEW object): "a" "ab" (new) "abc" (new) …O(n²), GC churn StringBuilder (one buffer, mutated in place): [ a | b | c | … ] — append, no new objects → O(n)
// BAD — O(n²), creates n strings String s = ""; for (int i = 0; i < n; i++) s += i; // GOOD — O(n), one buffer StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) sb.append(i); String result = sb.toString();
💡 Decision: String for fixed text · StringBuilder for building/modifying (single-threaded) · StringBuffer only when multiple threads mutate the same buffer (rare). Note: the compiler already optimizes simple "a" + "b" to StringBuilder — the problem is concatenation in loops.

String Pool

☕ Java · Strings · 5 min read

The String Pool (string interning) is a special area of the heap where the JVM stores one copy of each distinct string literal — so identical literals share the same object, saving memory.

String a = "hello"; // in the pool String b = "hello"; // reuses the SAME pooled object String c = new String("hello"); // forces a NEW heap object a == b; // true — same pooled reference a == c; // false — different objects a.equals(c); // true — same content a == c.intern(); // true — intern() returns the pooled copy
💡 This is why you compare strings with .equals(), never ==. Literals are auto-pooled; new String() bypasses the pool; intern() puts a string into (or fetches it from) the pool.

StringBuilder

☕ Java · Strings · 4 min read

A mutable sequence of characters — the go-to for building or modifying strings efficiently in single-threaded code. Not synchronized, so it's fast.

StringBuilder sb = new StringBuilder(); sb.append("Hello").append(", ").append("World"); sb.insert(0, ">> "); sb.reverse(); sb.deleteCharAt(0); String result = sb.toString();
💡 Pre-size the buffer (new StringBuilder(256)) when you know the rough output length — avoids internal array resizing. See Strings for the full comparison.

StringBuffer

☕ Java · Strings · 4 min read

The thread-safe sibling of StringBuilder — same API, but every method is synchronized. Use it only when multiple threads append to the same buffer.

StringBuffer sb = new StringBuffer(); sb.append("thread-safe"); // synchronized — safe across threads
💡 In practice StringBuffer is rarely the right tool: sharing a mutable buffer across threads is uncommon, and the locking costs performance. Prefer StringBuilder; reach for StringBuffer only for that specific shared-mutation case.

OOP Principles

☕ Java · OOP · ★ Interview Topic · 11 min read

Object-Oriented Programming organizes code around objects (data + behaviour) rather than functions. Java is built on four pillars: Encapsulation, Inheritance, Polymorphism, Abstraction.

The Four Pillars — Visual

Encapsulationbundle data + hidevia private + getters Inheritancereuse via "is-a"extends Polymorphismone interface,many forms Abstractionhide complexity,expose essentials

Each Pillar in One Example

// Encapsulation — private state, controlled access class Account { private double balance; // hidden public void deposit(double a) { if (a>0) balance += a; } public double getBalance() { return balance; } } // Abstraction + Inheritance + Polymorphism abstract class Shape { abstract double area(); } // abstraction class Circle extends Shape { // inheritance (is-a) double r; double area() { return Math.PI*r*r; } // override } Shape s = new Circle(); // polymorphism s.area(); // runs Circle.area() at runtime
💡 Memory aid "A PIE": Abstraction, Polymorphism, Inheritance, Encapsulation. Encapsulation = data hiding; Abstraction = hiding implementation; Inheritance = reuse; Polymorphism = one type, many behaviours.

Encapsulation

☕ Java · OOP · 5 min read

Encapsulation bundles data (fields) and the methods operating on them into one class, and hides the internal state behind private fields exposed via controlled getters/setters.

public class Employee { private double salary; // hidden public double getSalary() { return salary; } public void setSalary(double s) { if (s < 0) throw new IllegalArgumentException(); // validation gate this.salary = s; } }
💡 Benefits: validation in setters, freedom to change internals without breaking callers, and read-only/write-only fields. "Program to controlled access, not to fields."

Inheritance

☕ Java · OOP · 5 min read

Inheritance lets a subclass acquire fields and methods of a superclass via extends — modeling an "is-a" relationship and enabling code reuse.

class Animal { void eat() { ... } } class Dog extends Animal { // Dog is-a Animal void bark() { ... } } Dog d = new Dog(); d.eat(); // inherited d.bark(); // own
  • Java supports single class inheritance (one superclass) but multiple interface inheritance.
  • Use super to call the parent's constructor/methods.
  • Prefer composition over inheritance when there's no true "is-a".
💡 No multiple class inheritance (avoids the "diamond problem"); achieve it via interfaces with default methods instead.

Polymorphism

☕ Java · OOP · 5 min read

"Many forms" — the same interface/reference behaves differently based on the actual object. Two kinds:

TypeMechanismResolved
Compile-time (static)Method overloadingAt compile time
Runtime (dynamic)Method overridingAt runtime (virtual dispatch)
List<Shape> shapes = List.of(new Circle(), new Square()); for (Shape s : shapes) s.area(); // each runs its own area()
💡 Runtime polymorphism (overriding) is the powerful one — it's how you write code against a base type/interface and let the JVM dispatch to the right implementation dynamically.

Abstraction

☕ Java · OOP · 5 min read

Abstraction exposes what an object does while hiding how. Achieved with abstract classes and interfaces.

abstract classinterface
Methodsabstract + concreteabstract + default/static (8+)
Fieldsanypublic static final only
Inheritancesingle (extends)multiple (implements)
ConstructorYesNo
interface Payment { void pay(double amt); } // contract only class UpiPayment implements Payment { public void pay(double amt) { ... } // the "how" }
💡 Rule of thumb: use an interface for a capability/contract (can-do), an abstract class when you share common state/code among related classes (is-a with defaults).

Classes and Objects

☕ Java · OOP · 4 min read

A class is a blueprint; an object is an instance of it with its own state. Classes define fields (state) and methods (behaviour).

class Car { String model; // field Car(String m) { this.model = m; } // constructor void drive() { ... } // method } Car c = new Car("Tesla"); // object on the heap; c (ref) on the stack
💡 new allocates the object on the heap and returns a reference. Records (Java 16+) are concise immutable data classes: record Point(int x, int y) {}.

Constructors

☕ Java · OOP · 4 min read

A constructor initializes a new object. It has the class name, no return type, and runs when you call new.

class User { String name; int age; User() { this("Guest", 0); } // no-arg, calls another via this() User(String name, int age) { // parameterized this.name = name; this.age = age; } }
  • Default constructor — auto-added only if you declare none.
  • Overloading — multiple constructors with different params.
  • this(...) chains to another constructor; super(...) calls the parent's (must be first line).
💡 Once you declare any constructor, Java stops providing the implicit no-arg one — add it explicitly if you still need it.

Method Overloading

☕ Java · OOP · 4 min read

Overloading = multiple methods with the same name but different parameter lists (count/type/order) in the same class. Resolved at compile time (static polymorphism).

int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } int add(int a, int b, int c) { return a + b + c; }
⚠️ Return type alone does NOT overload — the parameter list must differ. Two methods differing only by return type won't compile.

Method Overriding

☕ Java · OOP · 5 min read

Overriding = a subclass provides its own implementation of a superclass method with the same signature. Resolved at runtime (dynamic dispatch) — the basis of runtime polymorphism.

class Animal { String sound() { return "..."; } } class Cat extends Animal { @Override String sound() { return "Meow"; } // @Override = compiler check } Animal a = new Cat(); a.sound(); // "Meow" — actual type decides

Overloading vs Overriding

OverloadingOverriding
WhereSame classSub vs super class
SignatureDifferent paramsSame signature
BoundCompile timeRuntime
💡 Rules for overriding: same signature, return type covariant, access not more restrictive, can't throw broader checked exceptions. Always use @Override — the compiler catches typos.

Static Keyword

☕ Java · Keywords · 4 min read

static binds a member to the class itself, not to instances — one copy shared by all objects, accessible without creating an object.

class Counter { static int count = 0; // shared across all instances static void reset() { count=0; } // static method static { System.out.println("class loaded"); } // static block (runs once) } Counter.count; // access via class, no instance
💡 Static methods can't use this or access instance members directly (no object context). main is static so the JVM can call it without instantiating your class. Use for utilities (Math.max) and constants.

Final Keyword

☕ Java · Keywords · 4 min read

final means "cannot change" — its effect depends on what it modifies.

Applied toEffect
VariableConstant — assign once
MethodCannot be overridden
ClassCannot be subclassed (e.g. String)
final int MAX = 100; // constant final List<String> list = new ArrayList<>(); list.add("ok"); // allowed — the reference is final, not the object // list = new ArrayList<>(); // compile error
⚠️ final on a reference fixes the reference, not the object's contents — a final List can still be mutated. For true immutability, the object itself must be immutable.

This Keyword

☕ Java · Keywords · 3 min read

this refers to the current object. Used to disambiguate fields from parameters, chain constructors, and pass the current instance.

class Point { int x, y; Point(int x, int y) { this.x = x; // field = parameter this.y = y; } Point() { this(0, 0); } // constructor chaining Point self() { return this; } }
💡 Most common use: resolving the field-vs-parameter name clash in constructors/setters. Not available in static context.

Super Keyword

☕ Java · Keywords · 3 min read

super refers to the parent class — call its constructor, methods, or access its fields.

class Animal { Animal(String n){...} void eat(){...} } class Dog extends Animal { Dog(String n) { super(n); // parent constructor — must be first line } @Override void eat() { super.eat(); // extend, don't replace, parent behaviour bark(); } }
💡 If you don't call super(...) explicitly, the compiler inserts a call to the parent's no-arg constructor — which fails to compile if the parent has none.

Access Modifiers

☕ Java · Keywords · 4 min read

Access modifiers control the visibility of classes, methods, and fields — the enforcement mechanism behind encapsulation.

ModifierSame classSame packageSubclassEverywhere
private
(default)
protected
public
💡 Default (no keyword) = "package-private". Best practice: keep fields private and expose behaviour through methods — least privilege for code.

Packages

☕ Java · Keywords · 3 min read

A package is a namespace that groups related classes — preventing name clashes and organizing code. It maps to a directory structure.

package com.sevendigital.platform.service; // first line of the file import java.util.List; // use another package's class import static java.lang.Math.PI; // static import
💡 Convention: reverse-domain naming (com.company.module). Packages also interact with access control — default visibility is package-scoped.

Wrapper Classes

☕ Java · Objects & Types · 4 min read

Each primitive has an object wrapper class (Integer, Double, Boolean…) so primitives can be used where objects are required — e.g. in collections and generics.

Integer i = Integer.valueOf(42); int x = i.intValue(); int parsed = Integer.parseInt("123"); List<Integer> nums = new ArrayList<>(); // can't use int here
💡 The JVM caches small Integer values (-128..127), so Integer.valueOf(100) == Integer.valueOf(100) is true but 200 == 200 (as Integers) is false. Always compare wrappers with .equals().

Autoboxing & Unboxing

☕ Java · Objects & Types · 4 min read

Automatic conversion between primitives and their wrappers: autoboxing (primitive → wrapper) and unboxing (wrapper → primitive), inserted by the compiler.

Integer boxed = 5; // autobox: int → Integer int unboxed = boxed; // unbox: Integer → int List<Integer> list = new ArrayList<>(); list.add(10); // autoboxed int v = list.get(0); // unboxed
⚠️ Watch for NPE: unboxing a null wrapper throws NullPointerException. And autoboxing in tight loops creates many objects — a hidden performance cost. Prefer primitives in hot paths.

Object Class Methods

☕ Java · Objects & Types · 4 min read

Every class implicitly extends java.lang.Object, inheriting these methods (often overridden):

MethodPurpose
equals(Object)Logical equality (override with hashCode)
hashCode()Hash bucket value
toString()String representation
getClass()Runtime class
clone()Field-copy (needs Cloneable)
wait/notify/notifyAllThread coordination
💡 Override toString() for readable logs and always override equals() and hashCode() together (next topic).

equals() & hashCode() — and ==

☕ Java · Objects & Types · ★ Interview Topic · 9 min read

Three closely-related concepts that trip up many candidates: == (reference identity), equals() (logical equality), and hashCode() (hash bucketing) — plus the contract binding the last two.

== vs equals()

==.equals()
ComparesReferences (same object?)Logical equality (same value?)
For objectsIdentityContent (if overridden)
For primitivesValueN/A
String a = new String("hi"), b = new String("hi"); a == b; // false — different objects a.equals(b); // true — same content

The hashCode() Contract

  • If a.equals(b) is true → a.hashCode() == b.hashCode() must be true.
  • Equal hashCodes do not require equal objects (collisions are allowed).
  • hashCode must be consistent across calls (while the object's equals-fields don't change).
equals() true ⇒ hashCode() equal (MUST) hashCode equal ⇏ equals (collision OK)
@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User u)) return false; return age == u.age && Objects.equals(name, u.name); } @Override public int hashCode() { return Objects.hash(name, age); // SAME fields as equals() }
⚠️ Break the contract and hash-based collections silently fail: a key you put can't be get-found because it lands in a different bucket. Always override both together, using the same fields.

Immutable Classes

☕ Java · Objects & Types · ★ Interview Topic · 8 min read

An immutable object's state cannot change after construction (like String, Integer, LocalDate). Immutability brings thread-safety, safe sharing/caching, and reliable use as map keys — for free.

The Recipe

  1. Make the class final (can't be subclassed to add mutability).
  2. Make all fields private final.
  3. No setters; set everything in the constructor.
  4. Defensively copy mutable fields in (constructor) and out (getters).
public final class Money { private final long amount; private final List<String> tags; public Money(long amount, List<String> tags) { this.amount = amount; this.tags = List.copyOf(tags); // defensive copy IN } public long getAmount() { return amount; } public List<String> getTags() { return tags; } // already unmodifiable }

Java 16+ records give shallow immutability concisely: record Money(long amount) {}.

💡 Why interviewers love this: immutable objects are inherently thread-safe (no synchronization needed) and safe as HashMap keys (hashCode never changes). The subtle part is defensive copying of mutable members — forgetting it leaks mutability.

Cloneable Interface

☕ Java · Objects & Types · 4 min read

Cloneable is a marker interface enabling Object.clone() to make a field-by-field copy. It's widely considered flawed — most code prefers copy constructors or factory methods.

class Point implements Cloneable { int x, y; @Override public Point clone() throws CloneNotSupportedException { return (Point) super.clone(); // shallow copy } }
⚠️ clone() does a shallow copy by default and has awkward semantics (checked exception, no constructor call). Prefer a copy constructor new Point(other) or a static factory — clearer and safer.

Deep Copy vs Shallow Copy

☕ Java · Objects & Types · 5 min read

When copying an object with reference fields, the question is whether the nested objects are shared or duplicated.

Shallow copy — nested object SHARED: copy original shared Address Deep copy — nested object DUPLICATED: copy original Address copy Address (orig)
  • Shallow — copies references; nested objects are shared (mutating one affects both).
  • Deep — recursively copies nested objects; fully independent.
💡 Achieve deep copy via copy constructors that copy nested objects, serialization round-trips, or libraries. Immutable nested objects make the distinction moot — another win for immutability.

Exception Handling

☕ Java · Exceptions · ★ Interview Topic · 9 min read

Exceptions signal abnormal conditions, separating error handling from normal logic. Java uses try-catch-finally and a typed exception hierarchy rooted at Throwable.

The Hierarchy — Visual

Throwable ErrorOOM, StackOverflow (don't catch) Exceptionrecoverable checkedIOException, SQLException RuntimeExceptionNPE, IllegalArg (unchecked)

try-catch-finally & try-with-resources

try { risky(); } catch (IOException | SQLException e) { // multi-catch log.error("failed", e); throw new ServiceException(e); // wrap & rethrow } finally { cleanup(); // always runs } // try-with-resources — auto-closes (AutoCloseable) try (var conn = ds.getConnection()) { conn.query(...); } // conn.close() called automatically, even on exception
💡 Best practices: catch specific types (not bare Exception), never swallow exceptions silently, use try-with-resources for anything closeable, and don't use exceptions for normal control flow. finally always runs (even after return) — but avoid return inside it.

Checked Exceptions

☕ Java · Exceptions · 4 min read

Checked exceptions are verified at compile time — you must either catch them or declare them with throws. They represent recoverable conditions outside your control (file missing, network down).

void read() throws IOException { // declared Files.readString(Path.of("x.txt")); } // caller must handle or propagate

Examples: IOException, SQLException, ClassNotFoundException.

💡 Checked exceptions force callers to acknowledge failure modes — good for truly recoverable cases, but overuse leads to boilerplate. Many modern APIs (and Spring) favour unchecked exceptions.

Unchecked Exceptions

☕ Java · Exceptions · 4 min read

Unchecked exceptions extend RuntimeException and are not checked at compile time — you're not forced to handle them. They usually signal programming bugs.

String s = null; s.length(); // NullPointerException int[] a = new int[2]; a[5] = 1; // ArrayIndexOutOfBoundsException Integer.parseInt("abc"); // NumberFormatException

Examples: NullPointerException, IllegalArgumentException, IllegalStateException, ArithmeticException.

💡 Rule of thumb: checked = recoverable external problem the caller should handle; unchecked = programming error to fix in code, not catch. Validate inputs to fail fast with IllegalArgumentException.

Custom Exceptions

☕ Java · Exceptions · 4 min read

Create domain-specific exceptions by extending Exception (checked) or RuntimeException (unchecked) — for clearer, meaningful error handling.

public class InsufficientFundsException extends RuntimeException { public InsufficientFundsException(String msg) { super(msg); } public InsufficientFundsException(String msg, Throwable cause) { super(msg, cause); // preserve the original cause } } // usage if (balance < amount) throw new InsufficientFundsException("Balance too low: " + balance);
💡 Prefer extending RuntimeException for business errors (less boilerplate), always provide a cause-preserving constructor, and give the message enough context to debug. In Spring, map them to HTTP statuses with @ResponseStatus or @ExceptionHandler.

Collections Framework

☕ Java · Collections · ★ Interview Topic · 10 min read

The Collections Framework is a unified architecture of interfaces and implementations for storing and manipulating groups of objects — Lists, Sets, Queues, and Maps.

The Hierarchy — Visual

Iterable → Collection List Set Queue Map(separate root) ArrayListLinkedList HashSetTreeSet ArrayDequePriorityQueue HashMap, TreeMapLinkedHashMap

Pick the Right Collection

NeedUse
Ordered, indexed, duplicatesArrayList
Frequent insert/delete at endsLinkedList / ArrayDeque
Unique elements, fast lookupHashSet
Unique & sortedTreeSet
Key→value lookupHashMap
Sorted by keyTreeMap
Insertion-order mapLinkedHashMap
Thread-safe mapConcurrentHashMap
💡 Note: Map is not a Collection — it's a separate hierarchy (key→value), though part of the framework. Choosing the right structure for your access pattern is the highest-leverage performance decision.

List Interface

☕ Java · Collections · 4 min read

A List is an ordered collection allowing duplicates, with positional (index) access.

List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("a"); // dups allowed list.get(0); list.set(1, "x"); list.remove(0); list.contains("a"); list.indexOf("x"); List<Integer> immutable = List.of(1, 2, 3); // unmodifiable

Main implementations: ArrayList (array-backed), LinkedList (doubly-linked), Vector (legacy, synchronized).

💡 Default to ArrayList. List.of(...) returns an immutable list — calling add on it throws UnsupportedOperationException.

ArrayList

☕ Java · Collections · 5 min read

A resizable array — the most-used List. Fast random access, slower middle inserts/deletes (shifting).

OperationComplexity
get(i) / set(i)O(1)
add (end, amortized)O(1)
add/remove (middle)O(n) — shifts elements
contains / indexOfO(n)
💡 When capacity is exceeded, ArrayList grows by ~50% (copies to a new array). Pre-size with new ArrayList<>(expectedSize) to avoid repeated resizing in bulk loads.

LinkedList

☕ Java · Collections · 4 min read

A doubly-linked list implementing both List and Deque. Fast inserts/removes at the ends or via an iterator; slow random access.

ArrayListLinkedList
get(i)O(1)O(n)
add/remove endsO(1)/O(n)O(1)
MemoryCompactExtra node pointers
💡 In practice ArrayList wins almost always (cache locality). Use LinkedList only when you need a Deque/queue or constant-time add/remove at both ends — and even then ArrayDeque is usually faster.

Set Interface

☕ Java · Collections · 4 min read

A Set models a mathematical set — no duplicates. Adding an existing element is a no-op.

ImplementationOrderBacked by
HashSetNoneHashMap
LinkedHashSetInsertion orderLinkedHashMap
TreeSetSortedRed-black tree
Set<String> set = new HashSet<>(); set.add("a"); set.add("a"); // size = 1
💡 Uniqueness relies on equals() + hashCode() — custom objects must override both or duplicates sneak in.

HashSet

☕ Java · Collections · 3 min read

The most common Set — O(1) add/remove/contains, no ordering. Internally a HashMap where elements are keys.

Set<Integer> seen = new HashSet<>(); if (!seen.add(x)) { /* x was a duplicate */ } // add returns false if present
💡 Perfect for de-duplication and fast membership checks. Ordering is unspecified and can change — use LinkedHashSet if you need insertion order.

TreeSet

☕ Java · Collections · 4 min read

A sorted Set backed by a red-black tree — elements kept in natural order (or by a Comparator). O(log n) operations.

TreeSet<Integer> ts = new TreeSet<>(); ts.add(5); ts.add(1); ts.add(3); // [1, 3, 5] ts.first(); ts.last(); ts.ceiling(2); // 3 — smallest ≥ 2 ts.floor(4); // 3 — largest ≤ 4 ts.headSet(3); ts.tailSet(3);
💡 TreeSet's navigation methods (ceiling, floor, headSet) make it ideal for range queries and "nearest value" problems. Elements must be Comparable or you must supply a Comparator.

Map Interface

☕ Java · Collections · 4 min read

A Map stores key→value pairs with unique keys. Not a Collection, but central to the framework.

Map<String,Integer> m = new HashMap<>(); m.put("a", 1); m.getOrDefault("b", 0); m.putIfAbsent("a", 99); // no-op, "a" exists m.computeIfAbsent("c", k -> new ...); // lazy init m.merge("a", 1, Integer::sum); // increment counter for (var e : m.entrySet()) { e.getKey(); e.getValue(); }

Implementations: HashMap, LinkedHashMap, TreeMap, ConcurrentHashMap.

💡 The modern Map methods (computeIfAbsent, merge, getOrDefault) replace verbose null-check patterns — e.g. word counting becomes a one-liner with merge(word, 1, Integer::sum).

HashMap Internal Working

☕ Java · Collections · ★ Interview Topic · 11 min read

HashMap stores key→value pairs in an array of buckets. The key's hashCode() decides the bucket; collisions (same bucket) are chained in a linked list — or a balanced tree when a bucket gets large.

Bucket Structure — Visual

hash(key) & (n-1) → bucket index [0] [1] [2] [3] k1→v1 k9→v9 (collision) k3→v3 linked list per bucket;≥8 entries → red-blacktree (O(log n))

put(k, v) Step by Step

  1. Compute hash = key.hashCode() (then spread the bits).
  2. Bucket index = hash & (capacity - 1).
  3. Empty bucket → store the entry.
  4. Collision → walk the chain; if a key equals() exists, replace its value; else append.
  5. Chain length ≥ 8 (and capacity ≥ 64) → convert chain to a red-black tree.
  6. Size > capacity × load factor (0.75)resize (double capacity, rehash).

Key Facts

PropertyValue
Default capacity16
Load factor0.75
Treeify threshold8 (untreeify 6)
Avg get/putO(1), worst O(log n) (treed)
⚠️ Keys must have correct equals()/hashCode(). Mutable keys are dangerous — if a key's hash changes after insertion, you can never find it again. Prefer immutable keys.

LinkedHashMap

☕ Java · Collections · 3 min read

A HashMap that maintains a predictable iteration order — insertion order by default, or access order (great for LRU caches).

// access-order = true → LRU cache new LinkedHashMap<K,V>(16, 0.75f, true) { protected boolean removeEldestEntry(Map.Entry<K,V> e) { return size() > MAX; // auto-evict oldest } };
💡 The classic interview "implement an LRU cache" answer: extend LinkedHashMap with access-order + removeEldestEntry. Slightly more memory than HashMap (it keeps a linked list of entries).

TreeMap

☕ Java · Collections · 4 min read

A sorted Map backed by a red-black tree — keys kept in natural/Comparator order. O(log n) operations, plus navigation methods.

TreeMap<Integer,String> tm = new TreeMap<>(); tm.put(3,"c"); tm.put(1,"a"); tm.put(2,"b"); // iterates 1,2,3 tm.firstKey(); tm.lastKey(); tm.ceilingKey(2); tm.floorKey(2); tm.headMap(2); tm.tailMap(2); tm.subMap(1,3);
💡 Use TreeMap when you need sorted keys or range queries. Keys must be Comparable (or supply a Comparator). No null keys.

ConcurrentHashMap

☕ Java · Collections · ★ Interview Topic · 9 min read

A thread-safe HashMap built for high concurrency. Unlike Collections.synchronizedMap (one global lock) or the legacy Hashtable, it lets many threads read and write concurrently with minimal contention.

Fine-Grained Locking — Visual

synchronizedMap / Hashtable — ONE lock (threads queue): 🔒 whole map locked per operation ConcurrentHashMap — per-bucket (node) locking, lock-free reads: 🔓 read 🔒 bin 1 🔒 bin 2 🔓 read many threads in parallel

Key Properties

  • Lock-free reads (volatile reads); writes lock only the affected bin (CAS + synchronized node).
  • No null keys or values (ambiguous with "absent").
  • Atomic compound ops: compute, merge, putIfAbsent.
  • Iterators are weakly consistent — no ConcurrentModificationException.
ConcurrentHashMap<String,Integer> counts = new ConcurrentHashMap<>(); counts.merge(word, 1, Integer::sum); // atomic increment, thread-safe
💡 Interview line: "ConcurrentHashMap allows concurrent reads and segment/bin-level writes, so it scales far better than a globally-locked synchronizedMap or Hashtable." Use it for any shared mutable map.

Iterator

☕ Java · Collections · 4 min read

An Iterator traverses a collection element by element. The enhanced for-loop uses one under the hood. Use iterator.remove() to safely delete during iteration.

Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.isBlank()) it.remove(); // safe removal }
⚠️ Modifying a collection during a for-each loop (e.g. list.remove()) throws ConcurrentModificationException (fail-fast). Use the iterator's own remove(), or removeIf(), instead.

Comparable vs Comparator

☕ Java · Collections · ★ Interview Topic · 8 min read

Both define ordering, but differently. Comparable gives a class its natural ordering (one, built-in). Comparator is an external, swappable ordering (many possible).

Comparison

ComparableComparator
MethodcompareTo(o)compare(a, b)
Packagejava.langjava.util
WhereInside the classSeparate object/lambda
OrderingsOne (natural)Many
Modifies class?YesNo
// Comparable — natural order baked into the class class User implements Comparable<User> { int age; public int compareTo(User o) { return Integer.compare(age, o.age); } } Collections.sort(users); // uses compareTo // Comparator — external, flexible, chainable (Java 8) users.sort(Comparator.comparing(User::getName) .thenComparing(User::getAge).reversed());
💡 Interview answer: "Use Comparable for the one obvious natural order (e.g. numeric); use Comparator when you need multiple or custom orderings without touching the class." Modern Comparators chain beautifully with comparing/thenComparing.

Comparator

☕ Java · Collections · 4 min read

A functional interface for defining custom orderings externally — the flexible, composable way to sort.

Comparator<User> byName = Comparator.comparing(User::getName); Comparator<User> byAgeDesc = Comparator.comparingInt(User::getAge).reversed(); users.sort(byName.thenComparing(byAgeDesc)); // multi-level sort users.sort(Comparator.comparing(User::getName, Comparator.nullsFirst(Comparator.naturalOrder()))); // null-safe
💡 Since it's a functional interface, any lambda (a, b) -> ... is a Comparator. Prefer the factory methods (comparing, thenComparing, reversed, nullsFirst) over hand-written compare logic. See Comparable vs Comparator.

Generics

☕ Java · Generics & Nested · ★ Interview Topic · 9 min read

Generics enable type-safe, reusable code by parameterizing types — catching type errors at compile time and removing casts. List<String> guarantees only Strings go in and come out.

Why Generics

// Before generics — unsafe, needs casts List list = new ArrayList(); list.add("hi"); String s = (String) list.get(0); // runtime ClassCastException risk // With generics — compile-time safety, no cast List<String> list2 = new ArrayList<>(); String s2 = list2.get(0);

Generic Class & Method

class Box<T> { // type parameter private T value; public void set(T v) { value = v; } public T get() { return value; } } <T> T firstOf(List<T> list) { return list.get(0); } // generic method

Bounded Types & Wildcards

SyntaxMeaning
<T extends Number>Upper bound — T is Number or subtype
<? extends T>Producer — read T's (covariant)
<? super T>Consumer — write T's (contravariant)
<?>Unbounded wildcard
💡 Remember PECS: "Producer Extends, Consumer Super." Also note type erasure — generics exist only at compile time; at runtime List<String> and List<Integer> are both just List. That's why you can't do new T() or instanceof List<String>.

Enum

☕ Java · Generics & Nested · 4 min read

An enum is a type with a fixed set of constants. Java enums are full classes — they can have fields, constructors, and methods.

enum Status { ACTIVE("A"), INACTIVE("I"), PENDING("P"); // constants with data private final String code; Status(String code) { this.code = code; } // constructor public String getCode() { return code; } } Status.ACTIVE.getCode(); // "A" Status.valueOf("ACTIVE"); Status.values();
💡 Enums are implicitly final, type-safe, and work great in switch and as map keys (EnumMap). The cleanest Singleton in Java is a single-element enum.

Inner Classes

☕ Java · Generics & Nested · 5 min read

A class defined within another class. Four kinds, each with a use:

TypeNotes
Static nestedNo outer instance ref; a namespaced helper
Inner (non-static)Holds an implicit outer reference
LocalDefined inside a method
AnonymousOne-off, no name (next topic)
class Outer { static class Builder { ... } // static nested (e.g. builders) class Inner { ... } // needs an Outer instance }
⚠️ Non-static inner classes hold a hidden reference to the outer instance — a common memory-leak source (e.g. an inner Runnable outliving its outer). Prefer static nested classes unless you truly need the outer reference.

Anonymous Classes

☕ Java · Generics & Nested · 4 min read

An anonymous class is a one-off, unnamed class declared and instantiated in a single expression — typically to implement an interface or extend a class on the spot.

// classic anonymous class Runnable r = new Runnable() { @Override public void run() { System.out.println("hi"); } }; // lambda — far more concise for functional interfaces Runnable r2 = () -> System.out.println("hi");
💡 For single-method (functional) interfaces, lambdas have replaced most anonymous classes. Anonymous classes are still useful when you need state, multiple methods, or to extend a class.

Functional Interfaces

☕ Java · Functional & Java 8+ · 5 min read

A functional interface has exactly one abstract method (SAM) — so it can be implemented by a lambda or method reference. Marked (optionally) with @FunctionalInterface.

Built-in Functional Interfaces (java.util.function)

InterfaceMethodUse
Function<T,R>R apply(T)Transform
Predicate<T>boolean test(T)Filter/condition
Consumer<T>void accept(T)Side effect
Supplier<T>T get()Produce
BiFunction<T,U,R>R apply(T,U)Two-arg transform
@FunctionalInterface interface Calculator { int op(int a, int b); } Calculator add = (a, b) -> a + b; // lambda implements the SAM
💡 @FunctionalInterface is optional but recommended — the compiler then enforces the single-abstract-method rule. Default/static methods don't count against it.

Lambda Expressions

☕ Java · Functional & Java 8+ · ★ Interview Topic · 8 min read

Lambdas (Java 8) are anonymous functions — concise implementations of functional interfaces. They enable functional-style programming and power the Stream API.

Syntax Evolution — Visual

Anonymous class (verbose)new Comparator(){ public int compare(a,b){return a-b; } } Lambda (concise)(a, b) -> a - b

Forms

() -> 42 // no args x -> x * 2 // one arg (parens optional) (x, y) -> x + y // multiple args (x, y) -> { return x + y; } // block body list.forEach(s -> System.out.println(s)); list.removeIf(s -> s.isBlank()); list.sort((a, b) -> a.compareTo(b));
💡 A lambda can only implement a functional interface. It captures effectively final local variables (you can't reassign captured locals). Under the hood it compiles to an invokedynamic call, not an anonymous class.

Stream API

☕ Java · Functional & Java 8+ · ★ Interview Topic · 11 min read

The Stream API (Java 8) processes sequences of elements declaratively — filter, map, reduce — in a readable pipeline. Streams don't store data; they compute on demand and are consumed once.

Pipeline — Visual

sourcelist.stream() filter()intermediate (lazy) map()intermediate (lazy) collect()terminal (triggers)

Common Operations

List<String> names = users.stream() .filter(u -> u.getAge() > 18) // intermediate .map(User::getName) // intermediate .sorted() .collect(Collectors.toList()); // terminal long count = users.stream().filter(User::isActive).count(); Map<Dept,List<User>> byDept = users.stream() .collect(Collectors.groupingBy(User::getDept)); int total = nums.stream().mapToInt(Integer::intValue).sum();
Intermediate (lazy)Terminal (eager)
filter, map, sorted, distinct, limit, peekcollect, forEach, reduce, count, anyMatch, findFirst
💡 Intermediate ops are lazy — nothing runs until a terminal op. A stream is single-use (consumed once). Use .parallelStream() for CPU-bound work on large datasets, but measure — it's not always faster.

Optional

☕ Java · Functional & Java 8+ · ★ Interview Topic · 7 min read

Optional<T> (Java 8) is a container that may or may not hold a value — making "absence" explicit in the type system and helping eliminate NullPointerException.

Optional<User> found = repo.findById(id); // safe handling — no null checks String name = found.map(User::getName).orElse("Unknown"); found.ifPresent(u -> send(u)); User u = found.orElseThrow(() -> new NotFoundException(id)); // creating Optional.of(value); // non-null Optional.ofNullable(maybe);// may be null Optional.empty();
⚠️ Use Optional as a return type for "might be absent" results — not for fields or method parameters. Don't call .get() without checking (defeats the purpose); use orElse/map/ifPresent. Never return null from a method declared to return Optional.

Method References

☕ Java · Functional & Java 8+ · 4 min read

A method reference (::) is shorthand for a lambda that just calls an existing method. More readable when the lambda only delegates.

KindSyntaxLambda equivalent
StaticInteger::parseInts -> Integer.parseInt(s)
Instance (of object)System.out::printlnx -> System.out.println(x)
Instance (of type)String::toUpperCases -> s.toUpperCase()
ConstructorUser::new() -> new User()
names.stream().map(String::toUpperCase).forEach(System.out::println);
💡 Use a method reference when a lambda would just forward its argument to one method; use a lambda when there's extra logic. They're interchangeable as functional-interface implementations.

Date and Time API

☕ Java · Functional & Java 8+ · 5 min read

Java 8's java.time package replaced the broken, mutable Date/Calendar with an immutable, thread-safe, fluent API.

ClassRepresents
LocalDateDate (no time)
LocalTimeTime (no date)
LocalDateTimeDate + time (no zone)
ZonedDateTimeDate + time + zone
InstantMachine timestamp (epoch)
Duration / PeriodTime / date amounts
LocalDate today = LocalDate.now(); LocalDate next = today.plusDays(7); // immutable — returns new Period age = Period.between(birth, today); Duration d = Duration.between(t1, t2); ZonedDateTime z = ZonedDateTime.now(ZoneId.of("Asia/Kolkata"));
💡 All java.time types are immutable and thread-safe — every "modification" returns a new instance. Always prefer this over legacy java.util.Date.

Java 8 Features

☕ Java · Functional & Java 8+ · ★ Interview Topic · 9 min read

Java 8 (2014) was the most transformative release — it brought functional programming to Java. Still the most-asked version in interviews.

The Headline Features

Lambda Expressions

Anonymous functions: (a,b) -> a+b. Enable functional style.

Stream API

Declarative collection processing: filter/map/reduce pipelines.

Functional Interfaces

SAM interfaces + java.util.function (Function, Predicate…).

Optional

Explicit absence to fight NullPointerException.

Default & static methods

Interfaces can now have implementations.

java.time

New immutable Date/Time API.

Method References

Class::method shorthand.

Collectors

groupingBy, joining, toMap, partitioningBy.

Map<Boolean,List<User>> partitioned = users.stream() .collect(Collectors.partitioningBy(u -> u.getAge() >= 18));
💡 Default methods solved interface evolution — they let Collection.stream() be added to existing interfaces without breaking implementers. The combination of lambdas + streams + Optional is the heart of every Java 8 interview.

Java 8 FAQ — Stream API & Lambda Expressions

☕ Java · Functional & Java 8+ · ★ Interview Topic · 18 min read

25 frequently asked Java 8 interview questions covering Stream API, Lambda expressions, Functional Interfaces and the Date/Time API — all demonstrated with a realistic Employee domain model.

All examples below assume this Employee class and a pre-built list employees. Copy once, use everywhere.
// Employee.java class Employee { private String name; private String department; private double salary; private int age; private String city; public Employee(String name, String department, double salary, int age, String city) { this.name = name; this.department = department; this.salary = salary; this.age = age; this.city = city; } public String getName() { return name; } public String getDepartment() { return department; } public double getSalary() { return salary; } public int getAge() { return age; } public String getCity() { return city; } @Override public String toString() { return name + " (" + department + ", ₹" + salary + ")"; } } // Sample data used in all examples below List<Employee> employees = Arrays.asList( new Employee("Aryan", "Engineering", 95000, 28, "Delhi"), new Employee("Priya", "Marketing", 72000, 34, "Mumbai"), new Employee("Rohan", "Engineering", 110000, 31, "Bangalore"), new Employee("Sneha", "HR", 58000, 26, "Pune"), new Employee("Vikram", "Marketing", 85000, 40, "Delhi"), new Employee("Ananya", "Engineering", 130000, 35, "Bangalore"), new Employee("Karan", "HR", 62000, 29, "Hyderabad"), new Employee("Meera", "Finance", 99000, 38, "Mumbai"), new Employee("Raj", "Finance", 88000, 33, "Delhi"), new Employee("Divya", "Engineering", 75000, 27, "Pune") );

Q1. What is a Stream? Is it a data structure?

A Stream is a sequence of elements supporting sequential and parallel aggregate operations. It is not a data structure — it doesn't store data. It reads from a source (collection, array, I/O), processes elements lazily via a pipeline, and produces a result through a terminal operation. A Stream can only be consumed once.

Data Structure

Stores elements in memory. Can be iterated multiple times. Examples: ArrayList, HashSet.

Stream

Describes computation over a data source. One-time use, lazy evaluation, no backing storage.

Q2. What is the difference between intermediate and terminal operations?

IntermediateTerminal
Returns a new Stream (lazy)Produces a result or side-effect (eager)
filter, map, sorted, distinct, peek, limit, skip, flatMapcollect, forEach, count, reduce, findFirst, anyMatch, min, max
Pipeline is built but not executedTriggers the entire pipeline to execute
// Nothing executes until collect() — the terminal op long count = employees.stream() .filter(e -> e.getSalary() > 80000) // intermediate .map(Employee::getName) // intermediate .count(); // terminal — triggers pipeline

Q3. What is lazy evaluation in Streams?

Intermediate operations are not executed when called — they build a lazy pipeline. Only when a terminal operation is invoked does the stream engine begin pulling elements through the chain. This means a filter + findFirst stops as soon as one match is found — it never processes the rest of the list.

// peek() shows lazy evaluation — only elements that pass filter are mapped Optional<String> first = employees.stream() .peek(e -> System.out.println("Before filter: " + e.getName())) .filter(e -> e.getSalary() > 100000) .peek(e -> System.out.println("After filter: " + e.getName())) .map(Employee::getName) .findFirst(); // Output: filter + map stops as soon as Rohan is found (not all 10 employees processed)

Q4. What is the difference between map() and flatMap()?

map()flatMap()
InputStream<T>Stream<Stream<T>>
OutputStream<R> (one element per input)Stream<R> (flattened — many elements per input)
Use when1:1 transformation1:N transformation — each element produces a list/stream
// map — transform each employee to their name List<String> names = employees.stream() .map(Employee::getName) .collect(Collectors.toList()); // flatMap — each employee has a list of skills; flatten all skills into one stream List<String> allSkills = employees.stream() .map(e -> getSkills(e)) // returns List<String> per employee .flatMap(Collection::stream) // flatten List<List<String>> → Stream<String> .distinct() .collect(Collectors.toList());

Q5. Sort employees by salary — ascending and descending

// Ascending salary List<Employee> byAscSalary = employees.stream() .sorted(Comparator.comparingDouble(Employee::getSalary)) .collect(Collectors.toList()); // Descending salary List<Employee> byDescSalary = employees.stream() .sorted(Comparator.comparingDouble(Employee::getSalary).reversed()) .collect(Collectors.toList()); // Multi-key sort: department ascending, then salary descending within department List<Employee> multiSort = employees.stream() .sorted(Comparator.comparing(Employee::getDepartment) .thenComparing(Comparator.comparingDouble(Employee::getSalary).reversed())) .collect(Collectors.toList());

Q6. Sort employees by name alphabetically

// Natural (A-Z) List<String> sortedNames = employees.stream() .map(Employee::getName) .sorted() .collect(Collectors.toList()); // Z-A using Comparator.reverseOrder() List<String> reversedNames = employees.stream() .map(Employee::getName) .sorted(Comparator.reverseOrder()) .collect(Collectors.toList()); // Sort Employee objects by name length, then alphabetically List<Employee> byNameLen = employees.stream() .sorted(Comparator.comparingInt((Employee e) -> e.getName().length()) .thenComparing(Employee::getName)) .collect(Collectors.toList());

Q7. Filter employees by department

// Single department List<Employee> engineers = employees.stream() .filter(e -> e.getDepartment().equals("Engineering")) .collect(Collectors.toList()); // Multiple departments using Set lookup (O(1) check) Set<String> techDepts = Set.of("Engineering", "Finance"); List<Employee> techEmployees = employees.stream() .filter(e -> techDepts.contains(e.getDepartment())) .collect(Collectors.toList()); // Employees NOT in HR List<Employee> nonHR = employees.stream() .filter(e -> !e.getDepartment().equals("HR")) .collect(Collectors.toList());

Q8. Find the employee with the highest / lowest salary

// Highest salary Optional<Employee> highest = employees.stream() .max(Comparator.comparingDouble(Employee::getSalary)); highest.ifPresent(e -> System.out.println("Highest: " + e.getName() + " → ₹" + e.getSalary())); // Lowest salary Optional<Employee> lowest = employees.stream() .min(Comparator.comparingDouble(Employee::getSalary)); // Top 3 highest paid List<Employee> top3 = employees.stream() .sorted(Comparator.comparingDouble(Employee::getSalary).reversed()) .limit(3) .collect(Collectors.toList()); // Second highest salary Optional<Employee> secondHighest = employees.stream() .sorted(Comparator.comparingDouble(Employee::getSalary).reversed()) .skip(1) .findFirst();

Q9. Group employees by department

// Map<Department, List<Employee>> Map<String, List<Employee>> byDept = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment)); byDept.forEach((dept, empList) -> System.out.println(dept + ": " + empList.size() + " employees")); // Group by department → collect only names Map<String, List<String>> deptNames = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.mapping(Employee::getName, Collectors.toList()) )); // Count employees per department Map<String, Long> deptCount = employees.stream() .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting()));

Q10. Find average and total salary — overall and per department

// Overall average salary OptionalDouble avg = employees.stream() .mapToDouble(Employee::getSalary) .average(); // Total salary (sum) double total = employees.stream() .mapToDouble(Employee::getSalary) .sum(); // Average salary per department Map<String, Double> avgByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.averagingDouble(Employee::getSalary) )); // Total salary per department Map<String, Double> sumByDept = employees.stream() .collect(Collectors.groupingBy( Employee::getDepartment, Collectors.summingDouble(Employee::getSalary) )); // Department with highest average salary avgByDept.entrySet().stream() .max(Map.Entry.comparingByValue()) .ifPresent(e -> System.out.println("Best paid dept: " + e.getKey() + " → ₹" + e.getValue()));

Q11. Partition employees into two groups (age > 30 and ≤ 30)

partitioningBy always returns a Map<Boolean, List<T>> with exactly two keys: true and false.

Map<Boolean, List<Employee>> partitioned = employees.stream() .collect(Collectors.partitioningBy(e -> e.getAge() > 30)); List<Employee> seniorEmployees = partitioned.get(true); List<Employee> juniorEmployees = partitioned.get(false); // Partition and count only Map<Boolean, Long> countByAge = employees.stream() .collect(Collectors.partitioningBy( e -> e.getAge() > 30, Collectors.counting() ));

Q12. Get distinct department names

// Using distinct() List<String> departments = employees.stream() .map(Employee::getDepartment) .distinct() .sorted() .collect(Collectors.toList()); // Output: [Engineering, Finance, HR, Marketing] // Using toSet() — inherently unique Set<String> deptSet = employees.stream() .map(Employee::getDepartment) .collect(Collectors.toSet());

Q13. Check if any / all / none match a condition

// anyMatch — short-circuits on first match boolean hasDelhi = employees.stream() .anyMatch(e -> e.getCity().equals("Delhi")); // allMatch — all must satisfy boolean allAbove50k = employees.stream() .allMatch(e -> e.getSalary() >= 50000); // noneMatch — none must satisfy boolean noNegativeSalary = employees.stream() .noneMatch(e -> e.getSalary() < 0); System.out.println("Any in Delhi? " + hasDelhi); // true System.out.println("All above 50k? " + allAbove50k); // true System.out.println("No negative? " + noNegativeSalary);// true

Q14. How does reduce() work? Show a salary sum example.

reduce(identity, BinaryOperator) combines elements into a single value by repeatedly applying a function.

// Sum salaries using reduce double totalSalary = employees.stream() .map(Employee::getSalary) .reduce(0.0, Double::sum); // Concatenate all names — reduce without identity returns Optional Optional<String> allNames = employees.stream() .map(Employee::getName) .reduce((a, b) -> a + ", " + b); // "Aryan, Priya, Rohan, Sneha, Vikram, ..." // Find highest salary using reduce Optional<Double> maxSal = employees.stream() .map(Employee::getSalary) .reduce(Double::max);

Q15. What is the difference between findFirst() and findAny()?

findFirst()findAny()
ResultAlways returns first element in encounter orderReturns any element — non-deterministic in parallel streams
ParallelForces coordination — slowerReturns whichever thread finds one first — faster
SequentialSame as findAny()Same as findFirst()
// Sequential — both return the same first Engineering employee Optional<Employee> first = employees.stream() .filter(e -> e.getDepartment().equals("Engineering")) .findFirst(); // always Aryan // Parallel — findAny() is faster, result may vary Optional<Employee> any = employees.parallelStream() .filter(e -> e.getDepartment().equals("Engineering")) .findAny(); // could be Aryan, Rohan, Ananya, or Divya

Q16. Convert the employee list to a Map<Name, Salary>

// toMap(keyMapper, valueMapper) Map<String, Double> nameSalaryMap = employees.stream() .collect(Collectors.toMap( Employee::getName, Employee::getSalary )); // If duplicate keys are possible, add a merge function Map<String, Double> safe = employees.stream() .collect(Collectors.toMap( Employee::getName, Employee::getSalary, (existing, replacement) -> existing // keep first on conflict )); // Preserve insertion order → LinkedHashMap Map<String, Double> ordered = employees.stream() .collect(Collectors.toMap( Employee::getName, Employee::getSalary, (a, b) -> a, LinkedHashMap::new ));

Q17. Join employee names into a comma-separated string

// Collectors.joining String csv = employees.stream() .map(Employee::getName) .collect(Collectors.joining(", ")); // "Aryan, Priya, Rohan, Sneha, Vikram, Ananya, Karan, Meera, Raj, Divya" // With prefix and suffix String bracketed = employees.stream() .map(Employee::getName) .collect(Collectors.joining(", ", "[", "]")); // "[Aryan, Priya, Rohan, ...]" // Sorted, upper-case, comma-separated String fancy = employees.stream() .map(e -> e.getName().toUpperCase()) .sorted() .collect(Collectors.joining(" | "));

Q18. Find employees earning above the average salary

// Compute average first (eager), then filter (lazy) double average = employees.stream() .mapToDouble(Employee::getSalary) .average() .orElse(0); List<Employee> aboveAverage = employees.stream() .filter(e -> e.getSalary() > average) .sorted(Comparator.comparingDouble(Employee::getSalary).reversed()) .collect(Collectors.toList()); System.out.println("Average: ₹" + average); aboveAverage.forEach(e -> System.out.println(e.getName() + " ₹" + e.getSalary()));

Q19. Get statistics (min, max, avg, count, sum) in one pass

// DoubleSummaryStatistics — all stats in a single stream traversal DoubleSummaryStatistics stats = employees.stream() .collect(Collectors.summarizingDouble(Employee::getSalary)); System.out.println("Count : " + stats.getCount()); System.out.println("Sum : ₹" + stats.getSum()); System.out.println("Min : ₹" + stats.getMin()); System.out.println("Max : ₹" + stats.getMax()); System.out.println("Avg : ₹" + stats.getAverage()); // Same with mapToDouble + summaryStatistics() DoubleSummaryStatistics s2 = employees.stream() .mapToDouble(Employee::getSalary) .summaryStatistics();

Q20. What is a Lambda Expression? Explain its syntax.

A lambda is a concise, anonymous function that implements a functional interface (an interface with exactly one abstract method). Syntax: (parameters) -> body

// Before Java 8 — anonymous class Comparator<Employee> old = new Comparator<Employee>() { @Override public int compare(Employee a, Employee b) { return Double.compare(a.getSalary(), b.getSalary()); } }; // Java 8 lambda — same thing, 1 line Comparator<Employee> byAge = (a, b) -> Double.compare(a.getSalary(), b.getSalary()); // Method reference shorthand (even cleaner) Comparator<Employee> bySal = Comparator.comparingDouble(Employee::getSalary); // Lambda with block body (multiple statements) Predicate<Employee> isHighEarner = e -> { double threshold = 90000; return e.getSalary() >= threshold && e.getAge() < 35; };

Q21. What are Functional Interfaces? Name the key ones in java.util.function.

InterfaceMethodTakesReturnsUse case
Predicate<T>test(T)TbooleanFilter / condition check
Function<T,R>apply(T)TRTransform / map
Consumer<T>accept(T)TvoidSide-effect (print, save)
Supplier<T>get()TFactory / lazy init
BiFunction<T,U,R>apply(T,U)T, URTwo-arg transform
UnaryOperator<T>apply(T)TTModify same type
BinaryOperator<T>apply(T,T)T, TTCombine two of same type
Predicate<Employee> isEngineer = e -> e.getDepartment().equals("Engineering"); Function<Employee,String> toName = Employee::getName; Consumer<Employee> printEmp = e -> System.out.println(e.getName() + " ₹" + e.getSalary()); Supplier<List<Employee>> emptyList = ArrayList::new; // Chaining Predicates Predicate<Employee> highPaidEngineer = isEngineer.and(e -> e.getSalary() > 100000); Predicate<Employee> notHR = ((Predicate<Employee>) e -> e.getDepartment().equals("HR")).negate(); employees.stream().filter(highPaidEngineer).forEach(printEmp);

Q22. What are Method References? Show the four types.

TypeSyntaxExample
Static methodClassName::staticMethodMath::abs
Instance method of a particular objectinstance::methodSystem.out::println
Instance method of an arbitrary objectClassName::instanceMethodEmployee::getName
ConstructorClassName::newEmployee::new
// All four in action employees.stream() .map(Employee::getName) // instance method of arbitrary obj .map(String::toUpperCase) // instance method of arbitrary obj .forEach(System.out::println); // instance method of particular obj // Constructor reference — create new list from stream employees.stream() .map(e -> new Employee(e.getName(), e.getDepartment(), e.getSalary() * 1.10, e.getAge(), e.getCity())) .collect(Collectors.toList()); // 10% raise for all

Q23. Comparable vs Comparator using Lambda — what's the difference?

ComparableComparator
Packagejava.langjava.util
MethodcompareTo(T o)compare(T o1, T o2)
Sort sequencesNatural / singleMultiple, external, flexible
Modifies class?Yes — class must implement itNo — defined externally
// Comparator with lambda — sort by salary, then by name employees.sort(Comparator.comparingDouble(Employee::getSalary) .thenComparing(Employee::getName)); // Comparator.comparing with key extractor employees.sort(Comparator.comparing(Employee::getDepartment, String.CASE_INSENSITIVE_ORDER)); // Reverse natural order of names employees.stream() .sorted(Comparator.comparing(Employee::getName, Comparator.reverseOrder())) .forEach(e -> System.out.println(e.getName()));

Q24. What is peek() and when should you use it?

peek(Consumer) is an intermediate operation that lets you inspect each element as it flows through the pipeline without modifying it. Primarily used for debugging.

// Debug: see which employees pass the filter List<Employee> result = employees.stream() .peek(e -> System.out.println("Input: " + e.getName())) .filter(e -> e.getSalary() > 90000) .peek(e -> System.out.println("Passed filter: " + e.getName())) .sorted(Comparator.comparingDouble(Employee::getSalary).reversed()) .peek(e -> System.out.println("After sort: " + e.getName())) .collect(Collectors.toList());
⚠️ Never use peek() to modify elements (e.g., calling setters) — its behaviour in parallel streams is non-deterministic and it may not execute if no terminal operation is attached.

Q25. Can a Stream be reused? What happens if you try?

Stream<Employee> stream = employees.stream() .filter(e -> e.getSalary() > 80000); List<Employee> list1 = stream.collect(Collectors.toList()); // ✅ OK List<Employee> list2 = stream.collect(Collectors.toList()); // ❌ IllegalStateException // java.lang.IllegalStateException: stream has already been operated upon or closed // Fix — use a Supplier to recreate the stream each time Supplier<Stream<Employee>> streamSupplier = () -> employees.stream().filter(e -> e.getSalary() > 80000); List<Employee> r1 = streamSupplier.get().collect(Collectors.toList()); List<Employee> r2 = streamSupplier.get().sorted(Comparator.comparing(Employee::getName)) .collect(Collectors.toList());

Q26. What is the difference between map() and mapToInt() / mapToDouble()?

mapToInt / mapToDouble / mapToLong return primitive specialised streams (IntStream, DoubleStream, LongStream) that avoid boxing overhead and expose numeric-specific methods like sum(), average(), range().

// map returns Stream<Integer> — boxed, slower Stream<Integer> ages = employees.stream().map(Employee::getAge); // mapToInt returns IntStream — unboxed, faster, more methods IntStream ageStream = employees.stream().mapToInt(Employee::getAge); int totalAge = ageStream.sum(); double avgAge = employees.stream().mapToInt(Employee::getAge).average().orElse(0); // IntStream.range() — iterate without an array IntStream.range(0, employees.size()) .forEach(i -> System.out.println(i + ": " + employees.get(i).getName()));

Q27. What is a Parallel Stream? When should you use it?

A parallel stream splits the work across multiple threads (using the common ForkJoinPool). Use it for CPU-intensive, independent, stateless operations on large datasets. Avoid for small lists, ordered results, or operations with shared mutable state.

// Parallel stream — automatically uses all available cores double sumParallel = employees.parallelStream() .mapToDouble(Employee::getSalary) .sum(); // Convert sequential to parallel mid-pipeline List<String> processed = employees.stream() .filter(e -> e.getSalary() > 70000) .parallel() .map(Employee::getName) .collect(Collectors.toList());
⚠️ Parallel streams are NOT always faster — thread coordination overhead dominates for small collections. Benchmark before using. Also, forEach on a parallel stream does not guarantee encounter order — use forEachOrdered if order matters.
💡 Interview cheat-sheet: Stream = lazy pipeline. Intermediate ops build it; terminal ops fire it. A stream is consumed once. map is 1:1; flatMap is 1:N. collect is the most powerful terminal op — master groupingBy, partitioningBy, joining, toMap and summarizingDouble.

Java 11 Features

☕ Java · Functional & Java 8+ · 4 min read

Java 11 (2018) is a popular LTS release. Highlights:

  • var in lambda params(var x, var y) -> ....
  • New String methodsisBlank(), strip(), lines(), repeat(n).
  • Files.readString() / writeString() — one-line file I/O.
  • HttpClient — modern, async HTTP client (no more HttpURLConnection).
  • Run source directlyjava Hello.java (no compile step).
  • Collection.toArray(IntFunction).
var client = HttpClient.newHttpClient(); var req = HttpRequest.newBuilder(URI.create(url)).build(); var res = client.send(req, BodyHandlers.ofString());
💡 Java 8 and 11 are the two most common LTS versions in enterprises. 11 is mostly incremental over 8 — the big jump was 8.

Java 17 Features

☕ Java · Functional & Java 8+ · ★ Interview Topic · 14 min read

Java 17 (September 2021) is the most widely adopted LTS after Java 8/11. It delivered multiple language-level modernisations that interviewers love — Records, Sealed Classes, Pattern Matching for instanceof, Switch Expressions and Text Blocks. All examples use the Employee domain.

Records (JEP 395)

Immutable data carriers with auto-generated constructor, getters, equals, hashCode, toString.

Sealed Classes (JEP 409)

Restrict which classes may extend/implement — enables closed type hierarchies.

Pattern Matching instanceof (JEP 394)

Binding variable in instanceof check — no explicit cast needed.

Switch Expressions (JEP 361)

Arrow syntax, returns a value, exhaustive — no fall-through bugs.

Text Blocks (JEP 378)

Multi-line strings with """ — perfect for JSON, SQL, HTML snippets.

Helpful NullPointerExceptions

JVM now tells you exactly which variable in a chain was null.

1. Records

A record is a concise, final, implicitly immutable class. The compiler generates: all-arg canonical constructor, accessor methods (no "get" prefix), equals(), hashCode(), and toString().

// Before Java 17 — verbose POJO public final class EmployeeDTO { private final String name; private final String department; private final double salary; public EmployeeDTO(String name, String department, double salary) { ... } public String getName() { return name; } // + equals, hashCode, toString — 40+ lines } // Java 17 record — everything above in ONE line public record EmployeeDTO(String name, String department, double salary) {} // Usage EmployeeDTO e = new EmployeeDTO("Aryan", "Engineering", 95000); System.out.println(e.name()); // accessor — no "get" prefix System.out.println(e.department()); System.out.println(e); // EmployeeDTO[name=Aryan, department=Engineering, salary=95000.0] // Records work seamlessly with Streams List<EmployeeDTO> dtos = employees.stream() .map(e2 -> new EmployeeDTO(e2.getName(), e2.getDepartment(), e2.getSalary())) .collect(Collectors.toList()); // Compact canonical constructor — add validation public record EmployeeDTO(String name, String department, double salary) { public EmployeeDTO { // compact constructor, no parens if (salary < 0) throw new IllegalArgumentException("Salary cannot be negative"); name = name.trim(); // normalise before storage } } // Custom methods are allowed public record EmployeeDTO(String name, String department, double salary) { public boolean isSenior() { return salary > 100000; } public EmployeeDTO withRaise(double percent) { return new EmployeeDTO(name, department, salary * (1 + percent / 100)); } }
⚠️ Records are implicitly final — cannot be extended. All fields are private final — no setters. Use them for DTOs, value objects, and return types, not for mutable entities.

2. Sealed Classes & Interfaces

A sealed type explicitly declares which types may extend/implement it using permits. Permitted subtypes must be final, sealed, or non-sealed. This enables exhaustive switch expressions without a default branch.

// Model employee role as a closed hierarchy public sealed interface EmployeeRole permits Manager, Developer, Intern {} public record Manager(String name, int teamSize) implements EmployeeRole {} public record Developer(String name, String stack) implements EmployeeRole {} public record Intern(String name, String college) implements EmployeeRole {} // Exhaustive switch — compiler verifies all subtypes covered String describe(EmployeeRole role) { return switch (role) { case Manager m -> m.name() + " manages " + m.teamSize() + " people"; case Developer d -> d.name() + " works on " + d.stack(); case Intern i -> i.name() + " from " + i.college(); // no default needed — sealed hierarchy is exhaustive }; } // Sealed class (not record) — some subtypes need mutable state public sealed class Compensation permits FixedSalary, HourlyWage, Commission {} public final class FixedSalary extends Compensation { double monthly; } public final class HourlyWage extends Compensation { double rate; int hours; } public non-sealed class Commission extends Compensation {} // open again

3. Pattern Matching for instanceof

Combines the type test and cast into a single expression, binding a new variable scoped to the if block.

// Before Java 17 — type check + explicit cast Object obj = getPayload(); if (obj instanceof Employee) { Employee e = (Employee) obj; // redundant cast System.out.println(e.getName()); } // Java 17 — pattern variable 'e' is bound automatically if (obj instanceof Employee e) { System.out.println(e.getName()); // 'e' only in scope when true } // Combine with conditions in the same expression if (obj instanceof Employee e && e.getSalary() > 100000) { System.out.println(e.getName() + " is a high earner"); } // Negation — variable NOT in scope in the if-block if (!(obj instanceof Employee e)) { return "Not an employee"; } System.out.println(e.getName()); // 'e' in scope here (after early return) // Useful in polymorphic processing void processPayload(Object payload) { if (payload instanceof EmployeeDTO dto) logDTO(dto); else if (payload instanceof List<?> list) logList(list); else if (payload instanceof String s) logString(s); }

4. Switch Expressions

Switch can now return a value using the arrow (->) syntax. Cases are exhaustive — the compiler enforces that all possible values are handled. No more fall-through bugs.

// Old switch — statement, fall-through, verbose String grade; switch (dept) { case "Engineering": grade = "Tech"; break; case "Marketing": grade = "Biz"; break; default: grade = "Other"; } // Java 17 switch expression — returns a value String grade = switch (dept) { case "Engineering", "Finance" -> "Core"; case "Marketing", "HR" -> "Support"; default -> "Other"; }; // With yield — multi-statement case arm double bonus = switch (employee.getDepartment()) { case "Engineering" -> employee.getSalary() * 0.20; case "Marketing" -> employee.getSalary() * 0.15; case "HR" -> employee.getSalary() * 0.10; default -> { System.out.println("Unknown dept: " + employee.getDepartment()); yield 0; // yield is the return statement inside a block arm } }; // Combine with employee salary bands String band = switch ((int)(employee.getSalary() / 25000)) { case 0, 1 -> "Band 1 (entry)"; case 2, 3 -> "Band 2 (mid)"; case 4, 5 -> "Band 3 (senior)"; default -> "Band 4 (principal+)"; };

5. Text Blocks

Text blocks ("""...""") let you write multi-line strings without escape characters. Leading whitespace is stripped based on the indentation of the closing """.

// Before — escape hell String json = "{\n \"name\": \"Aryan\",\n \"dept\": \"Engineering\"\n}"; // Java 17 text block — readable, indentation-aware String json = """ { "name": "%s", "department": "%s", "salary": %.2f } """.formatted(employee.getName(), employee.getDepartment(), employee.getSalary()); // SQL query for employees String sql = """ SELECT name, department, salary FROM employees WHERE department = '%s' AND salary > %f ORDER BY salary DESC """.formatted(dept, threshold); // HTML email snippet for employee onboarding String html = """ <div class="employee-card"> <h2>Welcome, %s!</h2> <p>Department: <strong>%s</strong></p> </div> """.formatted(employee.getName(), employee.getDepartment());

6. Helpful NullPointerExceptions (JEP 358)

Before Java 17, a NPE on a chained call just told you the line number. Now the JVM pins down exactly which dereference was null.

// Given: employee.getAddress().getCity().toUpperCase() // Before Java 14 — useless message // java.lang.NullPointerException // Java 14+ — precise message // Cannot invoke "Address.getCity()" because the return value of // "Employee.getAddress()" is null // Also helps with arrays, static fields, method args employees.get(0).getDepartment().toLowerCase(); // Cannot invoke "String.toLowerCase()" because the return value // of "Employee.getDepartment()" is null
💡 Interview summary: Java 17 = Records (immutable DTOs) + Sealed classes (closed hierarchies) + Pattern matching instanceof (no cast) + Switch expressions (value-returning, exhaustive) + Text blocks (multi-line strings). The first three work together: a sealed interface with record subtypes + a pattern-matching switch gives you algebraic data types in Java.

Java 21 Features

☕ Java · Functional & Java 8+ · ★ Interview Topic · 16 min read

Java 21 (September 2023) is the latest LTS. It delivered the most anticipated JVM feature in a decade — Virtual Threads — alongside record patterns, exhaustive switch, sequenced collections and string templates. All examples use the Employee domain.

Virtual Threads (JEP 444)

Lightweight JVM-managed threads — run millions concurrently without OS thread overhead.

Record Patterns (JEP 440)

Destructure record components directly inside instanceof or switch.

Pattern Matching for switch (JEP 441)

Type patterns + guarded patterns in switch — works with sealed hierarchies.

Sequenced Collections (JEP 431)

New interfaces (SequencedCollection, SequencedMap) with getFirst(), getLast(), reversed().

String Templates (Preview — JEP 430)

Safely interpolate values into strings — STR."Hello \{name}".

Unnamed Patterns & Variables (JEP 443)

Use _ for unused pattern variables — reduces boilerplate.

1. Virtual Threads (Project Loom)

Platform (OS) threads are expensive — a JVM caps at a few thousand. Virtual threads are JVM-managed, extremely lightweight (stack starts at ~1 KB vs ~1 MB for OS threads) — you can create millions. They are ideal for I/O-bound workloads (database calls, HTTP requests).

Platform ThreadVirtual Thread
Backed byOS thread (1:1)Carrier thread (M:N)
Stack size~1 MB~few KB (grows on demand)
Max practical~5,000–10,000Millions
Best forCPU-bound tasksI/O-bound tasks (blocking DB, HTTP)
Creation costHighNear zero
// Create a single virtual thread Thread vt = Thread.ofVirtual().start(() -> { System.out.println("Processing employee in virtual thread: " + Thread.currentThread()); }); // Virtual thread per task executor — best for I/O-heavy workloads try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { List<Future<Double>> futures = employees.stream() .map(emp -> executor.submit(() -> fetchSalaryFromDB(emp.getName()))) // I/O call .collect(Collectors.toList()); for (Future<Double> f : futures) { System.out.println("Fetched salary: " + f.get()); } } // executor auto-closed (AutoCloseable) // Process 100,000 employee records concurrently — impossible with OS threads try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 100_000).forEach(i -> executor.submit(() -> { var emp = fetchEmployee(i); // blocking I/O — fine, VT parks cheaply saveEnrichedRecord(emp); }) ); } // Named virtual threads — easier debugging Thread.ofVirtual() .name("emp-processor-", 0) // auto-incrementing name .start(() -> processEmployee(employees.get(0)));
⚠️ Virtual threads are NOT faster for CPU-bound work — use a bounded ForkJoinPool or fixed thread pool for that. Avoid synchronized blocks inside virtual threads (they pin the carrier thread) — prefer ReentrantLock instead.

2. Record Patterns

Java 21 lets you destructure a record's components directly in a pattern, eliminating the separate accessor calls.

// Records from Java 17 record EmployeeDTO(String name, String department, double salary) {} record Address(String city, String country) {} record EmployeeProfile(EmployeeDTO employee, Address address) {} // nested record // Java 17 — instanceof + accessors if (obj instanceof EmployeeDTO dto) { System.out.println(dto.name() + " in " + dto.department()); } // Java 21 record pattern — destructure components directly if (obj instanceof EmployeeDTO(String name, String dept, double salary)) { System.out.println(name + " in " + dept + " earns ₹" + salary); } // Nested record pattern — go deep in one expression if (obj instanceof EmployeeProfile(EmployeeDTO(var name, var dept, _), Address(var city, _))) { System.out.println(name + " from " + dept + " lives in " + city); // _ = unnamed pattern (JEP 443) — discard salary and country } // Record patterns in switch (combine with JEP 441) String describe(Object obj) { return switch (obj) { case EmployeeDTO(var n, "Engineering", var s) when s > 100000 -> n + " is a senior engineer"; case EmployeeDTO(var n, var d, _) -> n + " works in " + d; case null -> "null object"; default -> "unknown"; }; }

3. Pattern Matching for switch

Switch now accepts type patterns, works with null, supports guarded patterns (when clause), and is exhaustive on sealed hierarchies — replacing long if-else-instanceof chains.

// Sealed role hierarchy from Java 17 sealed interface EmployeeRole permits Manager, Developer, Intern {} record Manager(String name, int teamSize) implements EmployeeRole {} record Developer(String name, String stack) implements EmployeeRole {} record Intern(String name, String college) implements EmployeeRole {} // Java 21 — type patterns + guards in switch double calculateBonus(EmployeeRole role, double baseSalary) { return switch (role) { case Manager m when m.teamSize() > 10 -> baseSalary * 0.30; // guarded case Manager m -> baseSalary * 0.20; case Developer d when d.stack().contains("AI") -> baseSalary * 0.25; case Developer d -> baseSalary * 0.18; case Intern i -> baseSalary * 0.05; }; // exhaustive — no default needed (sealed hierarchy) } // null handling — switch now accepts null without NPE String label(EmployeeRole role) { return switch (role) { case null -> "No role assigned"; case Manager m -> "Manager: " + m.name(); case Developer d -> "Dev: " + d.name(); case Intern i -> "Intern: " + i.name(); }; } // Type pattern without sealed — still needs default String format(Object obj) { return switch (obj) { case Integer i -> "int: " + i; case String s -> "str: " + s; case Employee e -> "emp: " + e.getName(); default -> "other: " + obj.getClass().getSimpleName(); }; }

4. Sequenced Collections (JEP 431)

Java 21 added three new interfaces to the Collections hierarchy that give a uniform API for accessing the first and last element of any ordered collection, plus a reversed() view.

// New interfaces added to hierarchy // SequencedCollection<E> extends Collection<E> // getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast(), reversed() // SequencedSet<E> extends SequencedCollection + Set // SequencedMap<K,V> extends Map // firstEntry(), lastEntry(), reversed(), sequencedKeySet(), sequencedValues() List<Employee> sorted = employees.stream() .sorted(Comparator.comparingDouble(Employee::getSalary).reversed()) .collect(Collectors.toList()); // List now implements SequencedCollection // Before Java 21 — get first/last was inconsistent per collection type Employee first21 = sorted.get(0); // List Employee last21 = sorted.get(sorted.size() - 1); // verbose Employee dqFirst = deque.peekFirst(); // Deque — different API // Java 21 — uniform across all SequencedCollections Employee highest = sorted.getFirst(); // highest paid Employee lowest = sorted.getLast(); // lowest paid // Reversed view — no copy, O(1) List<Employee> ascending = sorted.reversed(); // LinkedHashMap now has sequenced access LinkedHashMap<String, Employee> empMap = employees.stream() .collect(Collectors.toMap(Employee::getName, e -> e, (a,b) -> a, LinkedHashMap::new)); Map.Entry<String, Employee> firstEntry = empMap.firstEntry(); Map.Entry<String, Employee> lastEntry = empMap.lastEntry();

5. String Templates (Preview — JEP 430)

String templates allow safe, readable string interpolation using template processors. STR. is the built-in processor. This is a preview feature in Java 21 (must enable with --enable-preview).

// Before — String.format or concatenation String msg1 = "Employee: " + emp.getName() + ", Dept: " + emp.getDepartment(); String msg2 = String.format("Employee: %s, Dept: %s, Salary: %.2f", emp.getName(), emp.getDepartment(), emp.getSalary()); // Java 21 String Template — STR processor (preview) String msg3 = STR."Employee: \{emp.getName()}, Dept: \{emp.getDepartment()}"; // Expressions inside \{ } — any valid Java expression String report = STR.""" === Employee Report === Name : \{emp.getName().toUpperCase()} Department : \{emp.getDepartment()} Salary : ₹\{String.format("%.2f", emp.getSalary())} Senior : \{emp.getSalary() > 100000 ? "Yes" : "No"} """; // FMT processor — format specifiers like printf String fmt = FMT."Salary: ₹%,.2f\{emp.getSalary()}";
💡 String Templates are safer than String.format because the template processor controls how embedded expressions are evaluated — preventing injection issues when building SQL or HTML.

6. Unnamed Patterns & Variables (JEP 443)

Use _ (underscore) to discard unused pattern variables or local variables — signals intent and reduces noise.

// Unnamed pattern variable — ignore salary component if (obj instanceof EmployeeDTO(String name, String dept, _)) { System.out.println(name + " is in " + dept); } // Unnamed variable in catch — we don't need the exception try { processEmployee(emp); } catch (IOException _) { log.warn("Processing failed — retrying"); } // Unnamed variable in for-each — iterate for side effects int count = 0; for (Employee _ : employees) count++; // just counting, don't need the element // Multiple _ allowed (unlike Java identifiers — each is independent) switch (role) { case Manager(_, int teamSize) when teamSize > 5 -> { ... } case Manager(_, _) -> { ... } default -> { ... } }

Java 8 → 11 → 17 → 21 at a Glance

VersionLTS?Key Features
Java 8 (2014)✅ LTSLambdas, Stream API, Optional, java.time, Default methods
Java 11 (2018)✅ LTSvar in lambda, HttpClient, String.isBlank/strip/lines, Files.readString
Java 17 (2021)✅ LTSRecords, Sealed classes, Pattern matching instanceof, Switch expressions, Text blocks
Java 21 (2023)✅ LTSVirtual Threads, Record Patterns, Pattern switch, Sequenced Collections, String Templates (preview)
💡 Interview cheat-sheet: Virtual Threads = Project Loom — millions of cheap I/O threads. Record patterns = destructure records in patterns. Pattern switch = type-safe switch over sealed hierarchies with guard clauses (when). Sequenced Collections = uniform getFirst()/getLast()/reversed() on any ordered collection.

Serialization

☕ Java · Advanced · ★ Interview Topic · 8 min read

Serialization converts an object's state into a byte stream (for storage or network transfer); deserialization reconstructs it. A class opts in by implementing the marker interface Serializable.

Object ↔ Bytes — Visual

Object (heap) byte stream Object (restored) serialize deserialize
class User implements Serializable { private static final long serialVersionUID = 1L; // version control private String name; private transient String password; // transient = NOT serialized } // write try (var out = new ObjectOutputStream(new FileOutputStream("u.ser"))) { out.writeObject(user); }

Key Points

  • transient fields are skipped (passwords, caches, derived data).
  • serialVersionUID guards version compatibility — set it explicitly.
  • static fields aren't serialized (they belong to the class).
⚠️ Native Java serialization is fragile and a security risk (deserializing untrusted data can execute code). In practice, prefer JSON (Jackson) or protobuf for APIs and persistence.

Deserialization

☕ Java · Advanced · 4 min read

Deserialization rebuilds an object from its byte stream. The JVM allocates the object and restores non-transient fields — without calling the constructor.

try (var in = new ObjectInputStream(new FileInputStream("u.ser"))) { User user = (User) in.readObject(); } // transient fields come back as defaults (null/0)
⚠️ Never deserialize untrusted input — it's a classic remote-code-execution vector (gadget chains). Validate, use allow-lists (ObjectInputFilter), or avoid Java serialization entirely for external data.

Reflection API

☕ Java · Advanced · ★ Interview Topic · 8 min read

Reflection lets a program inspect and manipulate classes, methods, and fields at runtime — even private ones. It's the engine behind frameworks like Spring, Hibernate, and JUnit.

Class<?> clazz = Class.forName("com.example.User"); Object obj = clazz.getDeclaredConstructor().newInstance(); for (Field f : clazz.getDeclaredFields()) { f.setAccessible(true); // bypass private System.out.println(f.getName() + " = " + f.get(obj)); } Method m = clazz.getMethod("getName"); Object name = m.invoke(obj); // read annotations at runtime if (clazz.isAnnotationPresent(Entity.class)) { ... }

Where It's Used

  • DI containers (Spring) — instantiate beans, inject fields.
  • ORMs (Hibernate) — map fields to columns.
  • Serialization libraries (Jackson) — read/write fields.
  • Testing (JUnit) — discover & invoke test methods.
⚠️ Reflection is powerful but: slower than direct calls, breaks compile-time safety, and can violate encapsulation. Use it for frameworks/tools — not everyday app logic.

Annotations

☕ Java · Advanced · 5 min read

Annotations are metadata attached to code (classes, methods, fields) — read by the compiler or at runtime (via reflection) to drive behaviour without changing logic.

Built-inPurpose
@OverrideCompiler checks it overrides
@DeprecatedMarks obsolete API
@FunctionalInterfaceEnforces single abstract method
@SuppressWarningsSilence specific warnings
// custom annotation, readable at runtime @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Audit { String value() default ""; } @Audit("delete-user") public void deleteUser(long id) { ... }
💡 @Retention(RUNTIME) makes an annotation visible to reflection — that's how Spring/JUnit read your @Autowired, @Test, etc. SOURCE retention is compile-only; CLASS is in bytecode but not at runtime.

Multithreading

☕ Java · Concurrency · ★ Interview Topic · 9 min read

Multithreading runs multiple threads (lightweight units of execution) within one process, sharing memory — for parallelism (use multiple cores) and responsiveness (don't block on I/O).

Creating Threads

// 1. Runnable (preferred — doesn't tie up the single inheritance slot) Thread t = new Thread(() -> System.out.println("running")); t.start(); // start() spawns a new thread; run() would NOT // 2. extends Thread class Worker extends Thread { public void run() { ... } } // 3. ExecutorService (best for real apps) ExecutorService pool = Executors.newFixedThreadPool(4); pool.submit(() -> doWork());

Process vs Thread

ProcessThread
MemoryIsolatedShared (heap)
Creation costHighLow
CommunicationIPCShared variables
⚠️ start() creates a new thread and calls run() on it; calling run() directly just runs it on the current thread (no concurrency) — a classic interview trap. Shared mutable state needs synchronization (next topics). Java 21 adds lightweight virtual threads.

Thread Lifecycle

☕ Java · Concurrency · 5 min read

A thread moves through six states (the Thread.State enum).

NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED
StateMeaning
NEWCreated, not started
RUNNABLERunning or ready to run
BLOCKEDWaiting for a monitor lock
WAITINGWaiting indefinitely (wait/join)
TIMED_WAITINGWaiting with timeout (sleep)
TERMINATEDFinished
💡 The scheduler moves threads between RUNNABLE and the waiting states; you don't control exact timing. wait() releases the lock; sleep() does not.

Runnable vs Callable

☕ Java · Concurrency · 4 min read

Both represent a task for a thread, but Callable can return a result and throw checked exceptions.

RunnableCallable<V>
Methodvoid run()V call() throws Exception
ReturnsNothingA value (via Future)
Checked exceptionsNoYes
Callable<Integer> task = () -> { return compute(); }; Future<Integer> future = pool.submit(task); Integer result = future.get(); // blocks until done
💡 Use Callable when you need a result or might throw; Runnable for fire-and-forget. Both are submitted to an ExecutorService; Callable returns a Future.

Executor Framework

☕ Java · Concurrency · ★ Interview Topic · 9 min read

The Executor framework (java.util.concurrent) decouples task submission from task execution, managing a pool of reusable threads — so you don't manually create threads per task (expensive and unbounded).

Submit → Pool → Future — Visual

tasks Queuepending tasks Thread Poolreused worker threads Future<result>
ExecutorService pool = Executors.newFixedThreadPool(4); Future<Integer> f = pool.submit(() -> compute()); Integer result = f.get(); pool.shutdown(); // graceful; awaitTermination(...) // scheduled work var sched = Executors.newScheduledThreadPool(2); sched.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
💡 Always use a thread pool, not new Thread() per task — pooling caps concurrency and reuses threads. Always shutdown(). Java 21 virtual threads (newVirtualThreadPerTaskExecutor) make per-task threads cheap again.

Thread Pools

☕ Java · Concurrency · 5 min read

A thread pool reuses a fixed/managed set of worker threads to run many tasks — bounding resource use and avoiding thread-creation overhead.

FactoryBehaviour
newFixedThreadPool(n)n threads, unbounded queue
newCachedThreadPool()Grows/shrinks on demand
newSingleThreadExecutor()One thread, sequential
newScheduledThreadPool(n)Delayed/periodic tasks
newVirtualThreadPerTaskExecutor()Java 21+ virtual threads
⚠️ In production prefer a custom ThreadPoolExecutor with a bounded queue + rejection policy — the convenience factories use unbounded queues that can OOM under load. Size CPU-bound pools to ~#cores; I/O-bound pools larger.

Synchronization

☕ Java · Concurrency · ★ Interview Topic · 9 min read

Synchronization controls access to shared mutable state so only one thread enters a critical section at a time — preventing race conditions. Java's built-in mechanism is the monitor lock via synchronized.

The Problem It Solves

// NOT thread-safe — count++ is read-modify-write (3 steps) class Counter { int count; void inc() { count++; } } // lost updates! // Thread-safe with synchronized class Counter { private int count; public synchronized void inc() { count++; } // one thread at a time public synchronized int get() { return count; } }

Forms of synchronized

synchronized void m() { ... } // locks "this" static synchronized void s() { ... } // locks the Class object void m() { synchronized (lockObj) { ... } // lock a specific object (finer) }

What It Guarantees

  • Mutual exclusion — only one thread holds the lock.
  • Visibility — changes made under the lock are visible to the next thread acquiring it (happens-before).
💡 Keep critical sections small (lock only what's needed). Prefer a private final lock object over synchronized(this). For higher-level control (tryLock, fairness, read/write), use ReentrantLock. For simple flags, volatile or atomics may suffice.

Volatile Keyword

☕ Java · Concurrency · ★ Interview Topic · 7 min read

volatile guarantees visibility: a write by one thread is immediately seen by all others (no thread-local caching), and it prevents instruction reordering around the variable. It does not provide atomicity for compound operations.

Visibility — Visual

Non-volatile: threads may cache stale copies T1 cache: flag=true T2 cache: flag=false ✗ volatile: always read/write main memory all threads see the latest value immediately
private volatile boolean running = true; // flag visible across threads public void stop() { running = false; } public void run() { while (running) { ... } } // sees the change // volatile does NOT make this atomic: volatile int count; count++; // still a race! use AtomicInteger or synchronized
💡 Use volatile for simple flags and the double-checked-locking singleton. For counters/compound updates use Atomic* classes or synchronized. Rule: volatile = visibility only; synchronized/atomic = visibility + atomicity.

Atomic Variables

☕ Java · Concurrency · 5 min read

Atomic classes (AtomicInteger, AtomicLong, AtomicReference) provide lock-free, thread-safe operations using CPU compare-and-swap (CAS) — faster than synchronization for simple counters/flags.

AtomicInteger counter = new AtomicInteger(0); counter.incrementAndGet(); // atomic ++ , no lock counter.addAndGet(5); counter.compareAndSet(10, 20); // CAS: set to 20 only if currently 10 AtomicReference<Node> head = new AtomicReference<>(); head.updateAndGet(h -> new Node(h)); // lock-free stack push
💡 CAS retries in a loop until it succeeds — no blocking. Great under low-to-moderate contention. Under very high contention, LongAdder scales better than AtomicLong.

Locks & ReentrantLock

☕ Java · Concurrency · 5 min read

ReentrantLock (java.util.concurrent.locks) is a more flexible alternative to synchronized — with try-lock, timed lock, interruptible lock, and fairness.

ReentrantLock lock = new ReentrantLock(); lock.lock(); try { /* critical section */ } finally { lock.unlock(); } // ALWAYS unlock in finally if (lock.tryLock(1, TimeUnit.SECONDS)) { ... } // avoid deadlock // ReadWriteLock — many readers OR one writer var rw = new ReentrantReadWriteLock(); rw.readLock().lock(); // concurrent reads
synchronizedReentrantLock
Try / timed lockNoYes
FairnessNoOptional
UnlockAutomaticManual (finally)
⚠️ Always unlock() in a finally block — forgetting it leaves the lock held forever (deadlock). Use synchronized when you don't need the extra features; it's simpler and auto-releases.

Deadlock

☕ Java · Concurrency · ★ Interview Topic · 8 min read

A deadlock occurs when two or more threads are each waiting for a lock the other holds — so none can proceed, forever.

The Classic Cycle — Visual

Thread 1holds A, wants B Thread 2holds B, wants A Lock A Lock B holds wants holds wants

Four Coffman Conditions (all must hold)

Mutual exclusion · Hold-and-wait · No preemption · Circular wait. Break any one to prevent deadlock.

Prevention

  • Lock ordering — always acquire multiple locks in the same global order (breaks circular wait).
  • tryLock with timeout — back off and retry instead of waiting forever.
  • Minimize lock scope; avoid nested locks; prefer higher-level concurrency utilities.
💡 Detect with a thread dump (jstack) — the JVM reports "Found one Java-level deadlock" and the cycle. The most reliable fix is consistent lock ordering.

Race Conditions

☕ Java · Concurrency · 4 min read

A race condition occurs when the correctness of a program depends on the timing/interleaving of threads accessing shared data — producing nondeterministic, hard-to-reproduce bugs.

// race: two threads run count++ → lost update if (balance >= amount) { // check balance -= amount; // act — another thread may have changed balance between }

This "check-then-act" is a classic race. Fixes: synchronize the whole compound action, use atomics, or use a database transaction.

💡 Race conditions stem from unsynchronized access to shared mutable state. The cures are the same toolkit: synchronized, Atomic*, locks, immutability, or confining state to one thread.

Concurrent Collections

☕ Java · Concurrency · 5 min read

Thread-safe collections in java.util.concurrent — built for concurrent access without the global locking of Collections.synchronizedXxx.

CollectionUse
ConcurrentHashMapConcurrent map (bin-level locking)
CopyOnWriteArrayListRead-heavy lists (copies on write)
ConcurrentLinkedQueueLock-free FIFO queue
BlockingQueue (ArrayBlockingQueue…)Producer-consumer (blocks when full/empty)
ConcurrentSkipListMapConcurrent sorted map
💡 Prefer these over synchronized wrappers — they scale far better and their iterators are weakly consistent (no ConcurrentModificationException). BlockingQueue is the backbone of the producer-consumer pattern.

CompletableFuture

☕ Java · Concurrency · ★ Interview Topic · 9 min read

CompletableFuture (Java 8) enables asynchronous, non-blocking programming — composing chains of async tasks with callbacks, without manually blocking on Future.get().

Chaining Async Tasks — Visual

supplyAsyncfetch user thenApplytransform thenComposefetch orders thenAcceptuse result
CompletableFuture .supplyAsync(() -> fetchUser(id)) // async, returns a value .thenApply(user -> user.getName()) // transform result .thenCompose(name -> fetchOrdersAsync(name)) // chain another future .thenAccept(System.out::println) // consume .exceptionally(ex -> { log(ex); return null; }); // handle errors // run several in parallel and combine CompletableFuture.allOf(f1, f2, f3).join(); f1.thenCombine(f2, (a, b) -> a + b);
MethodUse
thenApplyTransform result (sync fn)
thenComposeChain another future (flatMap)
thenCombineCombine two futures
allOf / anyOfWait for all / any
exceptionally / handleError handling
💡 thenApply vs thenCompose: use thenApply for a plain value, thenCompose when the function itself returns a CompletableFuture (avoids nested futures) — the async analogue of map vs flatMap.

Fork Join Framework

☕ Java · Concurrency · 5 min read

The Fork/Join framework (Java 7) parallelizes divide-and-conquer tasks by recursively splitting work (fork) and combining results (join), using a work-stealing thread pool.

class SumTask extends RecursiveTask<Long> { int lo, hi; int[] a; protected Long compute() { if (hi - lo <= THRESHOLD) return sequentialSum(); int mid = (lo + hi) / 2; var left = new SumTask(a, lo, mid); left.fork(); // async split var right = new SumTask(a, mid, hi); return right.compute() + left.join(); // combine } } new ForkJoinPool().invoke(new SumTask(arr, 0, arr.length));
💡 Idle threads "steal" tasks from busy threads' queues — keeping all cores busy. Parallel streams use the common ForkJoinPool under the hood. Best for CPU-bound, splittable work.

Producer Consumer Problem

☕ Java · Concurrency · 5 min read

A classic coordination problem: producers generate items, consumers process them, sharing a bounded buffer. The clean Java solution uses a BlockingQueue, which handles all the waiting/signaling.

BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100); // producer — blocks if full queue.put(task); // consumer — blocks if empty Task t = queue.take();

The older approach uses wait()/notify() in a synchronized block — error-prone. BlockingQueue encapsulates it correctly.

💡 Interview answer: "Use a BlockingQueueput() blocks when full, take() blocks when empty, so producers and consumers self-throttle with no manual locking or wait/notify."

SOLID Principles

☕ Java · Design · ★ Interview Topic · 10 min read

SOLID is five object-oriented design principles for code that's maintainable, extensible, and testable.

SSingle Responsibility OOpen/Closed LLiskov Substitution IInterface Segregation DDependency Inversion
PrincipleMeaning
Single ResponsibilityA class should have one reason to change (one job).
Open/ClosedOpen for extension, closed for modification (add behaviour via new code, not edits).
Liskov SubstitutionSubtypes must be usable wherever the base type is, without breaking behaviour.
Interface SegregationMany small, focused interfaces beat one fat interface.
Dependency InversionDepend on abstractions, not concrete classes (enables DI).
// Dependency Inversion + Open/Closed interface Payment { void pay(double amt); } // abstraction class Order { private final Payment payment; // depend on interface Order(Payment p) { this.payment = p; } // injected } // add UpiPayment, CardPayment without touching Order
💡 SOLID underpins clean architecture and is exactly what Spring's dependency injection enables (D). Interviewers love a concrete before/after refactor example for each letter.

Design Patterns

☕ Java · Design · ★ Interview Topic · 9 min read

Design patterns are proven, reusable solutions to recurring design problems (from the "Gang of Four"). Three categories:

CategorySolvesExamples
CreationalObject creationSingleton, Factory, Builder, Prototype
StructuralObject compositionAdapter, Decorator, Proxy, Facade
BehaviouralObject interactionObserver, Strategy, Command, Template

Patterns You See in Java/Spring

  • Singleton — Spring beans (default scope).
  • FactoryBeanFactory, Calendar.getInstance().
  • BuilderStringBuilder, Stream.Builder, Lombok @Builder.
  • Proxy — Spring AOP, @Transactional.
  • Strategy — pluggable algorithms via interfaces (Comparator!).
  • Observer — event listeners, ApplicationEvent.
  • DecoratorBufferedReader(new FileReader(...)).
💡 Don't force patterns — apply them when a real problem calls for one. The detailed pattern pages (Singleton, Factory, Builder, Observer) follow. Strategy is just "program to an interface" — you already use it with Comparator.

Singleton Pattern

☕ Java · Design · 5 min read

Ensures a class has exactly one instance with a global access point (config, logging, connection pools).

// Thread-safe lazy singleton (double-checked locking) public class Config { private static volatile Config instance; // volatile is essential private Config() {} public static Config getInstance() { if (instance == null) { synchronized (Config.class) { if (instance == null) instance = new Config(); } } return instance; } } // Best & simplest — enum singleton (thread-safe, serialization-safe) public enum Config { INSTANCE; }
💡 The enum singleton is the cleanest, recommended approach (Effective Java) — inherently thread-safe and immune to reflection/serialization attacks. Eager init (static final) is also simpler when lazy loading isn't needed.

Factory Pattern

☕ Java · Design · 4 min read

Encapsulates object creation behind a method, so callers ask for what they want without knowing which concrete class is instantiated.

interface Notification { void send(String msg); } class EmailNotification implements Notification { ... } class SmsNotification implements Notification { ... } class NotificationFactory { static Notification create(String type) { return switch (type) { case "email" -> new EmailNotification(); case "sms" -> new SmsNotification(); default -> throw new IllegalArgumentException(type); }; } }
💡 Decouples client code from concrete classes (supports Open/Closed & Dependency Inversion). Seen everywhere: Calendar.getInstance(), LoggerFactory.getLogger(), Spring's BeanFactory.

Builder Pattern

☕ Java · Design · 4 min read

Constructs complex objects step by step with a fluent API — avoiding telescoping constructors and making optional parameters readable.

User user = User.builder() .name("Aftab") .email("a@x.com") .age(30) // optional, in any order .build(); // skeleton class User { static Builder builder() { return new Builder(); } static class Builder { private String name, email; Builder name(String n) { this.name = n; return this; } // chainable User build() { return new User(this); } } }
💡 Ideal for objects with many optional fields. Lombok's @Builder generates all this. Pairs well with immutability — build once, never mutate.

Observer Pattern

☕ Java · Design · 4 min read

Defines a one-to-many dependency: when one object (subject) changes state, all its dependents (observers) are notified automatically — the basis of event systems.

interface Observer { void update(String event); } class Subject { private final List<Observer> observers = new ArrayList<>(); void subscribe(Observer o) { observers.add(o); } void notifyAll(String event) { observers.forEach(o -> o.update(event)); // push to all } }
💡 Everywhere in practice: UI event listeners, Spring ApplicationEvent/@EventListener, reactive streams, pub/sub messaging. Decouples the publisher from its subscribers.

Java Security Basics

☕ Java · Best Practices · 5 min read

Core practices for writing secure Java applications.

  • Input validation — never trust external input; validate & sanitize.
  • SQL injection — use PreparedStatement/JPA parameters, never string-concatenated SQL.
  • Password storage — hash with bcrypt/Argon2 (never plain text or fast hashes).
  • Cryptography — use javax.crypto (AES-GCM), SecureRandom for tokens.
  • Deserialization — never deserialize untrusted data (RCE risk).
  • Dependencies — scan for CVEs (OWASP Dependency-Check); patch promptly.
  • Least privilege — minimal permissions, encapsulate sensitive state.
// SQL injection — safe parameterized query var ps = conn.prepareStatement("SELECT * FROM users WHERE email = ?"); ps.setString(1, email); // never string concatenation
💡 Follow the OWASP Top 10. In Spring apps, lean on Spring Security for authn/authz, BCrypt for passwords, and parameterized queries via JPA/JDBC to stop injection.