🌊
Java 8

Java 8: Lambda और Streams

Writing Less, Doing More
💡 Lambda expression एक छोटी सी "instant note" जैसी है — पूरी formal चिट्ठी (class बनाके method लिखना) लिखने के बजाय, तुरंत एक line में काम बता देना। Stream एक factory conveyor belt है जिसमें data एक-एक stage से गुज़र कर (filter -> map -> collect) final result बनता है।

Lambda expression (param) -> { code } से एक functional interface (जिसमें सिर्फ़ एक abstract method हो) को छोटे तरीक़े से implement करते हैं।

Stream API collections पर filter, map, sort जैसे operations chain करके लिखने देता है — बिना manual loops लिखे, code छोटा और readable बन जाता है।

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 एक छोटी सी "instant note" जैसी है — पूरी formal चिट्ठी (class बनाके method लिखना) लिखने के बजाय, तुरंत एक line में काम बता देना। Stream एक factory conveyor belt है जिसमें data एक-एक stage से गुज़र कर (filter -> map -> collect) final result बनता है।
1 / 6
⚡ झट से Recap
  • Lambda = छोटी inline function
  • Functional Interface = एक ही abstract method
  • Stream = filter->map->collect chain
इस page में (14 subtopics)

Lambda expression एक नाम-लेस function है जो एक functional interface को तुरंत implement करता है। Syntax कई रूप में हो सकता है: (a, b) -> a + b (multiple params), x -> x * x (single param, बिना parentheses), () -> 42 (no params), या { } के अंदर multiple statements के साथ (a, b) -> { int sum = a+b; return sum; }।

Lambda अपने आस-पास के local variables को "capture" कर सकता है, लेकिन सिर्फ अगर वो "effectively final" हों — मतलब capture होने के बाद उनकी value कभी बदले नहीं। यह rule इसलिए है क्योंकि lambda किसी दूसरे thread में बाद में भी चल सकता है, और अगर original variable change हो जाए तो confusion हो जाएगा।

Interview में अक्सर पूछा जाता है कि lambda एक anonymous class से कैसे अलग है — anonymous class अपना खुद का "this" रखती है, जबकि lambda का "this" enclosing class को refer करता है। Real-world में lambdas सबसे ज़्यादा event listeners, Comparator, और Runnable जैसी जगह use होते हैं जहाँ थोड़ा सा logic pass करना होता है।

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 ने java.util.function package में ready-made functional interfaces दिए ताकि हर छोटी चीज़ के लिए खुद interface न बनाना पड़े: Predicate<T> (test(T): boolean — filtering के लिए), Function<T,R> (apply(T): R — एक type से दूसरे में convert), Consumer<T> (accept(T): void — value लेके कुछ करो, return नहीं), Supplier<T> (get(): T — बिना input के value produce करो)।

Do-argument versions भी हैं: BiFunction<T,U,R> (दो inputs, एक output), BiConsumer<T,U>, BiPredicate<T,U>। UnaryOperator<T> (Function जहाँ input और output same type हो) और BinaryOperator<T> (BiFunction जहाँ सब types same हों) special cases हैं।

@FunctionalInterface annotation लगाना optional है लेकिन best practice है — compiler check कर लेता है कि interface में सच में सिर्फ एक ही abstract method है, वरना गलती से दूसरा method add करने पर तुरंत error मिल जाता है, runtime तक wait नहीं करना पड़ता।

InterfaceMethodकाम
Predicate<T>test(T): booleanCondition check (filtering)
Function<T,R>apply(T): RT को R में convert
Consumer<T>accept(T): voidT लेके action लो, return नहीं
Supplier<T>get(): Tबिना input, T produce करो
UnaryOperator<T>apply(T): TFunction जहाँ input=output type
BinaryOperator<T>apply(T,T): TBiFunction जहाँ सब 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;

जब lambda सिर्फ एक existing method call कर रहा हो, method reference (::) से और छोटा लिख सकते हो। Type 1 — Static method: ClassName::staticMethod (जैसे Integer::parseInt)। Type 2 — Particular object का instance method: object::instanceMethod (जैसे System.out::println)।

Type 3 — Arbitrary object का instance method (class के नाम से, first argument automatically "this" बन जाता है): ClassName::instanceMethod (जैसे String::toUpperCase, जिसमें string खुद पहला argument बन जाता है)। Type 4 — Constructor reference: ClassName::new, नया object बनाने के लिए।

Real projects में method reference तभी use करो जब वो genuinely readable हो — अगर lambda में already थोड़ा extra logic है (जैसे s -> s.trim().toUpperCase()), उसे method reference में तोड़ना ज़्यादा readable नहीं बनाता, सीधा lambda ही रखो।

  • Type 1 — Static: ClassName::staticMethod, जैसे Integer::parseInt
  • Type 2 — Particular object: object::instanceMethod, जैसे System.out::println
  • Type 3 — Arbitrary object: ClassName::instanceMethod, जैसे String::toUpperCase
  • Type 4 — Constructor: ClassName::new, जैसे 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)

सबसे common: collection.stream() (List/Set से)। Arrays.stream(array) array से। Stream.of(1, 2, 3) direct values से। IntStream.range(1, 10) numbers की sequence के लिए (range() end exclude करता है, rangeClosed() include करता है)।

Infinite streams भी बन सकते हैं: Stream.generate(supplier) (हर बार supplier call होता है) और Stream.iterate(seed, fn) (पहली value से शुरू करके fn बार-बार apply होता है) — इनके साथ limit() लगाना ज़रूरी है वरना infinite चलते रहेंगे।

Interview trap: एक stream सिर्फ एक बार consume हो सकता है — अगर दोबारा use करने की कोशिश करोगे (जैसे एक terminal operation के बाद फिर से कोई operation call करना), IllegalStateException मिलेगी। हर बार नया stream चाहिए तो source से दोबारा .stream() call करो।

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 नया stream return करते हैं और "lazy" होते हैं (जब तक terminal operation न आए, चलते नहीं)। filter(predicate) — condition match करने वाले elements रखो। map(function) — हर element को transform करो। flatMap(function) — nested streams (जैसे List<List<T>>) को flatten करके एक single stream बनाओ।

distinct() — duplicates हटाओ। sorted() या sorted(comparator) — order लगाओ। limit(n) — पहले n elements। skip(n) — पहले n elements छोड़ो। peek() — बिना change किए बीच में देखने/debug करने के लिए।

Performance tip: filter() को map() से पहले रखो जब possible हो — पहले elements कम करो, फिर transform करो, इससे unnecessary transformations बचते हैं। यह छोटी सी बात बड़े datasets पर noticeable speed difference दे सकती है।

  • filter(predicate) — condition match करने वाले रखो
  • map(function) — हर element transform करो
  • flatMap(function) — nested streams को flatten करो
  • distinct() / sorted() — duplicates हटाओ / order लगाओ
  • limit(n) / skip(n) — पहले n रखो / पहले n छोड़ो
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 को "consume" करके final result देते हैं — एक stream सिर्फ एक बार consume हो सकता है। forEach(consumer) — हर element पर action। collect(collector) — result को List/Set/Map में बदलो (अगला subtopic देखो)। reduce(identity, accumulator) — सब elements को combine करके एक single value बनाओ (जैसे sum, product)।

count() — कितने elements हैं। anyMatch/allMatch/noneMatch(predicate) — boolean checks। findFirst()/findAny() — Optional<T> return करता है। min()/max(comparator) — भी Optional<T> return करते हैं।

anyMatch()/allMatch()/noneMatch() "short-circuit" operations हैं — जैसे ही result पक्का हो जाए (जैसे anyMatch में एक match मिल गया), बाकी elements process ही नहीं होते। यह बड़े streams पर पूरी list scan करने से काफी fast हो सकता है।

IntermediateTerminal
Return करता हैनया StreamFinal result (List, int, boolean...)
Lazy?हाँ (terminal तक नहीं चलता)चलते ही execute होता है
कितनी बार chain कर सकते होMultiple timesसिर्फ एक 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 को useful shapes में collect करने के helper methods देता है। toList()/toSet() — simple collection। joining(", ") — Strings को एक साथ जोड़ता है (separator के साथ)। groupingBy(fn) — elements को key के हिसाब से Map<Key, List<T>> में बांटता है (जैसे students को grade के हिसाब से group करना)।

partitioningBy(predicate) — सिर्फ true/false दो groups में बांटता है (Map<Boolean, List<T>>)। counting(), summingInt(), averagingDouble() — aggregate calculations, अक्सर groupingBy के साथ combine होते हैं।

toMap(keyFn, valueFn) use करते वक्त अगर दो elements की key same निकल आए, IllegalStateException आती है — इससे बचने के लिए तीसरा argument (merge function) दे सकते हो, जैसे toMap(k, v, (a,b) -> a) जो duplicate key पर पुरानी value रख ले।

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() (या .stream().parallel()) automatically काम को multiple CPU cores में बांट देता है — बड़े datasets पर speed बढ़ सकती है। लेकिन: shared mutable state (जैसे एक normal ArrayList में बाहर से add करना) के साथ use करना dangerous है (race conditions), और छोटे datasets के लिए overhead की वजह से उल्टा slow हो सकता है।

Rule of thumb: सिर्फ तब use करो जब dataset बड़ा हो, operations CPU-intensive हों, और elements एक-दूसरे पर depend न करते हों (independent operations)।

Interview में common gotcha: parallelStream() by default JVM-wide ForkJoinPool.commonPool() use करता है — मतलब अगर application में कहीं और (जैसे CompletableFuture) यही pool busy है, parallel streams unexpectedly slow हो सकते हैं एक-दूसरे से resources compete करके।

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

Optional<T> null के बजाय "value हो सकती है या नहीं" को explicitly represent करता है। बनाने के तरीके: Optional.of(value) (value null नहीं होनी चाहिए), Optional.ofNullable(value) (null हो सकता है), Optional.empty() (खाली)।

Use करने के तरीके: isPresent()/isEmpty() check करो, get() (risky — अगर empty हो तो exception, avoid करो), orElse(default) fallback दो, orElseGet(supplier) (lazy fallback, तभी compute होता है जब ज़रूरत हो), orElseThrow() (custom exception throw करो)। map()/filter() से chain कर सकते हो बिना manually null-check किए।

Best practice: Optional को कभी भी class field या method parameter के रूप में use मत करो — यह सिर्फ return types के लिए design हुआ है। Optional को field बनाना extra wrapping overhead देता है बिना कोई real फ़ायदे के।

  • Optional.of(value) — value कभी null नहीं होनी चाहिए
  • Optional.ofNullable(value) — value null हो सकती है
  • Optional.empty() — खाली Optional
  • orElse() vs orElseGet() — orElseGet lazy है, तभी compute होता है जब ज़रूरत हो
Optional<String> name = Optional.ofNullable(getNameOrNull());
String result = name
    .map(String::toUpperCase)
    .filter(n -> n.length() > 2)
    .orElse("GUEST");

default method interface में body के साथ होता है — implementing classes को override करना optional है। static method interface के नाम से directly call होता है (Interface.method()), implementing class के through नहीं। यह Java 8 से पहले नहीं था — अगर एक interface में नया method चाहिए होता था, सारी implementing classes तोड़नी पड़ती थीं।

अगर एक class दो interfaces implement करती है और दोनों में *same* default method हो ("diamond problem"), Java compile error देता है — class को explicitly override करके बताना पड़ता है कौनसा version (या अपना खुद का) use करना है।

Real-world example: List.sort(Comparator) खुद एक default method है List interface में (Java 8 से पहले Collections.sort(list) अलग से call करना पड़ता था) — यह दिखाता है कैसे default methods से पुरानी interfaces बिना break किए नए features मिल गए।

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
}

पुराना java.util.Date और Calendar mutable थे (thread-unsafe) और confusing API (months 0-indexed!)। Java 8 लाया java.time package: LocalDate (सिर्फ date, जैसे 2026-07-28), LocalTime (सिर्फ time), LocalDateTime (दोनों), और ये सब *immutable* हैं — कोई भी "modify" method actually एक नया object return करता है।

Period date differences के लिए (years/months/days), Duration time differences के लिए (hours/minutes/seconds)। DateTimeFormatter custom formatting के लिए (जैसे "dd-MM-yyyy")।

Thread-safety interview point: पुराना java.util.Date mutable था, इसलिए multiple threads एक ही Date object share करें तो race condition हो सकती थी। java.time के सारे classes immutable हैं, इसलिए multiple threads के बीच safely share हो सकते हैं बिना किसी synchronization के।

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 लिखना आसान बनाता है बिना manually threads manage किए। supplyAsync(supplier) एक task background में शुरू करता है और तुरंत एक "future" (promise जैसा) return करता है। thenApply() result को transform करता है (जब ready हो), thenAccept() result consume करता है, thenCombine() दो async tasks के results को जोड़ता है।

Exception handling के लिए exceptionally() या handle() use करो — अगर supplyAsync() के अंदर exception आ जाए और उसे handle न करो, future.get() call करते वक्त ExecutionException मिलेगी जिसमें original exception "wrapped" होगा।

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

Java 8 ने Comparator interface में handy default/static methods add किए: Comparator.comparing(keyExtractor) से एक field के basis पर sort। thenComparing() से secondary sort criteria (जैसे पहले name से, फिर age से)। reversed() order उल्टा करता है। naturalOrder()/reverseOrder() built-in Comparable order के लिए।

Comparator.nullsFirst()/nullsLast() भी Java 8 में आया — जब list में null values हो सकती हैं और sort करते वक्त NullPointerException से बचना हो, ये wrap करके बता देते हैं null को कहाँ रखना है (शुरू में या end में)।

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

StringJoiner — Strings को delimiter, prefix, suffix के साथ जोड़ने का utility (Collectors.joining() internally इसी का use करता है)। Base64 class — java.util.Base64 से encoding/decoding built-in मिल गई, बिना external library के। Repeating Annotations — अब एक ही annotation को एक जगह multiple बार लगा सकते हो।

Interview trivia: Base64 encoding से पहले developers Apache Commons Codec जैसी external library use करते थे सिर्फ इस छोटी सी चीज़ के लिए — Java 8 में built-in मिल जाने से एक common dependency हट गई।

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());