🌊
Java 8

Java 8: Lambda & Streams

Writing Less, Doing More
💡 Lambda expression ek chhoti si "instant note" jaisi hai — poori formal chitthi (class banake method likhna) likhne ke bajaye, turant ek line mein kaam bata dena. Stream ek factory conveyor belt hai jisme data ek-ek stage se guzar kar (filter -> map -> collect) final result banta hai.

Lambda expression (param) -> { code } se ek functional interface (jisme sirf ek abstract method ho) ko chhote tareeke se implement karte hain.

Stream API collections par filter, map, sort jaise operations chain karke likhne deta hai — bina manual loops likhe, code chhota aur readable ban jaata hai.

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenSquares = nums.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * n)
    .toList();
// [4, 16, 36]
🌊
Lambda expression ek chhoti si "instant note" jaisi hai — poori formal chitthi (class banake method likhna) likhne ke bajaye, turant ek line mein kaam bata dena. Stream ek factory conveyor belt hai jisme data ek-ek stage se guzar kar (filter -> map -> collect) final result banta hai.
1 / 6
⚡ Quick Recap
  • Lambda = chhoti inline function
  • Functional Interface = ek hi abstract method
  • Stream = filter->map->collect chain
Is page mein (14 subtopics)

Lambda expression ek naam-less function hai jo ek functional interface ko turant implement karta hai. Syntax kai roop mein ho sakta hai: (a, b) -> a + b (multiple params), x -> x * x (single param, bina parentheses), () -> 42 (no params), ya { } ke andar multiple statements ke saath (a, b) -> { int sum = a+b; return sum; }.

Lambda apne aas-paas ke local variables ko "capture" kar sakta hai, lekin sirf agar wo "effectively final" hon — matlab capture hone ke baad unki value kabhi badle nahi. Ye rule isliye hai kyunki lambda kisi doosre thread mein baad mein bhi chal sakta hai, aur agar original variable change ho jaaye to confusion ho jaayega.

Interview mein aksar poocha jaata hai ki lambda ek anonymous class se kaise alag hai — anonymous class apna khud ka "this" rakhti hai, jabki lambda ka "this" enclosing class ko refer karta hai. Real-world mein lambdas sabse zyada event listeners, Comparator, aur Runnable jaisi jagah use hote hain jaha thoda sa logic pass karna hota hai.

int factor = 10;          // effectively final
Function<Integer,Integer> multiply = n -> n * factor;
System.out.println(multiply.apply(5)); // 50
// factor = 20;  <- ye line likhoge to lambda mein error aa jaayega!

Java 8 ne java.util.function package mein ready-made functional interfaces diye taaki har chhoti cheez ke liye khud interface na banana pade: Predicate<T> (test(T): boolean — filtering ke liye), Function<T,R> (apply(T): R — ek type se doosre mein convert), Consumer<T> (accept(T): void — value leke kuch karo, return nahi), Supplier<T> (get(): T — bina input ke value produce karo).

Do-argument versions bhi hain: BiFunction<T,U,R> (do inputs, ek output), BiConsumer<T,U>, BiPredicate<T,U>. UnaryOperator<T> (Function jaha input aur output same type ho) aur BinaryOperator<T> (BiFunction jaha sab types same ho) special cases hain.

@FunctionalInterface annotation lagana optional hai lekin best practice hai — compiler check kar leta hai ki interface mein sach mein sirf ek hi abstract method hai, warna galti se doosra method add karne par turant error mil jaata hai, runtime tak wait nahi karna padta.

InterfaceMethodKaam
Predicate<T>test(T): booleanCondition check (filtering)
Function<T,R>apply(T): RT ko R mein convert
Consumer<T>accept(T): voidT leke action lo, return nahi
Supplier<T>get(): TBina input, T produce karo
UnaryOperator<T>apply(T): TFunction jaha input=output type
BinaryOperator<T>apply(T,T): TBiFunction jaha sab same type
Predicate<String> isLong = s -> s.length() > 5;
Function<String, Integer> toLength = String::length;
Consumer<String> print = System.out::println;
Supplier<Double> randomVal = Math::random;
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

Jab lambda sirf ek existing method call kar raha ho, method reference (::) se aur chhota likh sakte ho. Type 1 — Static method: ClassName::staticMethod (jaise Integer::parseInt). Type 2 — Particular object ka instance method: object::instanceMethod (jaise System.out::println).

Type 3 — Arbitrary object ka instance method (class ke naam se, first argument automatically "this" ban jaata hai): ClassName::instanceMethod (jaise String::toUpperCase, jisme string khud pehla argument ban jaata hai). Type 4 — Constructor reference: ClassName::new, naya object banane ke liye.

Real projects mein method reference tabhi use karo jab wo genuinely readable ho — agar lambda mein already thoda extra logic hai (jaise s -> s.trim().toUpperCase()), usse method reference mein todna zyada readable nahi banata, seedha lambda hi rakho.

  • Type 1 — Static: ClassName::staticMethod, jaise Integer::parseInt
  • Type 2 — Particular object: object::instanceMethod, jaise System.out::println
  • Type 3 — Arbitrary object: ClassName::instanceMethod, jaise String::toUpperCase
  • Type 4 — Constructor: ClassName::new, jaise ArrayList::new
list.forEach(System.out::println);              // Type 2
List<String> upper = list.stream().map(String::toUpperCase).toList(); // Type 3
Supplier<ArrayList<String>> maker = ArrayList::new;                    // Type 4 (constructor)

Sabse common: collection.stream() (List/Set se). Arrays.stream(array) array se. Stream.of(1, 2, 3) direct values se. IntStream.range(1, 10) numbers ki sequence ke liye (range() end exclude karta hai, rangeClosed() include karta hai).

Infinite streams bhi ban sakte hain: Stream.generate(supplier) (har baar supplier call hota hai) aur Stream.iterate(seed, fn) (pehli value se shuru karke fn baar-baar apply hota hai) — inke saath limit() lagana zaroori hai warna infinite chalte rahenge.

Interview trap: ek stream sirf ek baar consume ho sakta hai — agar dobara use karne ki koshish karoge (jaise ek terminal operation ke baad phir se koi operation call karna), IllegalStateException milegi. Har baar naya stream chahiye to source se dobara .stream() call karo.

Stream<Integer> s1 = List.of(1,2,3).stream();
IntStream s2 = IntStream.rangeClosed(1, 5); // 1,2,3,4,5
Stream<Integer> s3 = Stream.iterate(1, n -> n * 2).limit(5); // 1,2,4,8,16

Intermediate operations naya stream return karte hain aur "lazy" hote hain (jab tak terminal operation na aaye, chalte nahi). filter(predicate) — condition match karne wale elements rakho. map(function) — har element ko transform karo. flatMap(function) — nested streams (jaise List<List<T>>) ko flatten karke ek single stream banao.

distinct() — duplicates hatao. sorted() ya sorted(comparator) — order lagao. limit(n) — pehle n elements. skip(n) — pehle n elements chhodo. peek() — bina change kiye beech mein dekhne/debug karne ke liye.

Performance tip: filter() ko map() se pehle rakho jab possible ho — pehle elements kam karo, phir transform karo, isse unnecessary transformations bachte hain. Ye chhoti si baat bade datasets par noticeable speed difference de sakti hai.

  • filter(predicate) — condition match karne wale rakho
  • map(function) — har element transform karo
  • flatMap(function) — nested streams ko flatten karo
  • distinct() / sorted() — duplicates hatao / order lagao
  • limit(n) / skip(n) — pehle n rakho / pehle n chhodo
List<String> names = List.of("Riya","Aman","Riya","Zoya");
names.stream()
  .distinct()
  .filter(n -> n.length() > 3)
  .sorted()
  .forEach(System.out::println); // Aman, Riya, Zoya

Terminal operations stream ko "consume" karke final result dete hain — ek stream sirf ek baar consume ho sakta hai. forEach(consumer) — har element par action. collect(collector) — result ko List/Set/Map mein badlo (agla subtopic dekho). reduce(identity, accumulator) — sab elements ko combine karke ek single value banao (jaise sum, product).

count() — kitne elements hain. anyMatch/allMatch/noneMatch(predicate) — boolean checks. findFirst()/findAny() — Optional<T> return karta hai. min()/max(comparator) — bhi Optional<T> return karte hain.

anyMatch()/allMatch()/noneMatch() "short-circuit" operations hain — jaise hi result pakka ho jaaye (jaise anyMatch mein ek match mil gaya), baaki elements process hi nahi hote. Ye bade streams par poori list scan karne se kaafi fast ho sakta hai.

IntermediateTerminal
Return karta haiNaya StreamFinal result (List, int, boolean...)
Lazy?Haan (terminal tak nahi chalta)Chalte hi execute hota hai
Kitni baar chain kar sakte hoMultiple timesSirf ek terminal per stream
Examplesfilter, map, sorted, distinctcollect, reduce, forEach, count
List<Integer> nums = List.of(1,2,3,4,5);
int sum = nums.stream().reduce(0, (a,b) -> a+b);       // 15
boolean hasEven = nums.stream().anyMatch(n -> n%2==0); // true
Optional<Integer> max = nums.stream().max(Integer::compareTo);

Collectors class stream ko useful shapes mein collect karne ke helper methods deta hai. toList()/toSet() — simple collection. joining(", ") — Strings ko ek saath jodta hai (separator ke saath). groupingBy(fn) — elements ko key ke hisaab se Map<Key, List<T>> mein baantta hai (jaise students ko grade ke hisaab se group karna).

partitioningBy(predicate) — sirf true/false do groups mein baantta hai (Map<Boolean, List<T>>). counting(), summingInt(), averagingDouble() — aggregate calculations, aksar groupingBy ke saath combine hote hain.

toMap(keyFn, valueFn) use karte waqt agar do elements ki key same nikal aaye, IllegalStateException aati hai — isse bachne ke liye teesra argument (merge function) de sakte ho, jaise toMap(k, v, (a,b) -> a) jo duplicate key par purani value rakh le.

Map<Boolean, List<Integer>> parts = nums.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1,3,5], true=[2,4]}

String joined = names.stream().collect(Collectors.joining(", "));
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

collection.parallelStream() (ya .stream().parallel()) automatically kaam ko multiple CPU cores mein baant deta hai — bade datasets par speed badh sakti hai. Lekin: shared mutable state (jaise ek normal ArrayList mein baahar se add karna) ke saath use karna dangerous hai (race conditions), aur chhote datasets ke liye overhead ki wajah se ulta slow ho sakta hai.

Rule of thumb: sirf tab use karo jab dataset bada ho, operations CPU-intensive hon, aur elements ek-doosre par depend na karte hon (independent operations).

Interview mein common gotcha: parallelStream() by default JVM-wide ForkJoinPool.commonPool() use karta hai — matlab agar application mein kahi aur (jaise CompletableFuture) ye hi pool busy hai, parallel streams unexpectedly slow ho sakte hain ek-doosre se resources compete karke.

long count = hugeList.parallelStream()
    .filter(x -> isPrime(x))
    .count();

Optional<T> null ke bajaye "value ho sakti hai ya nahi" ko explicitly represent karta hai. Banane ke tarike: Optional.of(value) (value null nahi honi chahiye), Optional.ofNullable(value) (null ho sakta hai), Optional.empty() (khaali).

Use karne ke tarike: isPresent()/isEmpty() check karo, get() (risky — agar empty ho to exception, avoid karo), orElse(default) fallback do, orElseGet(supplier) (lazy fallback, tabhi compute hota hai jab zaroorat ho), orElseThrow() (custom exception throw karo). map()/filter() se chain kar sakte ho bina manually null-check kiye.

Best practice: Optional ko kabhi bhi class field ya method parameter ke roop mein use mat karo — ye sirf return types ke liye design hua hai. Optional ko field banana extra wrapping overhead deta hai bina koi real fayda ke.

  • Optional.of(value) — value kabhi null nahi honi chahiye
  • Optional.ofNullable(value) — value null ho sakti hai
  • Optional.empty() — khaali Optional
  • orElse() vs orElseGet() — orElseGet lazy hai, tabhi compute hota hai jab zaroorat ho
Optional<String> name = Optional.ofNullable(getNameOrNull());
String result = name
    .map(String::toUpperCase)
    .filter(n -> n.length() > 2)
    .orElse("GUEST");

default method interface mein body ke saath hota hai — implementing classes ko override karna optional hai. static method interface ke naam se directly call hota hai (Interface.method()), implementing class ke through nahi. Ye Java 8 se pehle nahi tha — agar ek interface mein naya method chahiye hota tha, saari implementing classes todni padti thi.

Agar ek class do interfaces implement karti hai aur dono mein *same* default method ho ("diamond problem"), Java compile error deta hai — class ko explicitly override karke batana padta hai kaunsa version (ya apna khud ka) use karna hai.

Real-world example: List.sort(Comparator) khud ek default method hai List interface mein (Java 8 se pehle Collections.sort(list) alag se call karna padta tha) — ye dikhata hai kaise default methods se purani interfaces bina break kiye naye features mil gaye.

interface A { default void greet() { System.out.println("Hi from A"); } }
interface B { default void greet() { System.out.println("Hi from B"); } }
class C implements A, B {
  public void greet() { A.super.greet(); } // explicitly choose karna padta hai
}

Purana java.util.Date aur Calendar mutable the (thread-unsafe) aur confusing API (months 0-indexed!). Java 8 laaya java.time package: LocalDate (sirf date, jaise 2026-07-28), LocalTime (sirf time), LocalDateTime (dono), aur ye sab *immutable* hain — koi bhi "modify" method actually ek naya object return karta hai.

Period date differences ke liye (years/months/days), Duration time differences ke liye (hours/minutes/seconds). DateTimeFormatter custom formatting ke liye (jaise "dd-MM-yyyy").

Thread-safety interview point: purana java.util.Date mutable tha, isliye multiple threads ek hi Date object share karein to race condition ho sakti thi. java.time ke saare classes immutable hain, isliye multiple threads ke beech safely share ho sakte hain bina kisi synchronization ke.

LocalDate today = LocalDate.now();
LocalDate birthday = LocalDate.of(2010, 5, 15);
Period age = Period.between(birthday, today);
System.out.println(age.getYears() + " saal ka");

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd-MM-yyyy");
System.out.println(today.format(fmt));

CompletableFuture async (background) tasks likhna aasan banata hai bina manually threads manage kiye. supplyAsync(supplier) ek task background mein shuru karta hai aur turant ek "future" (promise jaisa) return karta hai. thenApply() result ko transform karta hai (jab ready ho), thenAccept() result consume karta hai, thenCombine() do async tasks ke results ko jodta hai.

Exception handling ke liye exceptionally() ya handle() use karo — agar supplyAsync() ke andar exception aa jaaye aur usse handle na karo, future.get() call karte waqt ExecutionException milegi jisme original exception "wrapped" hoga.

CompletableFuture<Integer> future = CompletableFuture
    .supplyAsync(() -> slowCalculation())
    .thenApply(result -> result * 2);
System.out.println(future.get()); // wait karke final result

Java 8 ne Comparator interface mein handy default/static methods add kiye: Comparator.comparing(keyExtractor) se ek field ke basis par sort. thenComparing() se secondary sort criteria (jaise pehle name se, phir age se). reversed() order ulta karta hai. naturalOrder()/reverseOrder() built-in Comparable order ke liye.

Comparator.nullsFirst()/nullsLast() bhi Java 8 mein aaya — jab list mein null values ho sakti hain aur sort karte waqt NullPointerException se bachna ho, ye wrap karke bata dete hain null ko kaha rakhna hai (shuru mein ya end mein).

students.sort(
  Comparator.comparing((Student s) -> s.grade)
    .thenComparing(s -> s.name)
    .reversed()
);

StringJoiner — Strings ko delimiter, prefix, suffix ke saath jodने ka utility (Collectors.joining() internally isi ka use karta hai). Base64 class — java.util.Base64 se encoding/decoding built-in mil gayi, bina external library ke. Repeating Annotations — ab ek hi annotation ko ek jagah multiple baar laga sakte ho.

Interview trivia: Base64 encoding se pehle developers Apache Commons Codec jaisi external library use karte the sirf is chhoti si cheez ke liye — Java 8 mein built-in mil jaane se ek common dependency hat gayi.

StringJoiner sj = new StringJoiner(", ", "[", "]");
sj.add("A").add("B").add("C");
System.out.println(sj); // [A, B, C]

String encoded = Base64.getEncoder().encodeToString("Hello".getBytes());