★ 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.
💡 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
Type
Size
Example
byte
8-bit
-128 to 127
short
16-bit
±32k
int
32-bit
default integer
long
64-bit
100L
float
32-bit
3.14f
double
64-bit
default decimal
char
16-bit
'A' (Unicode)
boolean
1-bit
true/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.
Type
Declared
Lifetime
Local
Inside a method/block
While the block runs
Instance
In a class, non-static
Per object
Static (class)
In a class with static
One 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
Category
Operators
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.
💡 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.
Contains
Use
JVM
Bytecode executor
Runs the program
JRE
JVM + libraries
Run Java apps
JDK
JRE + 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.
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
The Areas
Area
Holds
Scope
Heap
Objects, arrays, instance fields
Shared
Metaspace
Class metadata, static fields
Shared (off-heap)
Stack
Method frames: locals, partial results, refs
Per thread
PC Register
Address of current instruction
Per thread
Native Stack
Native (JNI) calls
Per 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.
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
Comparison
Stack
Heap
Stores
Primitives, references, method frames
Objects, arrays
Scope
Per thread
Shared by all threads
Lifetime
Until method returns
Until GC reclaims
Speed
Very fast (LIFO)
Slower (GC overhead)
Error
StackOverflowError
OutOfMemoryError
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 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
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.
Collector
Best for
Flag
Serial
Small heaps, single CPU
-XX:+UseSerialGC
Parallel (Throughput)
Batch jobs, max throughput
-XX:+UseParallelGC
G1 (default since 9)
Balanced, large heaps
-XX:+UseG1GC
ZGC
Ultra-low pause (<1ms), huge heaps
-XX:+UseZGC
Shenandoah
Low 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 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
Three Phases
Loading — find & read the .class, create a Class object.
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.
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).
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 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.
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
String
StringBuilder
StringBuffer
Mutable
No (immutable)
Yes
Yes
Thread-safe
Yes (immutable)
No
Yes (synchronized)
Performance
Slow for edits
Fastest
Slower (locking)
Since
1.0
1.5
1.0
Why Immutability Hurts in Loops — Visual
// 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
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:
Type
Mechanism
Resolved
Compile-time (static)
Method overloading
At compile time
Runtime (dynamic)
Method overriding
At 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 class
interface
Methods
abstract + concrete
abstract + default/static (8+)
Fields
any
public static final only
Inheritance
single (extends)
multiple (implements)
Constructor
Yes
No
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
Overloading
Overriding
Where
Same class
Sub vs super class
Signature
Different params
Same signature
Bound
Compile time
Runtime
💡 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 to
Effect
Variable
Constant — assign once
Method
Cannot be overridden
Class
Cannot 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.
Modifier
Same class
Same package
Subclass
Everywhere
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):
Method
Purpose
equals(Object)
Logical equality (override with hashCode)
hashCode()
Hash bucket value
toString()
String representation
getClass()
Runtime class
clone()
Field-copy (needs Cloneable)
wait/notify/notifyAll
Thread coordination
💡 Override toString() for readable logs and always override equals() and hashCode() together (next topic).
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()
Compares
References (same object?)
Logical equality (same value?)
For objects
Identity
Content (if overridden)
For primitives
Value
N/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).
@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.
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
Make the class final (can't be subclassed to add mutability).
Make all fields private final.
No setters; set everything in the constructor.
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 constructornew 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 — 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.
Exceptions signal abnormal conditions, separating error handling from normal logic. Java uses try-catch-finally and a typed exception hierarchy rooted at Throwable.
💡 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
💡 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
💡 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.
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
Pick the Right Collection
Need
Use
Ordered, indexed, duplicates
ArrayList
Frequent insert/delete at ends
LinkedList / ArrayDeque
Unique elements, fast lookup
HashSet
Unique & sorted
TreeSet
Key→value lookup
HashMap
Sorted by key
TreeMap
Insertion-order map
LinkedHashMap
Thread-safe map
ConcurrentHashMap
💡 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.
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).
Operation
Complexity
get(i) / set(i)
O(1)
add (end, amortized)
O(1)
add/remove (middle)
O(n) — shifts elements
contains / indexOf
O(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.
ArrayList
LinkedList
get(i)
O(1)
O(n)
add/remove ends
O(1)/O(n)
O(1)
Memory
Compact
Extra 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.
Implementation
Order
Backed by
HashSet
None
HashMap
LinkedHashSet
Insertion order
LinkedHashMap
TreeSet
Sorted
Red-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'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(); }
💡 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 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
put(k, v) Step by Step
Compute hash = key.hashCode() (then spread the bits).
Bucket index = hash & (capacity - 1).
Empty bucket → store the entry.
Collision → walk the chain; if a key equals() exists, replace its value; else append.
Chain length ≥ 8 (and capacity ≥ 64) → convert chain to a red-black tree.
⚠️ 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).
💡 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.
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
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.
💡 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.
Both define ordering, but differently. Comparable gives a class its natural ordering (one, built-in). Comparator is an external, swappable ordering (many possible).
Comparison
Comparable
Comparator
Method
compareTo(o)
compare(a, b)
Package
java.lang
java.util
Where
Inside the class
Separate object/lambda
Orderings
One (natural)
Many
Modifies class?
Yes
No
// 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.
💡 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 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
Syntax
Meaning
<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:
Type
Notes
Static nested
No outer instance ref; a namespaced helper
Inner (non-static)
Holds an implicit outer reference
Local
Defined inside a method
Anonymous
One-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.
@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.
Lambdas (Java 8) are anonymous functions — concise implementations of functional interfaces. They enable functional-style programming and power the Stream API.
Syntax Evolution — Visual
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.
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.
💡 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<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.
💡 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.
Class
Represents
LocalDate
Date (no time)
LocalTime
Time (no date)
LocalDateTime
Date + time (no zone)
ZonedDateTime
Date + time + zone
Instant
Machine timestamp (epoch)
Duration / Period
Time / 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.
💡 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.
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?
collect, forEach, count, reduce, findFirst, anyMatch, min, max
Pipeline is built but not executed
Triggers 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()
Input
Stream<T>
Stream<Stream<T>>
Output
Stream<R> (one element per input)
Stream<R> (flattened — many elements per input)
Use when
1:1 transformation
1: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
// 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()
Result
Always returns first element in encounter order
Returns any element — non-deterministic in parallel streams
Parallel
Forces coordination — slower
Returns whichever thread finds one first — faster
Sequential
Same 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
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.
Q22. What are Method References? Show the four types.
Type
Syntax
Example
Static method
ClassName::staticMethod
Math::abs
Instance method of a particular object
instance::method
System.out::println
Instance method of an arbitrary object
ClassName::instanceMethod
Employee::getName
Constructor
ClassName::new
Employee::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?
Comparable
Comparator
Package
java.lang
java.util
Method
compareTo(T o)
compare(T o1, T o2)
Sort sequences
Natural / single
Multiple, external, flexible
Modifies class?
Yes — class must implement it
No — 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().
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 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 methods — isBlank(), strip(), lines(), repeat(n).
HttpClient — modern, async HTTP client (no more HttpURLConnection).
Run source directly — java 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 (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 (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 Thread
Virtual Thread
Backed by
OS thread (1:1)
Carrier thread (M:N)
Stack size
~1 MB
~few KB (grows on demand)
Max practical
~5,000–10,000
Millions
Best for
CPU-bound tasks
I/O-bound tasks (blocking DB, HTTP)
Creation cost
High
Near 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).
💡 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 -> { ... }
}
💡 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 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
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 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.
⚠️ 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-in
Purpose
@Override
Compiler checks it overrides
@Deprecated
Marks obsolete API
@FunctionalInterface
Enforces single abstract method
@SuppressWarnings
Silence 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 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
Process
Thread
Memory
Isolated
Shared (heap)
Creation cost
High
Low
Communication
IPC
Shared 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).
State
Meaning
NEW
Created, not started
RUNNABLE
Running or ready to run
BLOCKED
Waiting for a monitor lock
WAITING
Waiting indefinitely (wait/join)
TIMED_WAITING
Waiting with timeout (sleep)
TERMINATED
Finished
💡 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.
💡 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.
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
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.
Factory
Behaviour
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 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 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
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
synchronized
ReentrantLock
Try / timed lock
No
Yes
Fairness
No
Optional
Unlock
Automatic
Manual (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.
💡 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.
Collection
Use
ConcurrentHashMap
Concurrent map (bin-level locking)
CopyOnWriteArrayList
Read-heavy lists (copies on write)
ConcurrentLinkedQueue
Lock-free FIFO queue
BlockingQueue (ArrayBlockingQueue…)
Producer-consumer (blocks when full/empty)
ConcurrentSkipListMap
Concurrent 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 8) enables asynchronous, non-blocking programming — composing chains of async tasks with callbacks, without manually blocking on Future.get().
Chaining Async Tasks — Visual
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);
Method
Use
thenApply
Transform result (sync fn)
thenCompose
Chain another future (flatMap)
thenCombine
Combine two futures
allOf / anyOf
Wait for all / any
exceptionally / handle
Error 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 BlockingQueue — put() 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.
Principle
Meaning
Single Responsibility
A class should have one reason to change (one job).
Open/Closed
Open for extension, closed for modification (add behaviour via new code, not edits).
Liskov Substitution
Subtypes must be usable wherever the base type is, without breaking behaviour.
Interface Segregation
Many small, focused interfaces beat one fat interface.
Dependency Inversion
Depend 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:
Strategy — pluggable algorithms via interfaces (Comparator!).
Observer — event listeners, ApplicationEvent.
Decorator — BufferedReader(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.