0% found this document useful (0 votes)
3 views61 pages

Java Interview Notes Handbook-1

This document is a comprehensive handbook designed for Java developers preparing for interviews and placements, covering essential topics such as Reflection, Generics, Lambda expressions, and the Stream API. It includes definitions, code examples, interview questions, and best practices, aimed at enhancing understanding and readiness for technical interviews. The content is structured for effective learning and revision, with specific sections for quick reference and preparation strategies.

Uploaded by

mihir304singh
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views61 pages

Java Interview Notes Handbook-1

This document is a comprehensive handbook designed for Java developers preparing for interviews and placements, covering essential topics such as Reflection, Generics, Lambda expressions, and the Stream API. It includes definitions, code examples, interview questions, and best practices, aimed at enhancing understanding and readiness for technical interviews. The content is structured for effective learning and revision, with specific sections for quick reference and preparation strategies.

Uploaded by

mihir304singh
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

JAVA INTERVIEW

COMPLETE NOTES
HANDBOOK
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

Reflection • Generics • Lambda • Functional Interfaces


Predicate • Consumer • Supplier • Function • Method Reference
Stream API • Optional • CompletableFuture
HashMap Internals • Comparable vs Comparator
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

Designed for Java Developer & Backend Developer Placements


Internship & Placement Preparation | July 2026
Includes: Definitions • Code Examples • Interview Q&A; • Traps • Revision Guide

Java Interview Notes • Page 1 • Mihir – Placement Prep 2026


TABLE OF CONTENTS

1 Reflection Class inspection and manipulation at runtime. Used by


Spring/Hibernate.

2 Generics Type-safe code. Type erasure, wildcards, PECS principle.

3 Lambda Expressions Concise anonymous functions. Functional programming basics.

4 Functional Interfaces @FunctionalInterface, built-in and custom interfaces.

5 Predicate Boolean-valued functions. test(), and(), or(), negate().

6 Consumer Consumes values, returns nothing. accept(), andThen().

7 Supplier Supplies values, takes nothing. get().

8 Function<T,R> Transforms T to R. apply(), andThen(), compose().

9 Method References Shorthand for lambdas. Static, instance, constructor references.

10 Stream API Pipeline processing. map, filter, reduce, collect, parallel streams.

11 Optional<T> Null-safe wrapper. Eliminates NullPointerException.

12 CompletableFuture Async programming in Java 8. supplyAsync, thenApply,


exceptionally.

13 HashMap Internal Working Hashing, buckets, collision, load factor, Java 7 vs Java 8.

14 Comparable vs Comparator Natural vs custom sorting. compareTo(), compare(), PECS in


sorting.

15 Complete Interview Revision One-liners, comparison tables, 1-day & 3-day revision plan.
Guide

HOW TO USE THIS HANDBOOK


Learning Mode → Read Why + Definition + Internal Working + Examples. Then try writing code yourself.
Revision Mode → Jump to Revision Notes at end of each topic.
Interview Prep → Read Interview Questions, Traps, and the Final Revision Guide.
1-Day Prep → See the 1-Day Revision Plan in the last section.

Java Interview Notes • Page 2 • Mihir – Placement Prep 2026


TOPIC 01

Reflection
Inspect and manipulate class structure at runtime – the backbone of Spring and Hibernate

1. Why Was Reflection Introduced?


THE PROBLEM BEFORE REFLECTION
Before Reflection, Java was purely compile-time rigid. You could only call methods and access fields that you
KNEW about at compile time.
Frameworks like Spring and Hibernate could NOT exist. They need to: (a) Scan classes and detect annotations,
(b) Wire dependencies automatically, (c) Read class metadata without knowing exact types in advance.
Without Reflection: Every framework would need you to register every bean manually. Configuration would be
10x more verbose.

Java introduced Reflection so that code can inspect, create, and call other code at runtime without knowing it at
compile time. This is the foundation for dependency injection, ORM mapping, serialization, and test frameworks.

2. Definition
SIMPLE EXPLANATION
Reflection = Holding a mirror up to your Java class at runtime. You can see: What fields does this class have?
What methods? What constructors? What annotations?
And then act on that info: call a method, set a field, create an instance — all without knowing the class name at
compile time.

FORMAL INTERVIEW DEFINITION


Reflection is a feature in the [Link] package that allows a running Java program to examine or modify
its own structure and behavior. It gives access to class metadata (fields, methods, constructors, annotations) at
runtime and allows invoking methods, creating instances, and accessing/modifying fields — even private ones
— using setAccessible(true).

3. Key Classes in [Link]


Class What It Represents Key Methods

Class<T> The class itself — metadata about a getDeclaredMethods(), getDeclaredFields(),


type getDeclaredConstructors(), getAnnotations()

Method A single method of a class invoke(obj, args), getName(), getParameterTypes(),


getReturnType()

Field A single field (variable) of a class get(obj), set(obj, value), getName(), getType()

Constructor<T> A constructor of a class newInstance(args), getParameterTypes()

4. Internal Working – Step by Step


How does reflection actually work under the hood?
1. JVM loads the .class file and creates a Class object in the Method Area (part of JVM memory)

Java Interview Notes • Page 3 • Mihir – Placement Prep 2026


2. This Class object contains all metadata: field names, types, method signatures, access modifiers,
annotations
3. When you call [Link]("[Link]") or [Link], you get a reference to this Class object
4. From the Class object, you extract Method, Field, or Constructor objects
5. setAccessible(true) bypasses Java access control — allows access to private members
6. invoke(obj, args) on a Method object executes that method on the given object instance

// Reflection Flow
// REFLECTION FLOW DIAGRAM (ASCII)
YourCode
|
v
[Link]("[Link]") <---- Class Object in JVM Method Area
|
+-- getDeclaredFields() ------> Field[] (name, type, modifiers)
|
+-- getDeclaredMethods() ------> Method[] (name, params, return type)
|
+-- getDeclaredConstructors() -> Constructor[]
|
v
[Link](true)
[Link](instanceObject, args)
|
v
Result returned to your code

5. Syntax & Usage


// Core Reflection Syntax
// Step 1 – Get the Class object
Class<?> clazz = [Link]("[Link]");
// OR
Class<Employee> clazz = [Link];
// OR
Class<?> clazz = [Link]();
// Step 2 – Get declared fields (even private)
Field[] fields = [Link]();
for (Field f : fields) {
[Link](true); // unlock private access
[Link]([Link]() + " = " + [Link](empInstance));
}
// Step 3 – Get and invoke a method
Method method = [Link]("getSalary");
[Link](true);
Object result = [Link](empInstance);
// Step 4 – Create new instance via Constructor
Constructor<?> con = [Link]([Link], [Link]);
Object emp = [Link]("Alice", 30);

6. Simple Example

Java Interview Notes • Page 4 • Mihir – Placement Prep 2026


// Simple Reflection Example
public class Person {
private String name = "Alice";
private int age = 25;
}
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Person p = new Person();
Class<?> c = [Link]();
for (Field f : [Link]()) {
[Link](true);
[Link]([Link]() + " : " + [Link](p));
}
// Output:
// name : Alice
// age : 25
}
}

7. Real World – How Spring Uses Reflection

// Spring DI via Reflection (Simplified)


// How Spring performs @Autowired injection (simplified)
// 1. Spring scans classpath, finds all @Component classes
// 2. For each class, it does:
Class<?> serviceClass = [Link]("[Link]");
// 3. Creates instance
Object instance = [Link]().newInstance();
// 4. Scans fields for @Autowired annotation
for (Field field : [Link]()) {
if ([Link]([Link])) {
[Link](true);
Object dependency = [Link]([Link]());
[Link](instance, dependency); // Inject the bean!
}
}
// Hibernate does similar: reads @Column, @Table annotations via Reflection
// to map Java fields to DB columns automatically.

COMMON MISTAKES
1. Forgetting setAccessible(true) on private fields — causes IllegalAccessException
2. Using reflection for regular coding — it is slow and breaks encapsulation
3. Not handling checked exceptions: ReflectiveOperationException, InvocationTargetException
4. Calling invoke() on wrong instance type — causes ClassCastException
5. Using reflection in performance-critical loops — it is 50-100x slower than direct calls

Java Interview Notes • Page 5 • Mihir – Placement Prep 2026


BEST PRACTICES
1. Use reflection only in framework/library code, not in regular application logic
2. Cache Method and Field objects — getting them via getDeclaredMethod() is expensive
3. Wrap invoke() in try-catch for InvocationTargetException specifically
4. Prefer constructor injection over field injection to avoid reflection overhead
5. Add SecurityManager checks in production to prevent unauthorized reflection access

8. Reflection Drawbacks
Drawback Why It Is a Problem Alternative

Performance JVM cannot optimize reflected calls; 50-100x Use direct calls or MethodHandles (Java 7+)
Overhead slower

Breaks Encapsulation setAccessible(true) bypasses private — Use proper APIs or redesign class
violates OOP

Security Risk Can access sensitive fields in third-party code Use SecurityManager or module system (Java 9+)

Compile-time Safety Errors appear only at runtime, not compile Use typed interfaces or code generation
Lost time

Maintenance Hard Refactoring class names breaks reflection Use annotations + APT for code generation
silently

9. Interview Questions

Q1. What is Reflection in Java?


Short: API to inspect and modify class structure at runtime using [Link] package.
Detail: Reflection allows a Java program to examine and manipulate its own structure at runtime. You can get
metadata about any class — fields, methods, constructors, annotations — and then invoke methods, create
instances, or read/write fields without knowing the class at compile time. Core classes: Class, Method, Field,
Constructor.
Follow-up Questions:
➤ Which package contains Reflection classes?
→ [Link] — Method, Field, Constructor. The Class class is in [Link] itself.
➤ Difference between getClass() and .class?
→ getClass() is called on an object instance at runtime. .class is a literal on the class name at compile time. Both
return Class<?>.

Q2. What does setAccessible(true) do? Is it safe?


Short: Bypasses Java access control, allowing access to private members. Not safe in all contexts.
Detail: By default, accessing a private field or method throws IllegalAccessException. setAccessible(true)
disables this check for that specific Field/Method object. It does NOT change the access modifier in the class
— it only bypasses the check for that reflection object. In Java 9+ with modules, setAccessible may throw
InaccessibleObjectException unless the module exports the package.
Follow-up Questions:
➤ Can setAccessible(true) be prevented?
→ Yes. Using a SecurityManager, you can restrict setAccessible calls. In Java 9+ module system, unexported
packages cannot be reflected by default.

Java Interview Notes • Page 6 • Mihir – Placement Prep 2026


Q3. How does Spring use Reflection?
Short: Spring uses Reflection to perform dependency injection, scan annotations, and create beans.
Detail: Spring ApplicationContext scans classpath for @Component, @Service, @Repository classes. It uses
[Link]() to load them, DeclaredConstructor().newInstance() to create instances, and then iterates
over DeclaredFields to find @Autowired. It calls setAccessible(true) on those fields and sets the dependent
beans. Similarly, @Transactional and AOP proxies wrap methods using reflection-based invocation.
Follow-up Questions:
➤ Why does Spring prefer constructor injection over field injection?
→ Constructor injection does not need reflection (or needs it less). Field injection requires setAccessible(true)
which is slower and breaks encapsulation.

Q4. What is the difference between getDeclaredMethods() and getMethods()?


Short: getDeclaredMethods returns all methods of this class only. getMethods returns all public
methods including inherited ones.
Detail: getDeclaredMethods(): Returns all methods declared in this specific class — private, protected, public,
package-private. Does NOT include inherited methods. getMethods(): Returns only PUBLIC methods, but
includes all inherited public methods from superclasses and interfaces.
Follow-up Questions:
➤ How to get private methods from a parent class via reflection?
→ You cannot get them directly via a subclass. You must get the parent Class via [Link]() and then
call getDeclaredMethods() on it.

Q5. Why is Reflection slow? When should you NOT use it?
Short: JVM cannot inline or optimize reflected calls. Avoid in hot code paths.
Detail: Reflection bypasses JVM optimizations like method inlining and JIT compilation. Every invoke() call
requires: (1) security checks, (2) argument boxing/unboxing, (3) dynamic dispatch. This is 50-100x slower
than direct calls. Do not use in: (a) tight loops processing millions of records, (b) serialization hot paths, (c) any
production code called millions of times per second.
Follow-up Questions:
➤ What is the alternative to Reflection for performance?
→ [Link] (Java 7+) — typed and optimizable. Also code generation (APT, cglib,
ByteBuddy) which generates bytecode at class load time instead of using reflection.

INTERVIEW TRAPS — Watch Out!


TRAP 1: "Can Reflection access truly private fields?" → YES, with setAccessible(true). Private is enforced only
by the compiler, not the JVM byte-level.
TRAP 2: "Does setAccessible(true) make the field public?" → NO. It only disables the access check for that
specific Field object. The class definition is unchanged.
TRAP 3: "Is [Link]() the only way to get Class object?" → No. Also: [Link], [Link](),
[Link] (for primitives), [Link].
TRAP 4: "Can you use Reflection on final fields?" → Yes, you can read them. Writing is possible but behavior is
undefined for compile-time constants (the JVM may have inlined them).
TRAP 5: "Does Java 9 module system break all Reflection?" → Only cross-module access to non-exported
packages. Within same module, or for explicitly opened modules, it still works.

Java Interview Notes • Page 7 • Mihir – Placement Prep 2026


MEMORY TRICKS
MIRROR: Reflection = Mirror. Just as a mirror shows you what you look like without you "knowing" it at design
time, reflection shows a class its own structure at runtime.
CAFE Acronym for Class API: C=Class, A=Access, F=Field, E=Execute (invoke). You Class-check,
Access-modify, Field-read, and Execute-invoke.
getDeclared vs get: Declared = "from THIS class" (includes private). No "Declared" = "public + inherited".
setAccessible: Think "security override". You override the access security check — not change the class itself.

QUICK REVISION
Class<T>: Metadata about a type (fields, methods, constructors)
[Link]("name"): Dynamically load a class at runtime
getDeclaredFields() vs getFields(): All fields of THIS class vs all public fields including inherited
getDeclaredMethods() vs getMethods(): Same pattern as above
setAccessible(true): Bypass private/protected access check
[Link](obj, args): Call the method on an object
[Link](args): Create a new instance
Spring use: Bean scanning, @Autowired injection, AOP proxies
Hibernate use: @Column, @Table mapping from annotations to SQL
Drawbacks: Slow, breaks encapsulation, loses compile-time safety

Java Interview Notes • Page 8 • Mihir – Placement Prep 2026


TOPIC 02

Generics
Write once, work with any type safely — type safety at compile time

1. Why Were Generics Introduced?


THE PROBLEM BEFORE GENERICS (Java 1.4 and earlier)
All collections stored Object. You could add anything — String, Integer, Employee — to the same List.
Every time you retrieved an element, you had to cast it manually: (String) [Link](0)
The cast could fail at RUNTIME with ClassCastException — the bug appeared only when the code ran, not
when it was compiled.
There was no way for the compiler to warn you "Hey, you are adding an Integer to a List that should only have
Strings."

// Before vs After Generics


// BEFORE GENERICS (Java 1.4)
List list = new ArrayList();
[Link]("Hello");
[Link](42); // No compiler error!
String s = (String) [Link](1); // BOOM! ClassCastException at runtime
// WITH GENERICS (Java 5+)
List<String> list = new ArrayList<>();
[Link]("Hello");
[Link](42); // COMPILER ERROR immediately — bug caught at compile time!
String s = [Link](0); // No cast needed — compiler knows it is a String

2. Definition
SIMPLE EXPLANATION
Generics = Type Parameters. Instead of hardcoding a specific type, you use a placeholder (T, E, K, V) that gets
filled in when someone uses your class or method.
Think of it like a template. Box<T> is a template for any box — Box<String> is a string box, Box<Integer> is an
integer box. Same code, different types.

FORMAL DEFINITION
Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and
methods. The type parameter provides a way to re-use the same code with different types as input, while
guaranteeing compile-time type safety and eliminating the need for explicit type casting.

3. Type Erasure — The Most Important Concept

Java Interview Notes • Page 9 • Mihir – Placement Prep 2026


WHAT IS TYPE ERASURE?
Generics exist ONLY at compile time. At runtime, all type parameters are erased — replaced with Object (or
their upper bound).
List<String> and List<Integer> are BOTH just List at runtime. They compile to the same bytecode.
The compiler inserts casts where needed — you just do not see them.
This was done for backward compatibility with pre-Java-5 code.

// Type Erasure in Action


// What you write:
List<String> names = new ArrayList<>();
[Link]("Alice");
String name = [Link](0);
// What the compiler generates (after type erasure):
List names = new ArrayList();
[Link]("Alice");
String name = (String) [Link](0); // Cast inserted by compiler
// Proof of type erasure:
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
[Link]([Link]() == [Link]()); // TRUE! Both are ArrayList

4. Generic Classes and Methods


// Generic Class and Method
// Generic Class
public class Box<T> {
private T value;
public void set(T value) { [Link] = value; }
public T get() { return value; }
}
// Usage
Box<String> strBox = new Box<>();
Box<Integer> intBox = new Box<>();
// Generic Method (T is defined on the METHOD, not the class)
public static <T extends Comparable<T>> T max(T a, T b) {
return [Link](b) > 0 ? a : b;
}
// Works with any Comparable type
int m1 = max(3, 7); // m1 = 7
String m2 = max("apple","mango"); // m2 = "mango"

5. Bounded Generics

Java Interview Notes • Page 10 • Mihir – Placement Prep 2026


// Bounded Generics
// Upper Bound: T must BE a Number or subclass of Number
public <T extends Number> double sum(List<T> list) {
double total = 0;
for (T n : list) total += [Link]();
return total;
}
// Multiple Bounds: T must implement both Comparable and Serializable
public <T extends Comparable<T> & Serializable> T findMin(List<T> list) { ... }
// Lower Bound: T must be Integer or a supertype of Integer
public void addNumbers(List<? super Integer> list) {
[Link](1); [Link](2); // Safe to add Integers
}

6. Wildcards — ?, ? extends T, ? super T


Wildcard Meaning Use When Can Can
READ? WRITE?

? Unknown type You don't care about the type at As Object No (except
all null)

? extends T T or any subtype of T Reading from a producer (PECS) Yes, as T No (unsafe)


(upper bounded)

? super T T or any supertype of T Writing to a consumer (PECS) As Object Yes


(lower bounded)

7. PECS Principle — Producer Extends, Consumer Super


PECS — The Golden Rule for Wildcards
PECS = Producer Extends, Consumer Super
Producer Extends: If a collection GIVES (produces) data to your code, use ? extends T — you only READ from it
Consumer Super: If a collection RECEIVES (consumes) data from your code, use ? super T — you only WRITE
to it
Remember: [Link](destination, source) — destination uses super, source uses extends

// PECS in Practice
// PECS Example
// Producer Extends — we READ from source (it produces values for us)
public void printAll(List<? extends Animal> animals) {
for (Animal a : animals) [Link]([Link]); // READ only
}
// Consumer Super — we WRITE into dest (it consumes our values)
public void addCats(List<? super Cat> list) {
[Link](new Cat("Whiskers")); // WRITE only
}
// Real example: [Link]
// static <T> void copy(List<? super T> dest, List<? extends T> src)
// dest = consumer = super (we write to dest)
// src = producer = extends (we read from src)

Java Interview Notes • Page 11 • Mihir – Placement Prep 2026


8. Interview Questions
Q1. What is the difference between List<?>, List<Object>, and List<T>?
Short: List<?> is unknown type. List<Object> is explicitly Object. List<T> is a type parameter to be
specified.
Detail: List<?> (unbounded wildcard): Can hold any type of List. You cannot add elements (except null)
because the type is unknown. List<Object>: A list that holds Objects. You can add any object to it, but you
cannot pass a List<String> where a List<Object> is expected — because generics are NOT covariant.
List<T>: T is a type parameter — the caller specifies the actual type. Used in generic methods/classes.
Follow-up Questions:
➤ Why can't you pass List<String> where List<Object> is expected?
→ Because generics are invariant. If it were allowed, you could add an Integer to what is actually a List<String>,
breaking type safety.

Q2. What is Type Erasure? Why does it exist?


Short: Generics are compile-time only. At runtime, type parameters are replaced by Object (or their
upper bound). For Java 5 backward compatibility.
Detail: Type Erasure means the JVM does not know about generic types at runtime. List<String> and
List<Integer> are both just List in bytecode. The compiler inserts casts automatically. This was done so
pre-Java-5 bytecode (which had no generics) could interoperate with new generic code. Implication: You
cannot do new T[], instanceof List<String>, or catch(Exception<T> e) because T does not exist at runtime.
Follow-up Questions:
➤ Why can't you do new T[] in a generic class?
→ Because T is erased to Object at runtime. The JVM cannot create a typed array when it does not know the
type.
➤ How does [Link]() return a typed list without creating one?
→ It casts an already-existing empty list — safe because you cannot add to an empty list, so no actual type
violation occurs.

Q3. Explain PECS with an example.


Short: PECS = Producer Extends, Consumer Super. If you read from a collection use extends. If you
write to it, use super.
Detail: Look at [Link](List<? super T> dest, List<? extends T> src). src PRODUCES values we read
— extends. dest CONSUMES values we write — super. For example, if T=Integer: src can be List<Integer> or
List<Number>. dest can be List<Integer>, List<Number>, or List<Object> — wider types that can safely hold
an Integer.
Follow-up Questions:
➤ Can you both read and write with ? extends?
→ No. With ? extends T you can read as T, but cannot add elements because the exact subtype is unknown.
What if it's List<Circle> and you try to add a Square (both extend Shape)?

Java Interview Notes • Page 12 • Mihir – Placement Prep 2026


INTERVIEW TRAPS
TRAP 1: Generics are NOT covariant. List<Dog> is NOT a subtype of List<Animal>, even though Dog extends
Animal.
TRAP 2: You CANNOT create generic arrays: new T[10] is illegal due to type erasure.
TRAP 3: Primitive types cannot be type parameters. List<int> is illegal — use List<Integer>.
TRAP 4: instanceof check with generic type fails: list instanceof List<String> is a compiler error.
TRAP 5: Overloading with generics: void foo(List<Integer>) and void foo(List<String>) are the SAME after
erasure — compile error.

MEMORY TRICKS
PECS = "Producer Extends, Consumer Super" — like a factory (producer) has narrow specialization (extends), a
bin (consumer) accepts many things (super).
Covariance: Arrays ARE covariant (Dog[] can be assigned to Animal[]). Generics are NOT. This is why generics
are safer.
Erasure: Think of generics as a code template that gets "compiled out" — like C macros, they exist only in
source, not in the final binary.
? extends = READ-ONLY (unknown subtype — dangerous to write). ? super = WRITE-OK (wider type can safely
hold our narrower value).

QUICK REVISION
Generics: Compile-time type safety. No ClassCastException. No manual casting.
Type Erasure: Generics replaced by Object at runtime. List<String> == List<Integer> at runtime.
<T extends Foo>: T must be Foo or subclass. Upper bound.
<T super Foo>: T must be Foo or superclass. Lower bound (wildcards only).
PECS: Producer=Extends (read from). Consumer=Super (write to).
<?> Wildcard: Unknown type. No reading as specific type, no writing except null.
Cannot: new T[], instanceof List<String>, List<int>, overload with same erasure.
Generic method: <T> declared before return type. Independent of class type param.

Java Interview Notes • Page 13 • Mihir – Placement Prep 2026


TOPIC 03

Lambda Expressions
Write concise, readable code for functional behavior — no more anonymous class boilerplate

1. Why Were Lambda Expressions Introduced?


THE PROBLEM BEFORE LAMBDAS
Java had functional interfaces (interfaces with one method) for years: Runnable, Comparator, ActionListener.
To pass behavior as a parameter, you had to write anonymous inner classes — verbose, hard to read.
A simple "sort by name" required 6 lines of boilerplate for just 1 line of actual logic.
The goal: treat code (behavior) as data — pass functions around just like you pass objects.

// Before vs After Lambda


// BEFORE LAMBDA (Anonymous Class) — 7 lines for 1 line of logic
[Link](employees, new Comparator<Employee>() {
@Override
public int compare(Employee a, Employee b) {
return [Link]().compareTo([Link]());
}
});
// WITH LAMBDA — 1 clean line
[Link]((a, b) -> [Link]().compareTo([Link]()));
// Even cleaner with Method Reference
[Link]([Link](Employee::getName));

2. Definition
SIMPLE EXPLANATION
A Lambda is an anonymous function — a function that has no name, no class, no access modifier.
It is a short way to implement a Functional Interface (interface with exactly one abstract method).
Lambdas let you pass behavior (code) as a value, just like you pass an int or a String.

FORMAL DEFINITION
A Lambda Expression is a concise representation of an anonymous function that can be passed as an argument
or stored as a variable. It implements the abstract method of a Functional Interface, providing a target type for
the lambda. Introduced in Java 8 as part of JSR 335.

3. Lambda Syntax Breakdown

Java Interview Notes • Page 14 • Mihir – Placement Prep 2026


// Lambda Syntax Variations
// Full Syntax
(parameter_list) -> { body; return result; }
// (a, b) -> { return a + b; }
// ^^^^^ ^^^^^^^^^^^^^^^^^^
// params body (with braces for multiple statements)
// SIMPLIFIED FORMS
// 1. No parameters
() -> [Link]("Hello");
// 2. One parameter (parentheses optional)
x -> x * x
(x) -> x * x // same thing
// 3. Two parameters
(a, b) -> a + b
// 4. Multi-line body (needs braces + explicit return)
(a, b) -> {
int sum = a + b;
return sum;
}
// 5. With type declarations (optional, compiler infers)
(int a, int b) -> a + b

4. Lambda vs Anonymous Class


Aspect Anonymous Class Lambda

Syntax Verbose (5-8 lines) Concise (1 line)

this keyword Refers to anonymous class Refers to enclosing class

Compilation Generates .class file Uses invokedynamic bytecode

State Can have fields (state) No state — pure function

Serialization Serializable if tagged Not directly serializable

Memory New object each time May be reused by JVM (invokedynamic)

Target type Can implement any interface Only Functional Interfaces

5. Internal Working of Lambdas


HOW LAMBDAS WORK INTERNALLY
Lambdas do NOT compile to anonymous classes. This is a common misconception.
Java compiler converts a lambda into a special bytecode instruction called invokedynamic (JVM instruction).
invokedynamic is resolved at runtime by LambdaMetafactory — which creates a class on the fly using ASM.
This is faster than creating anonymous .class files because: (1) No extra .class file loaded at startup, (2) JVM
can cache and reuse lambda instances when they capture no variables.
Captured variables (from surrounding scope) are passed as constructor args to the generated class.

Java Interview Notes • Page 15 • Mihir – Placement Prep 2026


// Variable Capture in Lambdas
// Lambda capturing a variable
String prefix = "Hello";
Runnable r = () -> [Link](prefix + " World");
// Internally (simplified):
// JVM generates:
// class Lambda$1 implements Runnable {
// String prefix; // captured variable
// Lambda$1(String prefix) { [Link] = prefix; }
// public void run() { [Link](prefix + " World"); }
// }
// new Lambda$1(prefix);
// IMPORTANT: Captured variables must be effectively final
// (either declared final, or never reassigned after capture)
int count = 0;
// count++; // This would make the below lambda invalid
Runnable bad = () -> [Link](count); // OK if count not reassigned

6. Lambda as Functional Interface Implementation

// Lambda with Common Functional Interfaces


// Runnable (no arg, no return)
Runnable r = () -> [Link]("Running");
new Thread(r).start();
// Comparator<String> (two args, returns int)
Comparator<String> c = (a, b) -> [Link]() - [Link]();
// Callable<Integer> (no arg, returns value)
Callable<Integer> calc = () -> 42;
// ActionListener (one arg, no return)
[Link](e -> [Link]("Clicked: " + e));

Q1. What is a Lambda Expression? What problem does it solve?


Short: Anonymous function that implements a functional interface. Eliminates anonymous class
boilerplate.
Detail: A lambda expression is a concise way to represent an anonymous function. It has parameters, a body,
and optionally a return type — but no name, no class, no access modifier. It must implement the single
abstract method of a functional interface. It solves the verbosity problem: before Java 8, passing behavior
required 5-8 lines of anonymous class code. Lambdas reduce that to 1 line.
Follow-up Questions:
➤ Can lambda be used with non-functional interfaces?
→ No. Lambda requires exactly ONE abstract method to implement. If an interface has 0 or 2+ abstract methods,
lambda cannot target it.
➤ What does effectively final mean?
→ A variable is effectively final if its value never changes after initialization — even if not declared final. Lambda
can capture it. If you reassign the variable, the lambda capture becomes a compile error.

Java Interview Notes • Page 16 • Mihir – Placement Prep 2026


Q2. How does Java compile lambdas internally?
Short: Not as anonymous classes. Using invokedynamic bytecode and LambdaMetafactory at
runtime.
Detail: The compiler does NOT generate anonymous .class files for lambdas. Instead it generates an
invokedynamic JVM instruction. At first call, the JVM invokes [Link]() which uses
ASM to dynamically generate a class that implements the target functional interface. This generated class is
cached. For non-capturing lambdas (no variables from outer scope), the instance may be reused across calls.
Follow-up Questions:
➤ Why is invokedynamic faster than anonymous classes?
→ No .class file to load at startup. Generated class is leaner. Non-capturing lambdas can be singletons — no new
object allocation per call.

INTERVIEW TRAPS
TRAP: "Lambda creates an anonymous class" — FALSE. It uses invokedynamic.
TRAP: this inside lambda refers to the enclosing class, not any lambda class. In anonymous class, this refers to
the anonymous class itself.
TRAP: Lambda is not serializable by default. You cannot serialize a lambda directly.
TRAP: Effectively final — you cannot modify a captured variable even if not declared final. Not just final, but
"never reassigned."

MEMORY TRICKS
Lambda = parameters -> body. Arrow separates input from output. Like => in math.
Three Lambda Laws: (1) Must target Functional Interface (2) Types inferred (3) Captured vars effectively final.
"invokedynamic" = "invoke dynamically". Lambda class created at runtime, not compile time.
this in lambda = enclosing class. this in anonymous class = anonymous class. Remember: lambda has no class
of its own.

QUICK REVISION
Lambda: Anonymous function implementing a Functional Interface
Syntax: (params) -> expression OR (params) -> { statements; return val; }
Compiled as: invokedynamic + LambdaMetafactory (NOT anonymous class)
this inside lambda: refers to ENCLOSING class (not lambda itself)
Captured variables: must be effectively final (never reassigned)
Target type: must be a Functional Interface (exactly 1 abstract method)
No state: lambdas are stateless — pure functions

Java Interview Notes • Page 17 • Mihir – Placement Prep 2026


TOPIC 04

Functional Interfaces
The contract that makes Lambda Expressions possible

1. What is a Functional Interface?


SIMPLE EXPLANATION
A Functional Interface is an interface with EXACTLY ONE abstract method.
This single abstract method is what a lambda or method reference implements.
It can have any number of default and static methods — those do not count.
@FunctionalInterface annotation is optional but strongly recommended — it makes the compiler verify the
contract.

// Custom Functional Interface


// Custom Functional Interface
@FunctionalInterface
public interface Greeting {
String greet(String name); // exactly ONE abstract method
// default and static methods are allowed
default void printGreeting(String name) {
[Link](greet(name));
}
}
// Using with Lambda
Greeting g = name -> "Hello, " + name + "!";
[Link]([Link]("Mihir")); // Hello, Mihir!
// @FunctionalInterface ENFORCES the contract
// Adding a second abstract method = COMPILE ERROR

2. Built-in Functional Interfaces in [Link]


Interface Method Input Output Use Case

Predicate<T> test(T t) T boolean Filter, validate

Consumer<T> accept(T t) T void Process, print, save

Supplier<T> get() none T Lazy creation, factory

Function<T,R> apply(T t) T R Transform, map, convert

UnaryOperator<T> apply(T t) T T Modify in-place (same type)

BinaryOperator<T> apply(T,T) T,T T Combine two of same type

BiPredicate<T,U> test(T,U) T,U boolean Two-arg filter

BiConsumer<T,U> accept(T,U) T,U void Two-arg consumer

BiFunction<T,U,R> apply(T,U) T,U R Two-arg transform

3. Primitive Specializations (Avoid Autoboxing)

Java Interview Notes • Page 18 • Mihir – Placement Prep 2026


Java provides primitive specializations to avoid autoboxing overhead:

• IntPredicate, LongPredicate, DoublePredicate — Predicate for primitive types


• IntConsumer, LongConsumer, DoubleConsumer — Consumer for primitives
• IntSupplier, LongSupplier, DoubleSupplier — Supplier for primitives
• IntFunction<R>, LongFunction<R>, DoubleFunction<R> — take primitive, return object
• ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> — take object, return primitive
• IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator

4. @FunctionalInterface Annotation
WHAT @FunctionalInterface DOES
1. It is a MARKER annotation — it does not change behavior.
2. It tells the compiler: "This interface must have exactly one abstract method."
3. If you accidentally add a second abstract method, the compiler gives an error immediately.
4. It is optional — an interface with one abstract method IS a functional interface with or without the annotation.
5. @FunctionalInterface is documentation + compile-time safety combined.

Q1. What is a Functional Interface? Can it have default methods?


Short: Interface with exactly one abstract method. Yes, it can have any number of default and static
methods.
Detail: A Functional Interface has exactly one abstract (unimplemented) method — this is what a lambda
expression provides the implementation for. It can also have default methods (implemented in the interface)
and static methods — these don't count toward the "one abstract method" rule. Example: Comparator<T> is a
functional interface — it has one abstract compare() but many default methods like reversed(),
thenComparing(), etc.
Follow-up Questions:
➤ Can a functional interface extend another interface?
→ Yes — as long as the total count of unimplemented abstract methods across both interfaces is exactly 1.

Q2. Difference between Runnable and Callable?


Short: Runnable: run() returns void, no checked exception. Callable: call() returns a value and can
throw checked exception.
Detail: Both are functional interfaces used with threads/executors. [Link]() returns void and cannot
throw checked exceptions. Callable<V>.call() returns type V and declares throws Exception. Use Callable
when you need a result from an async task — combine with Future<V> from ExecutorService.
Follow-up Questions:
➤ Which one to use with [Link]()?
→ supplyAsync() takes a Supplier<T> — not Callable. But you can wrap a Callable easily since both have similar
signatures except for checked exceptions.

QUICK REVISION
@FunctionalInterface: Marker annotation. Enforces single abstract method contract.
SAM = Single Abstract Method — the method a lambda implements.
Default/static methods don't count toward the "one abstract method" rule.
Key built-ins: Predicate (test), Consumer (accept), Supplier (get), Function (apply).
Primitive variants: IntPredicate, LongFunction, etc. — avoid boxing/unboxing overhead.

Java Interview Notes • Page 19 • Mihir – Placement Prep 2026


TOPIC 05

Predicate<T>
Boolean-valued function — test, filter, validate anything

1. What Is Predicate?
DEFINITION
Predicate<T> is a Functional Interface in [Link].
It takes one argument of type T and returns a boolean.
Used for: filtering collections, validating input, conditional checks.
Its single abstract method is: boolean test(T t)

2. Core Methods
Method Signature What It Does

test() boolean test(T t) Main method — evaluate condition on t

and() Predicate<T> and(Predicate<T> other) Logical AND — both must be true

or() Predicate<T> or(Predicate<T> other) Logical OR — at least one true

negate() Predicate<T> negate() Logical NOT — reverses the result

not() static Predicate<T> not(Predicate<T> p) Static NOT — Java 11+

// Predicate Usage
import [Link];
import [Link];
import [Link];
// Basic Predicate
Predicate<Integer> isEven = n -> n % 2 == 0;
[Link]([Link](4)); // true
[Link]([Link](5)); // false
// Combining Predicates
Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEvenAndPositive = [Link](isPositive);
Predicate<Integer> isEvenOrNegative = [Link]([Link]());
// Real-world: filter a list
List<String> names = [Link]("Alice", "Bob", "Charlie", "Ann");
Predicate<String> startsWithA = s -> [Link]("A");
Predicate<String> longName = s -> [Link]() > 3;
List<String> result = [Link]()
.filter([Link](longName))
.collect([Link]());
// Result: [Alice]
// [Link]() — Java 11
List<String> nonNull = [Link]()
.filter([Link](String::isEmpty))
.collect([Link]());

Java Interview Notes • Page 20 • Mihir – Placement Prep 2026


3. Real World Example — Input Validation

// Predicate for Validation


public class UserValidator {
private static final Predicate<String> NOT_EMPTY = s -> s != null && ![Link]();
private static final Predicate<String> VALID_EMAIL = s -> [Link]("@");
private static final Predicate<String> VALID_NAME = NOT_EMPTY.and(s -> [Link]() >= 2);
public static boolean validateUser(String name, String email) {
return VALID_NAME.test(name) && VALID_EMAIL.and(NOT_EMPTY).test(email);
}
}

Q1. What is Predicate<T>? What is its functional method?


Short: Functional interface representing a boolean-valued function. Functional method: boolean test(T
t).
Detail: Predicate<T> is in [Link] and represents a condition/criteria. test(T) evaluates that condition
and returns boolean. Combined with [Link](), it is the primary tool for collection filtering. You can chain
predicates with and(), or(), negate() to build complex conditions without if-else chains.
Follow-up Questions:
➤ How is Predicate<T> different from Function<T,Boolean>?
→ Functionally similar but Predicate is specialized for boolean conditions. Predicate has and/or/negate
composition methods. Function works with boxed Boolean. Use Predicate for boolean tests, Function for general
transforms.

QUICK REVISION
Predicate<T>: boolean test(T t) — evaluates a condition
and(): both predicates must return true (short-circuits on first false)
or(): at least one must return true (short-circuits on first true)
negate(): reverses the result (logical NOT)
Use in [Link]() to remove elements not matching condition
[Link]() is static, Java 11+ alternative to [Link]()

Java Interview Notes • Page 21 • Mihir – Placement Prep 2026


TOPIC 06

Consumer<T>
Accepts a value and performs an action — no return value

DEFINITION
Consumer<T> represents an operation that accepts a single T argument and returns void.
It is used when you want to DO something with a value — print it, save it, send it — but not transform it.
Single abstract method: void accept(T t)
Also: BiConsumer<T,U> accepts two arguments.

Method What It Does

void accept(T t) Main method — process the value

Consumer<T> andThen(Consumer<T> after) Chain consumers — first apply this, then after

// Consumer Usage
// Basic Consumer
Consumer<String> print = s -> [Link](s);
[Link]("Hello"); // Hello
// andThen — chain consumers
Consumer<String> printUpper = s -> [Link]([Link]());
Consumer<String> printAndLog = [Link](printUpper);
[Link]("hello");
// Output:
// hello
// HELLO
// Real-world: forEach uses Consumer
List<Employee> employees = getEmployees();
[Link](emp -> [Link](true)); // Consumer<Employee>
// BiConsumer
BiConsumer<String, Integer> showAge = (name, age) ->
[Link](name + " is " + age + " years old");
[Link]("Mihir", 21);
// [Link] uses BiConsumer<K,V>
Map<String, Integer> scores = [Link]("Alice", 90, "Bob", 85);
[Link]((name, score) -> [Link](name + ": " + score));

QUICK REVISION
Consumer<T>: void accept(T t) — takes a value, does something, returns nothing
andThen(): chains two consumers sequentially
Use case: forEach, logging, printing, saving, updating objects
BiConsumer<T,U>: two args, void return — used in [Link]()
Key distinction from Function: Consumer has NO return value

Java Interview Notes • Page 22 • Mihir – Placement Prep 2026


TOPIC 07

Supplier<T>
Supplies a value — takes nothing, gives something

DEFINITION
Supplier<T> represents a supplier of results. It takes NO arguments and returns T.
Used for lazy initialization, factory methods, and deferred computation.
Single abstract method: T get()

// Supplier Usage
// Basic Supplier
Supplier<String> greeting = () -> "Hello World";
[Link]([Link]()); // Hello World
// Lazy Initialization — DB not called until get() is invoked
Supplier<List<User>> usersFromDB = () -> [Link]();
// ... later when you actually need users ...
List<User> users = [Link](); // DB query runs here, not before
// [Link] uses Supplier — lazy!
Optional<User> user = findUser(id);
// BAD — computeDefault() always called
User u1 = [Link](computeDefault());
// GOOD — computeDefault() only called if user is empty
User u2 = [Link](() -> computeDefault());
// Random token generation
Supplier<String> tokenGen = () -> [Link]().toString();
String token1 = [Link]();
String token2 = [Link](); // New UUID each time

QUICK REVISION
Supplier<T>: T get() — no input, produces a value
Key use: lazy evaluation — computation deferred until get() is called
[Link](Supplier) is preferred over orElse(value) for expensive defaults
Factory methods, random generators, lazy initialization — all classic Supplier use cases

Java Interview Notes • Page 23 • Mihir – Placement Prep 2026


TOPIC 08

Function<T, R>
Transform T into R — the universal mapping tool

DEFINITION
Function<T, R> represents a function that accepts T and produces R.
It is the most general transformation interface.
Single abstract method: R apply(T t)
UnaryOperator<T> extends Function<T,T> — same input and output type.

Method Signature Flow

apply() R apply(T t) T -> R (core transformation)

andThen() Function<T,V> andThen(Function<R,V>) f -> g (apply f first, then g)

compose() Function<V,R> compose(Function<V,T>) g -> f (apply g first, then f)

identity() static Function<T,T> identity() Returns input unchanged: t -> t

// Function Usage
// Basic Function
Function<String, Integer> strLen = s -> [Link]();
[Link]([Link]("Hello")); // 5
// andThen — apply this first, then the next
Function<String, Integer> parse = Integer::parseInt;
Function<Integer, String> doubler = n -> "Result: " + (n * 2);
Function<String, String> parseAndDouble = [Link](doubler);
[Link]([Link]("21")); // Result: 42
// compose — apply parameter first, then this
Function<Integer, Integer> times2 = n -> n * 2;
Function<Integer, Integer> plus3 = n -> n + 3;
// andThen: [Link](plus3) = (n*2) + 3
// compose: [Link](plus3) = (n+3) * 2
[Link]([Link](plus3).apply(5)); // (5*2)+3 = 13
[Link]([Link](plus3).apply(5)); // (5+3)*2 = 16
// Function in [Link]()
List<String> names = [Link]("alice", "bob");
Function<String,String> capitalize = s -> [Link](0,1).toUpperCase() + [Link](1);
[Link]().map(capitalize).forEach([Link]::println);
// Alice
// Bob
// BiFunction<T, U, R> — two inputs
BiFunction<String, Integer, String> repeat = (s, n) -> [Link](n);
[Link]([Link]("Ha", 3)); // HaHaHa

andThen vs compose — The Key Difference


andThen(g): apply THIS first, then g. [Link](g) = g(f(x))
compose(g): apply g first, then THIS. [Link](g) = f(g(x))
Memory trick: andThen = "f, THEN g". compose = "f composed WITH g before".
They are inverse of each other: [Link](g) == [Link](f)

Java Interview Notes • Page 24 • Mihir – Placement Prep 2026


QUICK REVISION
Function<T,R>: R apply(T t) — transform T to R
andThen(g): apply f first, then g. Pipeline: input -> f -> g -> output
compose(g): apply g first, then f. Pipeline: input -> g -> f -> output
identity(): returns the input unchanged — useful as a no-op default
UnaryOperator<T>: specialization where T=R (same input and output type)
BiFunction<T,U,R>: takes two inputs, returns R

Java Interview Notes • Page 25 • Mihir – Placement Prep 2026


TOPIC 09

Method References
Shorthand for lambdas that just call an existing method

WHAT IS A METHOD REFERENCE?


A Method Reference is a shorter way to write a lambda that calls an existing method.
Instead of: x -> [Link](x) you write: SomeClass::someMethod
The :: operator is the method reference operator.
There are 4 types of method references.

4 Types of Method References


Type Syntax Equivalent Lambda Example

Static Method ClassName::staticMethod (args) -> Integer::parseInt


[Link](args)

Instance (specific obj) instance::method (args) -> [Link](args) [Link]::println

Instance (any obj) ClassName::instanceMetho (obj, args) -> String::toUpperCase


d [Link](args)

Constructor Reference ClassName::new (args) -> new ClassName(args) ArrayList::new

Java Interview Notes • Page 26 • Mihir – Placement Prep 2026


// 4 Types of Method References
// TYPE 1: Static Method Reference
// Lambda: x -> [Link](x)
// Method Reference: Integer::parseInt
Function<String, Integer> parse = Integer::parseInt;
[Link]([Link]("42")); // 42
// TYPE 2: Instance Method of a Specific Object
// Lambda: s -> [Link](s)
// Method Reference: [Link]::println
Consumer<String> printer = [Link]::println;
[Link]("Hello");
// TYPE 3: Instance Method of an Arbitrary Object (from parameter)
// Lambda: s -> [Link]()
// Method Reference: String::toUpperCase
Function<String,String> upper = String::toUpperCase;
[Link]([Link]("hello")); // HELLO
// In Stream context (most common use):
List<String> names = [Link]("alice", "bob", "charlie");
[Link]()
.map(String::toUpperCase) // Type 3
.forEach([Link]::println); // Type 2
// TYPE 4: Constructor Reference
// Lambda: () -> new ArrayList<>()
// Method Reference: ArrayList::new
Supplier<List<String>> listFactory = ArrayList::new;
List<String> newList = [Link]();
// Constructor reference in [Link]()
[Link]("a","b","c")
.collect([Link](ArrayList::new));

Q1. What is the difference between Type 2 and Type 3 method references?
Short: Type 2 is called on a specific, pre-existing object. Type 3 is called on an argument passed to
the lambda.
Detail: Type 2 (specific instance): "instance::method" — the object "instance" is captured. The resulting
lambda takes the method's remaining params. E.g., [Link]::println — [Link] is a specific
PrintStream object. Type 3 (arbitrary instance): "Class::instanceMethod" — the first parameter of the lambda
becomes the object the method is called on. E.g., String::toUpperCase — the lambda receives a String
argument and calls toUpperCase() on it.
Follow-up Questions:
➤ When would you use constructor reference?
→ When you need a factory function. Like Supplier<T> or in
[Link]([Link](ArrayList::new)) to specify the collection type to collect into.

QUICK REVISION
Method Reference: shorthand when lambda just delegates to an existing method
Operator: :: (double colon)
Static: ClassName::staticMethod
Instance on specific object: existingObject::method
Instance on lambda param: ClassName::instanceMethod (param becomes the object)
Constructor: ClassName::new
Most common in Stream: .map(String::length), .forEach([Link]::println)

Java Interview Notes • Page 27 • Mihir – Placement Prep 2026


TOPIC 10

Stream API
Process collections declaratively — pipeline-style data transformation

1. Why Streams?
THE PROBLEM BEFORE STREAMS
Before Java 8: Processing collections required explicit loops, temporary lists, manual counters.
Code was imperative ("how to do it") — tell the JVM every step.
Parallel processing required manual thread management.
Stream API makes it declarative ("what to do") — the JVM decides how.

// Before vs After Streams


// BEFORE Streams — find names of active employees, sorted, age > 25
List<String> result = new ArrayList<>();
for (Employee e : employees) {
if ([Link]() && [Link]() > 25) {
[Link]([Link]());
}
}
[Link](result);
// WITH Streams — clean, readable, parallelizable
List<String> result = [Link]()
.filter(Employee::isActive)
.filter(e -> [Link]() > 25)
.map(Employee::getName)
.sorted()
.collect([Link]());

2. Stream Pipeline Architecture


// Stream Pipeline Diagram
// STREAM PIPELINE
DATA SOURCE INTERMEDIATE OPS TERMINAL OP
■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■
■ Collection■■■■■■■>■ filter() / map() ■■■■■■■>■ collect()■
■ Array ■ ■ sorted() / limit()■ ■ count() ■
■ [Link] ■ ■ flatMap() / skip()■ ■ reduce() ■
■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■■ ■ forEach()■
(lazy - not executed ■■■■■■■■■■■■
until terminal op) (triggers execution)
Key rules:
1. Source: where data comes from
2. Intermediate ops: lazy (return new Stream, do not execute yet)
3. Terminal op: triggers the entire pipeline
4. Stream is consumed once — cannot be reused after terminal op

3. Creating Streams

Java Interview Notes • Page 28 • Mihir – Placement Prep 2026


// Stream Creation
List<Integer> nums = [Link](1,2,3,4,5);
[Link](); // from Collection
[Link](new int[]{1,2,3}); // from array
[Link](1, 2, 3, 4, 5); // varargs
[Link](() -> [Link]()).limit(5); // infinite, limited
[Link](1, n -> n * 2).limit(10); // 1,2,4,8,16...
[Link](1, 6); // 1,2,3,4,5 (primitive)
[Link](1, 5); // 1,2,3,4,5 (inclusive)

4. Intermediate Operations (Lazy)


Operation Signature What It Does

filter() Stream<T> filter(Predicate<T>) Keep elements matching predicate

map() Stream<R> map(Function<T,R>) Transform each element T -> R

flatMap() Stream<R> Flatten nested streams into one


flatMap(Function<T,Stream<R>)

sorted() Stream<T> sorted() / sorted(Comparator) Sort elements (natural or custom)

distinct() Stream<T> distinct() Remove duplicates (uses equals())

limit() Stream<T> limit(long n) Take first n elements

skip() Stream<T> skip(long n) Skip first n elements

peek() Stream<T> peek(Consumer<T>) Debug: see elements without changing them

mapToInt() IntStream mapToInt(ToIntFunction<T>) Map to primitive IntStream (no boxing)

5. Terminal Operations (Trigger Execution)


Operation Signature Returns What It Does

collect() collect(Collector) R Accumulate to List, Set, Map

forEach() forEach(Consumer<T>) void Process each element

count() count() long Count elements

findFirst() findFirst() Optional<T> First element (may be absent)

findAny() findAny() Optional<T> Any element (faster in parallel)

anyMatch() anyMatch(Predicate) boolean At least one matches

allMatch() allMatch(Predicate) boolean All elements match

noneMatch() noneMatch(Predicate) boolean No elements match

min() min(Comparator) Optional<T> Minimum element

max() max(Comparator) Optional<T> Maximum element

reduce() reduce(identity, BinaryOp) T Fold elements to single value

toArray() toArray() Object[] Collect to array

Java Interview Notes • Page 29 • Mihir – Placement Prep 2026


6. map() vs flatMap()
// map vs flatMap
// map() — one-to-one transformation
List<String> names = [Link]("Alice", "Bob");
List<Integer> lengths = [Link]()
.map(String::length) // "Alice"->5, "Bob"->3
.collect([Link]()); // [5, 3]
// flatMap() — one-to-many, then flatten
List<List<Integer>> nested = [Link](
[Link](1, 2, 3),
[Link](4, 5, 6)
);
List<Integer> flat = [Link]()
.flatMap(Collection::stream) // [[1,2,3],[4,5,6]] -> [1,2,3,4,5,6]
.collect([Link]());
// Real-world: employee skills
List<String> allSkills = [Link]()
.flatMap(emp -> [Link]().stream())
.distinct()
.collect([Link]());

7. reduce() — Fold to Single Value

// reduce() Examples
// reduce(identity, BinaryOperator)
int sum = [Link](1, 5)
.reduce(0, Integer::sum); // 0+1+2+3+4+5 = 15
// Without identity — returns Optional (could be empty stream)
Optional<Integer> product = [Link](1,2,3,4,5)
.reduce((a, b) -> a * b); // 1*2*3*4*5 = 120
// Max using reduce
Optional<Integer> max = [Link](3,1,4,1,5,9)
.reduce(Integer::max); // 9

8. collect() and Collectors

Java Interview Notes • Page 30 • Mihir – Placement Prep 2026


// Collectors in Action
List<Employee> employees = getEmployees();
// Collect to List
List<String> names = [Link]()
.map(Employee::getName)
.collect([Link]());
// Collect to Set (removes duplicates)
Set<String> depts = [Link]()
.map(Employee::getDept)
.collect([Link]());
// Collect to Map: name -> salary
Map<String,Double> salaryMap = [Link]()
.collect([Link](Employee::getName, Employee::getSalary));
// groupingBy — group employees by department
Map<String, List<Employee>> byDept = [Link]()
.collect([Link](Employee::getDept));
// partitioningBy — split by boolean condition
Map<Boolean, List<Employee>> seniorJunior = [Link]()
.collect([Link](e -> [Link]() > 30));
// true -> senior employees
// false -> junior employees
// counting per group
Map<String, Long> countByDept = [Link]()
.collect([Link](Employee::getDept, [Link]()));
// joining strings
String csvNames = [Link]()
.map(Employee::getName)
.collect([Link](", ", "[", "]"));
// [Alice, Bob, Charlie]

9. Lazy Evaluation — Important Concept


LAZY EVALUATION EXPLAINED
Intermediate operations are LAZY — they do nothing until a terminal operation is called.
When a terminal op is called, the pipeline is assembled and elements flow through ONE AT A TIME.
The JVM optimizes: if filter() rejects an element, map() never runs on it.
Short-circuit ops (findFirst, anyMatch, limit) can stop processing early.
This means: order of operations matters! filter() before map() is more efficient when many elements fail the filter.

// Lazy Evaluation Demo


// Lazy evaluation proof
[Link](1, 2, 3, 4, 5)
.filter(n -> { [Link]("filter: " + n); return n > 2; })
.map(n -> { [Link]("map: " + n); return n * 10; })
.findFirst(); // Terminal op
// Output:
// filter: 1 (fails filter)
// filter: 2 (fails filter)
// filter: 3 (passes filter)
// map: 3 (mapped to 30)
// STOPS — findFirst() got its answer!
// Elements 4 and 5 are NEVER processed

Java Interview Notes • Page 31 • Mihir – Placement Prep 2026


10. Parallel Streams

// Parallel Streams
// Create parallel stream
[Link]() // directly
[Link]().parallel() // convert
// Example: sum of squares in parallel
long sumOfSquares = [Link](1, 1_000_000L)
.parallel()
.map(n -> n * n)
.sum();
// WHEN to use parallel:
// 1. Large data (10,000+ elements)
// 2. CPU-intensive operations (not I/O bound)
// 3. No side effects (stateless operations)
// 4. Order does not matter
// WHEN NOT to use parallel:
// 1. Small collections — thread overhead > benefit
// 2. Shared mutable state — race conditions
// 3. Operations need sequential order (e.g., reading a file line by line)
// 4. I/O bound operations — parallel does not help

11. Stream vs Collection


Aspect Collection Stream

Storage Stores elements in memory Does not store — processes on the fly

Reusability Can be iterated multiple times Single use — consumed after terminal op

Iteration External (you write the loop) Internal (JVM handles iteration)

Modification Can add/remove elements Cannot modify source — read only

Evaluation Eager — all data exists upfront Lazy — computed only when needed

Parallelism Manual thread management needed Built-in with parallelStream()

Size Always finite Can be infinite ([Link])

Q1. What is a Stream? How is it different from a Collection?


Short: Stream is a pipeline for processing data. Collection stores data. Stream is lazy, single-use, and
cannot modify source.
Detail: A Stream is a sequence of elements that supports sequential and parallel aggregate operations. Unlike
Collections, Streams do not store data — they process it. Key differences: (1) Lazy evaluation — intermediate
ops don't run until terminal op called. (2) Single use — after terminal op, stream is consumed and cannot be
reused. (3) No modification — stream doesn't modify the underlying collection. (4) Internal iteration — you
describe what to do, JVM decides how. (5) Can be infinite with [Link]() or [Link]().
Follow-up Questions:
➤ Can you call stream() on an already iterated Stream?
→ No — you get IllegalStateException. You must create a new stream from the source collection.
➤ Why is internal iteration better than external?
→ JVM can optimize internally — apply short-circuit evaluation, use branch prediction, parallelize automatically.

Java Interview Notes • Page 32 • Mihir – Placement Prep 2026


Q2. Explain lazy evaluation in streams. Give an example.
Short: Intermediate ops do nothing until terminal op is called. Elements flow through pipeline one at a
time.
Detail: Lazy evaluation means that calling filter() or map() does not immediately process data — it just
registers the operation. Only when you call collect(), count(), findFirst() etc. does data flow through. This
enables two optimizations: (1) Short-circuit — findFirst() stops after first match, no need to process all
elements. (2) Fusion — filter and map on the same element before moving to next, reducing iterations from
N*2 to N.
Follow-up Questions:
➤ What are stateful vs stateless intermediate operations?
→ Stateless: filter, map, flatMap — process each element independently. Stateful: sorted, distinct, limit — need to
see multiple/all elements before producing output.

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


Short: map() transforms each element 1-to-1. flatMap() transforms each element into a stream and
flattens all streams into one.
Detail: map(Function<T,R>) takes each T and produces exactly one R. Result is Stream<R>.
flatMap(Function<T,Stream<R>>) takes each T, produces a Stream<R>, then flattens all those streams into a
single Stream<R>. Use flatMap when your mapping function produces a collection/stream per element and
you want a flat result. Classic example: List<List<Integer>> -> List<Integer>.
Follow-up Questions:
➤ Why does Optional also have flatMap()?
→ [Link]() is needed when the mapping function itself returns an Optional. Without it, you get
Optional<Optional<T>>. flatMap unwraps one layer.

Q4. When should you use parallel streams?


Short: Large datasets (10K+ elements), CPU-intensive stateless operations. Not for I/O, small data, or
shared mutable state.
Detail: Parallel streams split data using [Link]() and process chunks on multiple threads.
Use when: (1) Data is large enough to offset thread overhead (~10K+ elements), (2) Operations are
CPU-bound and stateless, (3) No ordering constraints (findAny vs findFirst), (4) No shared mutable state.
Avoid when: (1) Small data — overhead of splitting/merging > benefit, (2) I/O bound operations (threads block
on I/O, no speed gain), (3) Operations have side effects (race conditions), (4) Sequential order matters.
Follow-up Questions:
➤ What thread pool do parallel streams use?
→ [Link]() by default — shared across the JVM. You can use a custom pool by submitting
stream execution to it.

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


Short: findFirst() returns the first element in encounter order. findAny() returns any element — faster
in parallel.
Detail: In sequential streams, both typically return the same element. In parallel streams, findFirst() must
respect the encounter order — it waits for the first element in the original sequence. findAny() can return any
element from any thread that finishes first — much faster in parallel. Use findFirst() when order matters,
findAny() for maximum performance in parallel.

Java Interview Notes • Page 33 • Mihir – Placement Prep 2026


INTERVIEW TRAPS
TRAP: "Stream stores data like a collection" — FALSE. Stream processes data on the fly.
TRAP: "Intermediate ops are executed immediately" — FALSE. Lazy evaluation.
TRAP: "Parallel streams are always faster" — FALSE. Small data: overhead > benefit.
TRAP: "[Link]() is stateful" — TRUE. It must collect ALL elements before sorting — watch for memory
issues with huge streams.
TRAP: forEach() order in parallel streams — order is NOT guaranteed. Use forEachOrdered() if order matters.
TRAP: Modifying source collection inside stream pipeline — ConcurrentModificationException or undefined
behavior.

MEMORY TRICKS
Pipeline = Source -> Intermediate (lazy) -> Terminal (fires everything)
SIDE = Stateful Intermediate ops are: Sorted, dIstinct, limit, skip, pEek (debug only)
map = 1-to-1. flatMap = 1-to-N then flatten.
collect = box it up. reduce = fold it down.
Parallel streams: MORE data = more benefit. I/O bound = no benefit.

QUICK REVISION
Stream: Process data, not store data. Single-use. Lazy. Internal iteration.
Intermediate ops (lazy): filter, map, flatMap, sorted, distinct, limit, skip, peek
Terminal ops (trigger): collect, forEach, count, reduce, findFirst, anyMatch, min, max
map: 1-to-1. flatMap: 1-to-N then flatten into single stream
reduce(identity, binaryOp): fold stream to single value
Collectors: toList, toSet, toMap, groupingBy, partitioningBy, joining, counting
groupingBy: Map<K, List<T>>. partitioningBy: Map<Boolean, List<T>>
Parallel: parallelStream() or .parallel(). Uses ForkJoinPool. Best for big, CPU-intensive, stateless data.
Lazy evaluation: filter first (reject early), then map. Short-circuit with findFirst/anyMatch.

Java Interview Notes • Page 34 • Mihir – Placement Prep 2026


TOPIC 11

Optional<T>
Eliminate NullPointerException — the null-safe wrapper

1. Why Was Optional Introduced?


THE NULL PROBLEM
NullPointerException is the most common runtime exception in Java — Tony Hoare called null his "billion dollar
mistake".
Without Optional: every method that might return null forces the caller to write a null check.
Null checks get nested, forgotten, or silently skipped — leading to NPE in production.
Optional makes the "value might be absent" situation EXPLICIT in the method signature.
It forces the caller to handle the absence — you cannot accidentally treat an Optional as the actual value.

// NPE Problem vs Optional Solution


// BEFORE Optional — null everywhere, NPE risk
User user = [Link](id); // might return null
String city = [Link]().getCity(); // NPE if user or address is null
// WITH Optional — absence is explicit
Optional<User> userOpt = [Link](id);
String city = userOpt
.map(User::getAddress)
.map(Address::getCity)
.orElse("Unknown"); // safe — no NPE possible

2. Creating Optional Instances


Method When To Use Throws?

[Link](value) You are SURE value is not null NullPointerException if value is null

[Link](value) Value might or might not be null Never — wraps null as empty Optional

[Link]() You want to explicitly return "nothing" Never

// Creating Optionals
Optional<String> opt1 = [Link]("Hello"); // has value
Optional<String> opt2 = [Link](null); // NPE! Never do this
Optional<String> opt3 = [Link]("Hello"); // has value
Optional<String> opt4 = [Link](null); // empty Optional
Optional<String> opt5 = [Link](); // empty Optional

3. Consuming Optional — All Methods


Method Returns Behavior

get() T Returns value. Throws NoSuchElementException if empty. AVOID unless


sure.

isPresent() boolean True if value present. Use orElse chain instead of isPresent+get.

Java Interview Notes • Page 35 • Mihir – Placement Prep 2026


isEmpty() boolean True if empty (Java 11+). Opposite of isPresent().

orElse(T default) T Return value or default. Default is ALWAYS computed — even if value
present!

orElseGet(Supplier) T Return value or compute default lazily. Preferred over orElse for expensive
defaults.

orElseThrow(Supplier) T Return value or throw custom exception. Great for service layer validation.

ifPresent(Consumer) void Execute consumer only if value present. Else: nothing.

ifPresentOrElse() void Java 9+. If present: action. Else: other action.

map(Function) Optional<R> Transform value inside Optional. If empty: returns empty Optional.

flatMap(Function) Optional<R> Map to Optional<R>. Prevents Optional<Optional<R>>.

filter(Predicate) Optional<T> Keep value if predicate passes. Else: empty.

or(Supplier<Optional>) Optional<T> Java 9+. If empty, return another Optional from supplier.

stream() Stream<T> Java 9+. Empty Optional -> empty Stream. Present -> Stream of 1 element.

4. orElse vs orElseGet — Critical Difference


MOST ASKED OPTIONAL INTERVIEW QUESTION
orElse(T): The default value is ALWAYS computed/created, even if the Optional has a value.
orElseGet(Supplier<T>): The Supplier is called LAZILY — only if the Optional is empty.
Rule: If computing the default is expensive (DB call, network call, complex computation) — ALWAYS use
orElseGet.

// orElse vs orElseGet
Optional<User> user = [Link](new User("Alice"));
// orElse — computeDefault() ALWAYS called (even though user is present!)
User u1 = [Link](computeDefault()); // computeDefault() runs unnecessarily
// orElseGet — computeDefault() only called if user is empty
User u2 = [Link](() -> computeDefault()); // safe and efficient
// For simple constant defaults, both are fine:
String name = Optional.<String>empty().orElse("Unknown"); // OK, "Unknown" is cheap

5. Chaining Optional — Real World Example

Java Interview Notes • Page 36 • Mihir – Placement Prep 2026


// Optional Chaining
// Scenario: Get city of user's primary address, or "Unknown"
// Without Optional (NPE-prone)
User user = [Link](id);
if (user != null && [Link]() != null) {
return [Link]().getCity();
}
return "Unknown";
// With Optional (safe, readable)
return [Link](id) // Optional<User>
.map(User::getAddress) // Optional<Address>
.map(Address::getCity) // Optional<String>
.filter(city -> ![Link]()) // Optional<String>
.orElse("Unknown"); // String
// orElseThrow — for validation in service layer
User user = [Link](id)
.orElseThrow(() -> new UserNotFoundException("User " + id + " not found"));

6. Optional Best Practices


DO THIS
1. Use Optional as return type when a method might return "nothing".
2. Use orElseGet() over orElse() when default computation is expensive.
3. Use map/filter/flatMap for chaining — avoid isPresent() + get() pattern.
4. Use orElseThrow() in service layer to convert absent value to domain exception.

NEVER DO THIS
1. Never use Optional as a method parameter or field — it wastes memory and is not intended for that.
2. Never do [Link]() without checking isPresent() first — NoSuchElementException.
3. Never wrap null in [Link]() — use ofNullable() if null is possible.
4. Never use Optional for collections — return empty List/Set instead of Optional<List>.
5. Never serialize Optional fields — it is not Serializable.

Q1. What is Optional? Why was it introduced?


Short: A container that may or may not hold a non-null value. Introduced to avoid
NullPointerException.
Detail: Optional<T> is a value-based class in [Link] that wraps a value that might be absent. Without
Optional, methods returning "no result" return null — callers often forget to null-check. Optional makes the
absence explicit in the method signature, forcing callers to handle both cases. It enables functional-style
chaining with map/filter/orElse instead of nested if-null checks.
Follow-up Questions:
➤ Should Optional be used as method parameter?
→ No — Optional is designed as a return type only. As a parameter, it adds overhead and makes callers wrap
their value unnecessarily. Use method overloading or @Nullable instead.
➤ Is Optional Serializable?
→ No — Optional does not implement Serializable. Never use Optional as a field in a Serializable class.

Java Interview Notes • Page 37 • Mihir – Placement Prep 2026


Q2. Difference between orElse() and orElseGet()?
Short: orElse evaluates default always. orElseGet evaluates lazily — only when Optional is empty.
Detail: orElse(T default): The expression T default is evaluated eagerly — even if the Optional has a value. If
computing default is expensive (DB call, new object), this is wasteful. orElseGet(Supplier<T>): The Supplier
lambda is invoked only when Optional is empty — lazy evaluation. Always prefer orElseGet when the default
value is computationally expensive.
Follow-up Questions:
➤ When is orElse() safe to use?
→ When the default is a simple constant like null, "", 0, or a pre-created object. The evaluation cost is negligible.

Q3. What is the difference between [Link]() and [Link]()?


Short: map() wraps result in Optional. flatMap() expects the function to return Optional — avoids
Optional<Optional<T>>.
Detail: [Link](Function<T,R>): Applies function to value, wraps result in Optional. If function returns
null, you get empty Optional. [Link](Function<T,Optional<R>>): Used when your mapping function
itself returns an Optional. Without flatMap you'd get Optional<Optional<R>>. flatMap unwraps one layer.
Follow-up Questions:
➤ How does Optional chaining work with map?
→ Each map call operates on the value inside. If any step returns null or an empty Optional, the chain
short-circuits to empty. orElse/orElseGet at the end provides the fallback.

INTERVIEW TRAPS
TRAP: "orElse always returns the default" — NO. It returns the actual value if present. The issue is the default is
COMPUTED even when not returned.
TRAP: "[Link](null) returns empty Optional" — NO. It throws NullPointerException. Use ofNullable(null) for
empty.
TRAP: "Optional<List<T>> is good practice" — NO. Return empty list instead. Optional of collection =
redundant.
TRAP: "isPresent() + get() is the right pattern" — BAD practice. Use map/orElse chain instead.
TRAP: [Link]() without check — throws NoSuchElementException if empty. Java 11 has isEmpty() as
safer alternative.

MEMORY TRICKS
Optional = "maybe" container. Present = value exists. Empty = no value.
orElse = "or ELSE use this" (computed NOW). orElseGet = "or else GET from supplier" (computed LATER).
map = "transform if present". flatMap = "transform if present, result is already Optional".
of = "definitely not null". ofNullable = "might be null — handle it".
Never use as parameter/field. Never wrap collections. Never call get() without check.

Java Interview Notes • Page 38 • Mihir – Placement Prep 2026


QUICK REVISION
[Link](v): v must not be null. ofNullable(v): v can be null. empty(): explicitly empty.
get(): throws NoSuchElementException if empty — avoid.
orElse(default): always computes default. orElseGet(Supplier): lazy — compute only if empty.
orElseThrow(Supplier<Exception>): throw custom exception if empty.
ifPresent(Consumer): run action only if value exists.
map(Function): transform value, re-wrap in Optional.
flatMap(Function->Optional): for functions that return Optional, avoid double-wrapping.
filter(Predicate): keep value if condition met, else empty.
Do NOT: use as parameter, use as field, wrap collections, serialize.

Java Interview Notes • Page 39 • Mihir – Placement Prep 2026


TOPIC 12

CompletableFuture
Async programming in Java 8 — chain tasks without blocking

1. Why CompletableFuture?
PROBLEM WITH Future<T> (Java 5)
Future<T> was introduced in Java 5 for async tasks — but it had serious limitations.
[Link]() BLOCKS the calling thread — defeats the purpose of async.
Cannot chain multiple async tasks — no "when this is done, do that".
Cannot combine results of multiple futures easily.
No exception handling in the async chain.
Cannot manually complete a Future.

Aspect Future<T> (Java 5) CompletableFuture<T> (Java 8)

Blocking get() always blocks Non-blocking with thenApply, thenAccept

Chaining Cannot chain Full pipeline: thenApply -> thenAccept

Combining Manual, complex thenCombine(), allOf(), anyOf()

Exception Not built in exceptionally(), handle()


Handling

Manual Cannot complete(value), completeExceptionally()


Completion

Async execution Manual thread creation supplyAsync(), runAsync() built-in

2. Creating CompletableFutures
// Creating CompletableFutures
// supplyAsync — runs a Supplier asynchronously, returns a value
CompletableFuture<String> cf1 = [Link](() -> {
// runs in [Link]()
return fetchDataFromDB(); // heavy operation
});
// runAsync — runs a Runnable asynchronously, returns void (CompletableFuture<Void>)
CompletableFuture<Void> cf2 = [Link](() -> {
sendEmailNotification(); // fire and forget
});
// With custom Executor (thread pool)
ExecutorService pool = [Link](4);
CompletableFuture<String> cf3 = [Link](
() -> fetchData(), pool
);
// Already completed (useful for testing)
CompletableFuture<String> done = [Link]("result");

Java Interview Notes • Page 40 • Mihir – Placement Prep 2026


3. Chaining Operations
Method Input / Output Use When

thenApply(Function) T -> R (transform) Transform result to another type (like map)

thenAccept(Consumer) T -> void (consume) Process result without returning value

thenRun(Runnable) void -> void Do something after completion, ignore result

thenCompose(Function) T -> Chain dependent async tasks (flatMap equivalent)


CompletableFuture<R>

thenCombine(CF, Fn) T + U -> R Combine results of two independent futures

// Chaining CompletableFuture
// thenApply — transform the result (like [Link])
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "hello") // "hello"
.thenApply(String::toUpperCase) // "HELLO"
.thenApply(s -> s + "!"); // "HELLO!"
// thenAccept — consume result, return void
CompletableFuture<Void> consuming = CompletableFuture
.supplyAsync(() -> fetchUser(1))
.thenAccept(user -> [Link]("Got: " + [Link]()));
// thenRun — ignore result, run something
CompletableFuture<Void> notifier = CompletableFuture
.supplyAsync(() -> processOrder())
.thenRun(() -> [Link]("Order processing complete!"));
// thenCompose — chain DEPENDENT async tasks
// "Get user, THEN get their orders" (orders depend on user)
CompletableFuture<List<Order>> userOrders = CompletableFuture
.supplyAsync(() -> [Link](1)) // CF<User>
.thenCompose(user -> [Link](user)); // CF<List<Order>>
// Without thenCompose: would get CF<CF<List<Order>>>
// thenCombine — combine TWO INDEPENDENT async tasks
CompletableFuture<String> userName = [Link](() -> "Alice");
CompletableFuture<Integer> userAge = [Link](() -> 25);
CompletableFuture<String> combined = [Link](userAge,
(name, age) -> name + " is " + age + " years old.");
// "Alice is 25 years old." — both run in parallel!

4. Exception Handling

Java Interview Notes • Page 41 • Mihir – Placement Prep 2026


// Exception Handling
// exceptionally — handle exception, provide fallback value
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> {
if ([Link]() > 0.5) throw new RuntimeException("DB error!");
return "Data from DB";
})
.exceptionally(ex -> {
[Link]("Error: " + [Link]());
return "Default Data"; // fallback
});
// handle — handles both success and failure
CompletableFuture<String> handled = CompletableFuture
.supplyAsync(() -> fetchData())
.handle((data, ex) -> {
if (ex != null) return "Error: " + [Link]();
return [Link]();
});
// handle always runs. exceptionally only on failure.

5. Combining Multiple Futures

// allOf and anyOf


// allOf — wait for ALL to complete
CompletableFuture<String> f1 = [Link](() -> "Result 1");
CompletableFuture<String> f2 = [Link](() -> "Result 2");
CompletableFuture<String> f3 = [Link](() -> "Result 3");
CompletableFuture<Void> all = [Link](f1, f2, f3);
[Link](); // wait for all
// Then collect results:
String r1 = [Link](); String r2 = [Link](); String r3 = [Link]();
// anyOf — complete when the FIRST one finishes
CompletableFuture<Object> first = [Link](f1, f2, f3);
[Link]("First result: " + [Link]());

6. get() vs join()
Method Throws Use In

get() InterruptedException, ExecutionException Code that handles checked exceptions


(checked)

join() CompletionException (unchecked) Simpler code, streams — no checked exception handling


needed

7. Real World Example — Microservice Aggregator

Java Interview Notes • Page 42 • Mihir – Placement Prep 2026


// Parallel Service Calls
// Aggregate data from 3 services IN PARALLEL
// Sequential: takes 300ms (100+100+100)
// Parallel: takes ~100ms (all run simultaneously)
public UserDashboard buildDashboard(int userId) {
CompletableFuture<User> userFuture =
[Link](() -> [Link](userId));
CompletableFuture<List<Order>> ordersFuture =
[Link](() -> [Link](userId));
CompletableFuture<List<Product>> recsFuture =
[Link](() -> [Link](userId));
// Wait for ALL three
[Link](userFuture, ordersFuture, recsFuture).join();
return new UserDashboard(
[Link](),
[Link](),
[Link]()
);
}

Q1. What is CompletableFuture? How is it better than Future?


Short: Non-blocking async framework in Java 8. Supports chaining, combining, exception handling —
Future has none of these.
Detail: CompletableFuture<T> implements both Future<T> and CompletionStage<T>. Unlike Future, it
supports: (1) Non-blocking chaining with thenApply/thenAccept/thenRun, (2) Combining multiple async results
with thenCombine/allOf/anyOf, (3) Exception handling with exceptionally/handle, (4) Manual completion with
complete(), (5) Flexible execution with custom Executor. Future forces blocking via get().
Follow-up Questions:
➤ Which thread pool does supplyAsync use by default?
→ [Link]() — the same shared pool as parallel streams. You can pass a custom Executor as
second argument.
➤ What is the difference between thenApply and thenApplyAsync?
→ thenApply runs the function in the SAME thread that completed the future. thenApplyAsync submits to
ForkJoinPool (or custom executor). Use Async variants when the callback is heavy.

Q2. Difference between thenApply, thenAccept, and thenRun?


Short: thenApply transforms (T->R). thenAccept consumes (T->void). thenRun ignores result and runs
(void->void).
Detail: All three chain after a CompletableFuture completes. thenApply(Function<T,R>): takes result T, returns
new value R. Produces CompletableFuture<R>. Like [Link](). thenAccept(Consumer<T>): takes result
T, does something, returns CompletableFuture<Void>. thenRun(Runnable): ignores result, just runs after
completion, returns CompletableFuture<Void>.
Follow-up Questions:
➤ When to use thenRun?
→ When you want to trigger a side effect after completion — like logging, metrics recording, sending a notification
— but do not need the actual result.

Java Interview Notes • Page 43 • Mihir – Placement Prep 2026


Q3. Difference between thenCompose and thenCombine?
Short: thenCompose: dependent tasks (sequential). thenCombine: independent tasks (parallel merge).
Detail: thenCompose(Function<T, CompletableFuture<R>>): Like flatMap — for DEPENDENT async tasks.
The second task depends on the result of the first. thenCombine(CompletableFuture<U>,
BiFunction<T,U,R>): For INDEPENDENT async tasks. Both run in parallel, and when BOTH complete,
combine their results. Use thenCompose for "get user, THEN get their orders". Use thenCombine for "get user
data AND get recommendations simultaneously, merge them."
Follow-up Questions:
➤ What is the difference between thenCompose and thenApply?
→ thenApply expects a Function that returns R. thenCompose expects a Function that returns
CompletableFuture<R>. Use thenCompose when the next step is itself async — otherwise you get CF<CF<R>>.

Q4. What is the difference between exceptionally() and handle()?


Short: exceptionally: only runs on exception. handle: always runs — receives either result or
exception.
Detail: exceptionally(Function<Throwable, T>): Called only when the future completes exceptionally. Provides
a fallback value. handle(BiFunction<T, Throwable, R>): Called in BOTH success and failure cases. One of T
or Throwable will be null. More flexible — you can inspect both success and failure in one place.

INTERVIEW TRAPS
TRAP: "CompletableFuture is always non-blocking" — WRONG. Calling get() or join() still blocks the calling
thread. Non-blocking = chaining with callbacks.
TRAP: "thenApply and thenApplyAsync are the same" — WRONG. thenApply uses completing thread.
thenApplyAsync submits to pool.
TRAP: "exceptionally handles all exceptions" — It handles exceptions from the ENTIRE preceding chain, not just
one step.
TRAP: supplyAsync swallows exceptions silently if you don't add exceptionally/handle.
TRAP: allOf().join() — allOf itself returns CF<Void>. You still need individual .join() or .get() on each future to
retrieve values.

MEMORY TRICKS
supplyAsync = "supply a value async" (Supplier -> T). runAsync = "run async" (Runnable -> void).
thenApply/Accept/Run — think Stream: map/forEach/run-after.
thenCompose = flatMap (for dependent tasks). thenCombine = zip (independent parallel tasks).
exceptionally = catch block. handle = finally block (always runs).
get() = checked exception. join() = unchecked. In streams, use join().

QUICK REVISION
Future<T> limitations: blocking get(), no chaining, no exception handling.
supplyAsync(Supplier): async with return value. runAsync(Runnable): async, no return.
thenApply(Function<T,R>): transform result. thenAccept(Consumer): consume. thenRun(Runnable): side effect.
thenCompose: dependent async chain (flatMap). thenCombine: parallel merge of two.
exceptionally(Throwable->T): fallback on error. handle(T,Throwable->R): always runs.
allOf(CF...): wait for all. anyOf(CF...): wait for first.
get() throws checked. join() throws unchecked CompletionException.
Default thread pool: [Link](). Pass Executor for custom pool.

Java Interview Notes • Page 44 • Mihir – Placement Prep 2026


TOPIC 13

HashMap Internal Working


How O(1) lookup works — hashing, buckets, collision, treeification

1. What is HashMap?
OVERVIEW
HashMap<K,V> is a hash table based implementation of the Map<K,V> interface.
Stores key-value pairs. Allows one null key. NOT thread-safe.
Average O(1) for put(), get(), remove(). Worst case O(n) before Java 8, O(log n) with Java 8+ treeification.
Backed by an array of Node (linked list nodes, or TreeNodes in Java 8+).

2. Internal Structure
// HashMap Internal Structure
// HashMap internal representation (simplified)
HashMap<K,V>
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ Node<K,V>[] table (the array of buckets) ■
■ int size (number of key-value pairs) ■
■ int threshold (size at which to resize) ■
■ float loadFactor (default 0.75) ■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
table (default capacity = 16):
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■[0] ■[1] ■[2] ■[3] ■[4] ■[5] ■[6] ■...■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ ■ ■
Node Node Node -> Node -> Node (collision chain)
key="A" key="B" key="C" key="X"
val=1 val=2 val=3 val=9
Node structure:
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ int hash ■
■ K key ■
■ V value ■
■ Node<K,V> next (linked list)■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

3. How put(key, value) Works — Step by Step


1. Call [Link]() — gets the 32-bit hash of the key.
2. Apply hash spreading: hash = hash ^ (hash >>> 16) — reduces collisions by mixing high and low bits.
3. Calculate bucket index: index = hash & (capacity - 1) (equivalent to hash % capacity, but faster).
4. Go to table[index]. If null: create new Node and place it there. Done.
5. If NOT null (collision exists): iterate through the linked list at that bucket.
6. a. For each existing node: compare hash AND call [Link](existingKey).

Java Interview Notes • Page 45 • Mihir – Placement Prep 2026


7. b. If match found: UPDATE the value (key already exists).
8. c. If no match after iterating all: ADD new Node at end of the list.
9. If bucket's chain length exceeds TREEIFY_THRESHOLD (8): convert linked list to Red-Black Tree.
10. If size > threshold (capacity * loadFactor): REHASH — double array size, redistribute all entries.

// put() Simplified Implementation


// Simplified put() logic:
public V put(K key, V value) {
int hash = hash([Link]()); // spread hash
int index = hash & ([Link]-1); // bucket index
Node node = table[index];
if (node == null) {
table[index] = new Node(hash, key, value, null);
} else {
// Walk the chain
for (Node n = node; n != null; n = [Link]) {
if ([Link] == hash && ([Link] == key || [Link]([Link]))) {
V old = [Link];
[Link] = value; // UPDATE
return old;
}
}
// Not found — append to chain
appendToChain(table[index], new Node(hash, key, value, null));
if (chainLength > TREEIFY_THRESHOLD) treeify(index);
}
if (++size > threshold) resize(); // rehash
return null;
}

4. How get(key) Works

// get() Implementation
public V get(Object key) {
int hash = hash([Link]());
int index = hash & ([Link] - 1);
Node node = table[index];
while (node != null) {
if ([Link] == hash && ([Link] == key || [Link]([Link])))
return [Link]; // FOUND
node = [Link];
}
return null; // NOT FOUND
}
// get() is O(1) average because:
// 1. hash() + index calculation: O(1)
// 2. table[index] lookup: O(1) array access
// 3. Collision chain traversal: O(1) average (small chain if good hash)

5. hashCode() and equals() Contract — CRITICAL

Java Interview Notes • Page 46 • Mihir – Placement Prep 2026


THE MOST IMPORTANT CONTRACT IN HASHMAP
Rule 1: If [Link](b) is true, then [Link]() == [Link]() MUST be true.
Rule 2: If [Link]() == [Link](), [Link](b) MAY or MAY NOT be true (collision is OK).
Rule 3: You must override BOTH hashCode() and equals() together — never just one.
Breaking this contract: Two equal objects land in DIFFERENT buckets — get() returns null even though key
exists!

// hashCode() and equals() Contract


// WRONG — only overrides equals, not hashCode
class Employee {
String name;
@Override
public boolean equals(Object o) {
return ((Employee) o).[Link]([Link]);
}
// NO hashCode override!
}
Map<Employee, String> map = new HashMap<>();
Employee e1 = new Employee("Alice");
[Link](e1, "Engineer");
Employee e2 = new Employee("Alice"); // same name, equals() returns true
[Link]([Link](e2)); // NULL! different hashCode -> different bucket
// CORRECT — always override both
@Override
public int hashCode() { return [Link](name); }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
return [Link](name, ((Employee) o).name);
}

6. Collision and Chaining

WHAT IS A COLLISION?
A collision happens when two DIFFERENT keys hash to the SAME bucket index.
HashMap handles this with separate chaining — each bucket holds a linked list of nodes.
Multiple key-value pairs with different keys but same bucket index coexist in the same chain.
Performance degrades when many collisions happen — O(n) traversal in worst case.
Good hashCode() distributes keys uniformly across buckets — minimizes collisions.

7. Load Factor and Rehashing

Java Interview Notes • Page 47 • Mihir – Placement Prep 2026


LOAD FACTOR — CONTROLS RESIZE TRIGGER
Load Factor (default 0.75) = threshold for resizing = capacity * loadFactor.
With capacity=16, loadFactor=0.75: resize triggers when size > 12.
Rehashing: create new array (double size), recompute index for every existing entry, re-insert all.
Higher loadFactor: more memory efficient, but more collisions, slower.
Lower loadFactor: fewer collisions, faster lookup, but wastes memory.
0.75 is the optimal balance between time and space complexity.

// HashMap Constants
// Default values
int DEFAULT_INITIAL_CAPACITY = 16; // must be power of 2
float DEFAULT_LOAD_FACTOR = 0.75f;
int TREEIFY_THRESHOLD = 8; // Java 8: convert list to tree
int UNTREEIFY_THRESHOLD = 6; // Java 8: convert tree back to list
int MIN_TREEIFY_CAPACITY = 64; // min table size before treeifying
// threshold = capacity * loadFactor
// resize when: size > threshold
// new capacity = old capacity * 2 (always power of 2)
// Pre-sizing tip: if you know you will store N elements:
// initialCapacity = (int)(N / 0.75) + 1 to avoid rehashing
new HashMap<>(128); // for storing ~96 elements without resize

8. Java 7 vs Java 8 — Critical Changes


Aspect Java 7 Java 8

Bucket Structure Always Linked List Linked List -> Red-Black Tree when chain > 8

Worst Case get() O(n) — full chain traversal O(log n) — tree traversal after treeification

Hash Spreading Basic hash() function Improved: hash ^ (hash >>> 16)

Insertion in chain Head insertion (prepend) Tail insertion (append) — avoids infinite loop in concurrent
resize

Resize Entry[] table Node[] table with TreeNode for trees

Concurrency bug Infinite loop possible during Fixed — tail insertion prevents cycle
concurrent resize

9. Why O(1) on Average?

Java Interview Notes • Page 48 • Mihir – Placement Prep 2026


// O(1) Average Complexity Explanation
// Average O(1) reasoning:
// With good hash distribution and load factor 0.75:
// - 16 buckets, 12 entries max before resize
// - Expected chain length = size / capacity = 12/16 = 0.75
// - Traversing a chain of avg length 0.75 is effectively O(1)
// O(1) breaks down when:
// 1. hashCode() always returns same value (all keys in one bucket)
// 2. hashCode() is poorly distributed (many collisions)
// Java 8 mitigates worst case with treeification -> O(log n)
// Best hashCode practices:
// 1. Use all significant fields
// 2. Use [Link](field1, field2, ...) — handles nulls, prime multiplication
// 3. [Link]() uses polynomial: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

Q1. Explain how HashMap works internally.


Short: Array of buckets. Key's hashCode() determines bucket index. Collisions handled by linked list.
Java 8: tree when chain > 8.
Detail: HashMap uses an array (Node[]) where each slot is a bucket. When you put(key, value): (1)
hashCode() is called on key, then hash spread function applied. (2) Bucket index = hash & (capacity-1). (3) If
bucket empty: insert new Node. (4) If occupied (collision): iterate chain, check hash AND equals(). If key
found: update value. Else: append new Node. (5) If chain length > 8 (Java 8): convert to Red-Black Tree. (6) If
size > threshold (capacity * 0.75): rehash — double size, re-insert all.
Follow-up Questions:
➤ Why is capacity always a power of 2?
→ So that index = hash & (capacity-1) is equivalent to hash % capacity — bitwise AND is much faster than
modulus. Power of 2 means capacity-1 is all 1-bits: 15 = 0b1111, 31 = 0b11111, etc.
➤ What happens if two keys have same hashCode?
→ They land in the same bucket — a collision. HashMap iterates the chain and uses equals() to distinguish them.
Both can coexist in the same bucket as separate nodes in the linked list.

Q2. Why must you override both hashCode() and equals()?


Short: Because HashMap uses hashCode to find the bucket and equals to find the exact key. Breaking
either half breaks lookup.
Detail: If you override equals() but not hashCode(): two equal objects may have different hashCodes (using
default [Link]() = memory address). HashMap puts them in DIFFERENT buckets. get() uses
hashCode to find the bucket — it finds an empty bucket and returns null, even though the key "logically"
exists. If you override hashCode() but not equals(): all equal-looking objects would go to the same bucket, but
equals() would still use reference comparison — different object instances never found equal.
Follow-up Questions:
➤ Is it safe to use mutable objects as HashMap keys?
→ Dangerous. If you change a field that affects hashCode after insertion, the object's hash changes. HashMap
looks in the wrong bucket. The entry becomes unreachable — a memory leak.

Java Interview Notes • Page 49 • Mihir – Placement Prep 2026


Q3. What is the difference between HashMap, LinkedHashMap, and TreeMap?
Short: HashMap: no order, O(1). LinkedHashMap: insertion order maintained, O(1). TreeMap: sorted by
key, O(log n).
Detail: HashMap: no guaranteed iteration order. Best performance O(1) avg. LinkedHashMap: maintains
insertion order (or access order for LRU cache). Uses a doubly linked list alongside the hash table. O(1) for
most operations. TreeMap: keys are always sorted in natural order or by custom Comparator. Uses Red-Black
Tree internally. O(log n) for all operations. Use when you need sorted keys or range queries (headMap,
tailMap, subMap).
Follow-up Questions:
➤ How to implement LRU Cache using LinkedHashMap?
→ Create LinkedHashMap with accessOrder=true in constructor. Override removeEldestEntry() to return true
when size > capacity. Each get() moves accessed entry to tail; removeEldestEntry removes the head (LRU).

Q4. What is the default initial capacity and load factor of HashMap?
Short: 16 initial capacity, 0.75 load factor. Resize (rehash) when size > 12.
Detail: Default initial capacity = 16 (power of 2). Default load factor = 0.75. Threshold = 16 * 0.75 = 12. When
the 13th entry is added, HashMap doubles capacity to 32 and rehashes all entries. This is an expensive O(n)
operation — all keys are re-indexed. To avoid rehashing, pre-size: new HashMap<>(initialCapacity, 0.75f)
where initialCapacity = expectedSize / 0.75 + 1.
Follow-up Questions:
➤ What is the time complexity of rehashing?
→ O(n) — all n existing entries must be re-hashed and re-inserted into the new array. This is why pre-sizing is
important for performance-critical code.

Q5. What change did Java 8 make to HashMap? Why?


Short: Java 8 converts linked list to Red-Black Tree when chain length > 8. Worst case improves from
O(n) to O(log n).
Detail: In Java 7, a collision chain was always a linked list. With a bad hashCode() (e.g., always returns 0), all
entries pile into one bucket — O(n) for all operations. Hash flooding attacks exploited this (DoS via crafted
keys). Java 8 added treeification: when chain length > TREEIFY_THRESHOLD (8) and table size >= 64, the
chain converts to a Red-Black Tree (TreeNode). Tree lookup is O(log n). When entries are removed and chain
shrinks < UNTREEIFY_THRESHOLD (6), it reverts to linked list.
Follow-up Questions:
➤ Why 8 as the treeify threshold?
→ Statistical analysis: with good hash distribution and load factor 0.75, probability of a chain exceeding 8 is
~0.00006%. So treeification is rare — only needed for degenerate cases (attack or bad hash).

INTERVIEW TRAPS
TRAP: "HashMap is thread-safe" — FALSE. Use ConcurrentHashMap for thread safety.
TRAP: "HashMap does not allow duplicate keys" — TRUE. But it allows duplicate VALUES. Inserting same key
twice UPDATES the value.
TRAP: "HashMap maintains insertion order" — FALSE. Use LinkedHashMap for insertion order.
TRAP: "hashCode() equal means keys are equal" — FALSE. hashCode collision is normal. equals() is what
determines key equality.
TRAP: "HashMap allows multiple null keys" — FALSE. Only ONE null key is allowed (it maps to index 0).
Multiple null values are allowed.
TRAP: "Changing mutable key after insertion is safe" — DANGEROUS. HashCode changes, entry becomes
unreachable.

Java Interview Notes • Page 50 • Mihir – Placement Prep 2026


MEMORY TRICKS
HashMap = Array of Buckets + Linked List (+ Tree in Java 8)
Flow: hashCode -> spread -> & (capacity-1) -> bucket -> walk chain with equals()
Load Factor 0.75: 75% full = resize. Balances speed vs memory.
"Treeify at 8, Untreeify at 6" — hysteresis prevents rapid flip-flopping.
Always override BOTH hashCode AND equals — the "contract twins".
Java 7: Head insert (caused concurrent resize loop). Java 8: Tail insert (fixed it).
For DSA: HashMap internal = hashing + chaining. O(1) average depends on uniform hash.

QUICK REVISION
Structure: Node<K,V>[] table. Node has: hash, key, value, next.
put(): hash -> index -> check chain -> insert or update -> treeify if needed -> rehash if needed.
get(): hash -> index -> walk chain with hash check + equals() -> return value.
hashCode() + equals() contract: must override both together.
Default: capacity=16, loadFactor=0.75, threshold=12.
Collision: same bucket index for different keys. Handled by chaining.
Java 8: chain > 8 -> Red-Black Tree. Worst case O(n) -> O(log n).
Capacity always power of 2. index = hash & (capacity-1).
HashMap vs LinkedHashMap vs TreeMap: no order / insertion order / sorted order.
Thread safe alternative: ConcurrentHashMap (not Hashtable — synchronized is slow).

Java Interview Notes • Page 51 • Mihir – Placement Prep 2026


TOPIC 14

Comparable vs Comparator
Natural sorting vs custom sorting — know when to use which

1. The Core Difference


Aspect Comparable<T> Comparator<T>

Package [Link] [Link]

Method int compareTo(T other) int compare(T o1, T o2)

Implemented by The class being sorted (self) A separate class or lambda

Sorting type Natural ordering (one definition) Custom ordering (multiple possible)

Modifies class? YES — class must implement it NO — external, no class change needed

Flexibility One sort order only Multiple sort orders possible

Used by [Link](list), TreeSet/TreeMap [Link](list, comparator),


[Link](comparator)

Lambda support No (compareTo is on the object) YES — Comparator IS a functional interface

2. Comparable — Natural Ordering


WHAT compareTo() MUST RETURN
Negative integer: this < other (this comes BEFORE other)
Zero: this == other (equal — same position)
Positive integer: this > other (this comes AFTER other)
Rule of thumb: return [Link] - [Link] for numeric comparison (careful of overflow for large ints).

// Comparable Implementation
public class Employee implements Comparable<Employee> {
private String name;
private int salary;
private int age;
// Natural order = by salary ascending
@Override
public int compareTo(Employee other) {
return [Link]([Link], [Link]);
// [Link] is safe — avoids integer overflow
// return [Link] - [Link]; // RISKY for large values
}
}
// Usage
List<Employee> emps = getEmployees();
[Link](emps); // sorts by natural order (salary)
[Link](null); // same — null means natural order
// TreeSet uses natural order automatically
TreeSet<Employee> sorted = new TreeSet<>(emps);

Java Interview Notes • Page 52 • Mihir – Placement Prep 2026


3. Comparator — Custom Sorting
// Comparator Usage
// Multiple Comparators for same class (no class modification)
// Sort by NAME
Comparator<Employee> byName = [Link](Employee::getName);
// Sort by SALARY descending
Comparator<Employee> bySalary = [Link](Employee::getSalary)
.reversed();
// Sort by DEPARTMENT then by NAME (multi-key sort)
Comparator<Employee> byDeptThenName =
[Link](Employee::getDept)
.thenComparing(Employee::getName);
// Null-safe sort (nulls last)
Comparator<Employee> nullSafe =
[Link](Employee::getName,
[Link]([Link]()));
// Usage
[Link](byName);
[Link](bySalary);
[Link](byDeptThenName);
// In Streams
[Link]()
.sorted(byDeptThenName)
.forEach([Link]::println);
// TreeMap with custom Comparator
TreeMap<Employee, String> map =
new TreeMap<>([Link](Employee::getName));

4. compare() Return Value Rules


Return Value Meaning Example

Negative (< 0) o1 comes BEFORE o2 compare("Alice","Bob") < 0 -> Alice first

Zero (0) o1 and o2 are equal compare(5, 5) == 0

Positive (> 0) o1 comes AFTER o2 compare("Bob","Alice") > 0 -> Bob second

5. Comparator Utility Methods (Java 8+)


Method What It Does

[Link](keyExtractor) Sort by a field using its natural order

[Link]/Long/Double(fn) Primitive specialization — avoids boxing

[Link]() Reverse the sort order

[Link](next) Secondary sort when primary keys are equal

[Link]() Uses [Link]() — natural order

[Link]() Opposite of natural order

[Link](c) Nulls come before non-null values

[Link](c) Nulls come after non-null values

Java Interview Notes • Page 53 • Mihir – Placement Prep 2026


6. Real Interview Example — Multi-Criteria Sort

// Multi-Criteria Sort
// Sort employees: by department ASC, then by salary DESC, then by name ASC
Comparator<Employee> multiSort =
[Link](Employee::getDepartment) // 1st: dept ASC
.thenComparingInt(Employee::getSalary) // 2nd: salary ASC
.reversed() // flip both
.thenComparing(Employee::getName); // 3rd: name ASC
// Note: reversed() flips dept and salary. Name added after, unaffected.
// CORRECT approach (avoid reversed() on multi-level):
Comparator<Employee> correct =
[Link](Employee::getDepartment) // dept ASC
.thenComparing([Link](Employee::getSalary).reversed()) // salary DESC
.thenComparing(Employee::getName); // name ASC
[Link]()
.sorted(correct)
.collect([Link]());

Q1. What is the difference between Comparable and Comparator?


Short: Comparable: class defines its own natural order (compareTo). Comparator: external class
defines custom order (compare).
Detail: Comparable<T> is implemented by the class itself — it defines "natural ordering". compareTo() is
called on the object itself. Used by default sort methods. Comparator<T> is a separate functional interface —
defines a specific ordering externally without modifying the original class. Supports multiple sort orders,
chaining, and reversal. Use Comparable when there is one obvious natural order (like numbers, dates). Use
Comparator when you need multiple orderings or cannot modify the class.
Follow-up Questions:
➤ Can a class implement Comparable and still use Comparator?
→ Yes. The Comparable defines natural order used by default. Comparator can override it when explicitly passed
to sort methods.
➤ Is Comparator a functional interface?
→ Yes — it has one abstract method: compare(T o1, T o2). It can be implemented with a lambda: (a, b) ->
[Link]().compareTo([Link]()).

Q2. How does [Link]() use Comparable internally?


Short: It calls [Link]() with a merge sort (TimSort). Objects must implement Comparable or a
Comparator must be provided.
Detail: [Link](list) calls [Link](null), which calls [Link]() using TimSort (a hybrid
merge/insertion sort, O(n log n)). When no Comparator is provided, elements must implement Comparable.
The sort compares elements via compareTo(). If elements don't implement Comparable and no Comparator
provided: ClassCastException at runtime.
Follow-up Questions:
➤ What sorting algorithm does Java use internally?
→ TimSort for objects ([Link] for Object[]). Dual-Pivot Quicksort for primitive arrays (int[], long[], etc.) — more
cache-friendly for primitives.

Java Interview Notes • Page 54 • Mihir – Placement Prep 2026


Q3. How do you sort a list by multiple criteria?
Short: Use [Link]().thenComparing() chain.
Detail: Java 8 Comparator has thenComparing() for secondary sort: when primary keys are equal, it applies
the secondary comparator. Example:
[Link](Employee::getDept).thenComparing(Employee::getName) sorts by department first;
within same department, sorts by name. You can chain multiple thenComparing() calls and apply .reversed()
on any level.
Follow-up Questions:
➤ Does reversed() on a chained comparator affect all previous comparisons?
→ YES — reversed() reverses the ENTIRE accumulated comparator including all prior thenComparing() calls.
Add keys you don't want reversed AFTER calling reversed().

INTERVIEW TRAPS
TRAP: "return [Link] - [Link] is safe in compareTo" — WRONG for large values. Integer overflow:
-2147483648 - 1 = +2147483647. Use [Link]([Link], [Link]) instead.
TRAP: "Comparator has 2 abstract methods: compare() and equals()" — equals() comes from Object.
Comparator has only ONE abstract method: compare(). It IS a functional interface.
TRAP: "reversed() only reverses the last thenComparing()" — FALSE. reversed() flips the entire comparator
including all chained comparisons before it.
TRAP: "TreeMap uses Comparator for key equality" — TRUE. TreeMap uses compare/compareTo for BOTH
ordering AND equality. It does NOT call equals/hashCode. Two keys that compare to 0 are treated as the same
key.
TRAP: Comparable compareTo violating contract (not consistent with equals) causes wrong behavior in sorted
collections.

MEMORY TRICKS
Comparable = "I compare myself" (self-aware). Comparator = "I compare others" (external judge).
compareTo() = 1 argument (comparing to one other). compare() = 2 arguments (comparing two others).
Negative = comes first. Positive = comes after. Zero = equal.
Natural order = Comparable. Custom order = Comparator.
[Link](a, b) is ALWAYS safe. a - b overflows for large values.

QUICK REVISION
Comparable: [Link]. int compareTo(T other). Class defines own order. One ordering only.
Comparator: [Link]. int compare(T o1, T o2). External. Multiple orderings. Functional interface.
Return: negative = o1 before o2. zero = equal. positive = o1 after o2.
[Link](keyFn).thenComparing(keyFn2).reversed()
[Link](a,b) over (a-b) — avoids overflow.
TreeMap/TreeSet: use compareTo or Comparator for ordering AND equality (not equals()).
nullsFirst(), nullsLast() — handle null keys/values safely.

Java Interview Notes • Page 55 • Mihir – Placement Prep 2026


COMPLETE INTERVIEW REVISION GUIDE
One-Liners • Comparison Tables • 1-Day & 3-Day Plans • Company Q&A;

One-Line Definitions
Reflection Inspect and modify class structure at runtime using [Link].

Generics Compile-time type safety with type parameters; erased at runtime (type erasure).

Lambda Anonymous function that implements a functional interface; compiled via


invokedynamic.

Functional Interface Interface with exactly ONE abstract method; target type for lambdas.

Predicate<T> boolean test(T t) — evaluates a condition; compose with and/or/negate.

Consumer<T> void accept(T t) — consumes a value, returns nothing; chain with andThen.

Supplier<T> T get() — takes nothing, returns a value; enables lazy evaluation.

Function<T,R> R apply(T t) — transforms T to R; chain with andThen/compose.

Method Reference Shorthand for lambda calling existing method: ClassName::method or


instance::method.

Stream API Lazy, single-use pipeline for processing data; intermediate + terminal ops.

Optional<T> Null-safe container; forces handling of absent values; eliminates NPE.

CompletableFuture Non-blocking async framework; chain tasks with


thenApply/thenCompose/exceptionally.

HashMap Hash table: hashCode -> bucket -> equals chain; O(1) avg; treeify at chain>8 (Java 8).

Comparable Class implements int compareTo(T): defines natural ordering.

Comparator External int compare(T,T): custom ordering; functional interface; chainable.

Master Comparison Table


Interface Method Input Output Use Case Chain Methods

Predicate<T> test(T) T boolean Filter / Validate and, or, negate

Consumer<T> accept(T) T void Consume / Side-effect andThen

Supplier<T> get() none T Produce / Factory none

Function<T,R> apply(T) T R Transform andThen, compose

UnaryOperator apply(T) T T Same-type transform andThen, compose

BinaryOperator apply(T,T) T,T T Combine two values andThen

Java Interview Notes • Page 56 • Mihir – Placement Prep 2026


BiFunction apply(T,U) T,U R Two-arg transform andThen

BiConsumer accept(T,U) T,U void Two-arg consume andThen

BiPredicate test(T,U) T,U boolean Two-arg filter and, or, negate

Runnable run() none void Task (no result) none

Callable<V> call() none V Task with result none

Frequently Confused Concepts


orElse vs orElseGet
orElse ALWAYS evaluates default. orElseGet = lazy, only on empty. Use orElseGet for expensive defaults.

map vs flatMap (Stream)


map: 1-to-1 transform. flatMap: 1-to-N and flatten. Use flatMap for nested collections.

map vs flatMap (Optional)


[Link] wraps result. flatMap for when function returns Optional (avoids Optional<Optional<T>>).

thenCompose vs thenCombine
thenCompose = dependent sequential. thenCombine = independent parallel merge.

thenApply vs thenApplyAsync
thenApply uses completing thread. thenApplyAsync submits to ForkJoinPool.

findFirst vs findAny
findFirst: first in order. findAny: any (faster in parallel, non-deterministic).

? extends vs ? super
extends = read only (producer). super = write ok (consumer). PECS rule.

Comparable vs Comparator
Comparable = self-sorting (compareTo). Comparator = external judge (compare).

getDeclaredMethods vs getMethods
getDeclared = ALL methods of THIS class only. getMethods = only PUBLIC, incl. inherited.

Stream vs Collection
Stream = process only, lazy, single-use, no storage. Collection = stores, reusable, eager.

reduce vs collect
reduce = fold to single value (sum, product). collect = accumulate to container (List, Map).

groupingBy vs partitioningBy
groupingBy: Map<K, List<T>>. partitioningBy: Map<Boolean, List<T>> (split on predicate).

Java Interview Notes • Page 57 • Mihir – Placement Prep 2026


Most Asked Interview Questions by Topic
■ Reflection
• What is Reflection? How does Spring use it?
• What does setAccessible(true) do? Is it safe?
• getDeclaredMethods() vs getMethods() difference?
• Why is Reflection slow?

■ Generics
• What is Type Erasure? Why does it exist?
• Explain PECS with an example.
• Why can't you pass List<String> where List<Object> is expected?
• What cannot you do because of type erasure? (new T[], instanceof List<String>)

■ Lambda
• How is a lambda different from an anonymous class internally?
• What is effectively final? Why does it matter?
• What does this refer to inside a lambda?

■ Stream API
• What is lazy evaluation in Streams?
• map() vs flatMap() — difference and examples.
• When to use parallel streams? When not to?
• reduce() vs collect() — when to use which?
• groupingBy() vs partitioningBy() difference.

■ Optional
• orElse() vs orElseGet() — key difference?
• [Link]() vs ofNullable() vs empty()?
• When should you NOT use Optional?
• [Link]() vs flatMap()?

■ CompletableFuture
• CompletableFuture vs Future — key improvements?
• thenApply vs thenAccept vs thenRun?
• thenCompose vs thenCombine?
• exceptionally() vs handle()?
• What thread pool does supplyAsync use?

■ HashMap
• Explain HashMap internal working.
• Why must you override both hashCode() and equals()?
• What is Java 8 treeification? Why?
• What is load factor? When does rehashing happen?

Java Interview Notes • Page 58 • Mihir – Placement Prep 2026


• HashMap vs ConcurrentHashMap?

■ Comparable vs Comparator
• When to use Comparable vs Comparator?
• Why use [Link]() over (a-b)?
• How to sort by multiple fields?
• Does reversed() affect all prior thenComparing() chains?

Java Interview Notes • Page 59 • Mihir – Placement Prep 2026


3-Day Revision Plan

DAY 1 — Foundations + Collections


• Morning (2h): Reflection + Generics (definitions, type erasure, PECS) • Afternoon (2h): Lambda + Functional
Interfaces + Predicate + Consumer • Evening (1.5h): Supplier + Function + Method References • Night (1h):
Write code for each — make 5 Predicates, 3 Functions, chain them

DAY 2 — Java 8 Core + Data Structures


• Morning (2h): Stream API full — pipeline, all intermediate/terminal ops • Afternoon (1.5h): Optional — all
methods, orElse vs orElseGet, map/flatMap • Evening (2h): HashMap deep dive — internal, hashCode/equals,
Java 7 vs 8 • Night (1h): Comparable vs Comparator — implement multi-field sort

DAY 3 — Async + Mock Interviews


• Morning (2h): CompletableFuture — all methods with examples • Afternoon (2h): Revision of confused
concepts table + one-liner sheet • Evening (2h): Practice answering all "Most Asked" questions out loud • Night
(1h): Review Interview Traps from each topic — do not get caught

1-Day Revision Plan (Emergency!)


1. Hour 1: One-liner sheet — read all definitions aloud. Fix any gaps.
2. Hour 2: HashMap internals + hashCode/equals contract — most asked topic.
3. Hour 3: Stream API — lazy eval, map/flatMap, groupingBy, partitioningBy.
4. Hour 4: Optional — orElse vs orElseGet, map/flatMap, best practices.
5. Hour 5: CompletableFuture — vs Future, thenApply/Compose/Combine, exceptionally.
6. Hour 6: Comparator chaining + Predicate/Consumer/Supplier/Function quick fire.
7. Buffer: Review interview traps + frequently confused concepts table.

Common Interview Mistakes to Avoid

1. Saying "Lambda creates anonymous class" — Wrong! It uses invokedynamic.


2. Saying "Stream stores data" — Wrong! Stream processes. Collection stores.
3. Forgetting to override hashCode when overriding equals — broken HashMap.
4. Using orElse() for expensive defaults instead of orElseGet().
5. Saying "parallel streams are always faster" — Wrong for small/IO-bound data.
6. Confusing thenCompose (sequential dependent) with thenCombine (parallel merge).
7. Saying setAccessible(true) makes the field public — it only disables the check.
8. Using generic array new T[] — type erasure makes this impossible.
9. Not mentioning treeification when explaining HashMap Java 8 changes.
10. Forgetting that reversed() in Comparator affects ALL prior chained comparisons.
11. Saying [Link]() is non-blocking — it always blocks.
12. Not mentioning the hashCode/equals contract when discussing HashMap keys.

Java Interview Notes • Page 60 • Mihir – Placement Prep 2026


Questions Asked by Top Companies — Java Backend
■ Product Companies (Flipkart, Swiggy, Zomato, CRED)
• Design an LRU Cache using LinkedHashMap.
• Implement a custom groupingBy using Stream reduce.
• Why does String have a stable hashCode() in Java?
• Write a stream pipeline that finds top-3 highest-paid employees per department.
• Explain ConcurrentHashMap vs [Link]() vs HashMap.
• How would you handle 1 million records without loading all into memory? (Streams / pagination)

■ Service Companies (TCS, Infosys, Wipro, Cognizant)


• What is Reflection? How is it used in Spring?
• Difference between Comparable and Comparator with code example.
• What is a lambda expression? How is it different from anonymous class?
• Explain Stream API with an example. What is lazy evaluation?
• What are functional interfaces? Name 4 built-in ones.
• What is Optional? How does it help avoid NullPointerException?

■ Startups / Mid-tier Tech


• Explain HashMap internal working from scratch.
• What happens during rehashing? When does it trigger?
• How do you chain CompletableFutures for parallel API calls?
• What is type erasure? Why can't you do new T[] in Java?
• Write a method that takes a list of orders and groups them by status using streams.
• Explain PECS with a real collection example.

FINAL TIP FOR INTERVIEWS


Do not just memorize answers — understand the WHY. Interviewers at product companies follow up
aggressively. If you explain HashMap as "it uses hashCode and equals", expect: "What if hashCode
always returns 0?", "What changes in Java 8?", "Why is capacity always a power of 2?". Know your
topics 2 levels deep.

Best of luck for your placements, Mihir! — Java Developer & Backend Developer roles await.

Java Interview Notes • Page 61 • Mihir – Placement Prep 2026

You might also like