সরাসরি পর্বে যান
পর্ব ৩৫ · ৫৯-এর মধ্যে · মডিউল ৫

Stream API — filter, map আর collect দিয়ে ডেটার পাইপলাইন

The Stream API — Pipelines, Laziness, Collectors, Reduction and When a Loop Is Better

Taught in Bangla, this lesson teaches the Java Stream API: sources, intermediate operations like filter, map, flatMap and sorted, laziness and short-circuiting, terminal operations and reduce, Collectors for grouping, counting and building maps, IntStream to avoid boxing, side-effect and parallel stream pitfalls, and when a plain loop is the better choice.

এই পর্বে যা শিখবেন

  • Stream-এর ধারণা: pipeline, intermediate আর terminal operation, আর stream কেন ডেটা রাখে না
  • Collection, array, সংখ্যার পরিসর, অসীম ক্রম আর String থেকে stream তৈরি
  • filter, map, flatMap, distinct, sorted, limit, skip, peek — আর অলসতা (laziness) নিজের চোখে দেখা
  • toList, count, sum, average, anyMatch, reduce আর Collectors দিয়ে দলে ভাগ, গোনা, Map বানানো
  • Boxing, side effect আর parallel stream-এর ফাঁদ — আর কখন লুপই ভালো

১ · Stream কী — “কীভাবে” নয়, “কী” লিখুন

একটি অনলাইন দোকানের অর্ডারের তালিকা ভাবুন। মালিক জানতে চান: ঢাকার যেসব অর্ডারের দাম ১,০০০ টাকার বেশি, সেগুলোর পণ্যের নাম, বর্ণানুক্রমে। পর্ব ১১-এর লুপ দিয়ে লিখলে এমন দাঁড়ায়:

List<String> names = new ArrayList<>();
for (Order o : orders) {
    if (o.district().equals("Dhaka") && o.total() > 1000) {
        names.add(o.product());
    }
}
Collections.sort(names);

কোডটি ঠিক আছে, কিন্তু পড়তে হলে মাথায় চালাতে হয়: একটি খালি তালিকা, একটি লুপ, একটি শর্ত, একটি যোগ, শেষে সাজানো। আসল উদ্দেশ্য — “ঢাকার দামি অর্ডারের নাম, সাজানো” — কোডের ভেতরে লুকিয়ে আছে। একই কাজ Stream API দিয়ে:

List<String> names = orders.stream()
    .filter(o -> o.district().equals("Dhaka"))
    .filter(o -> o.total() > 1000)
    .map(Order::product)
    .sorted()
    .toList();

এবার কোডটি প্রায় বাক্যের মতো পড়া যায়: অর্ডারগুলো নাও, ঢাকারগুলো রাখো, দামিগুলো রাখো, পণ্যের নাম নাও, সাজাও, তালিকা বানাও। কোনো খালি তালিকা নেই, কোনো লুপ-ভ্যারিয়েবল নেই, কোনো add নেই। আপনি বলছেন কী চান; কীভাবে ঘুরে ঘুরে করতে হবে, সেটা library-র দায়িত্ব। এই ধাঁচের নাম declarative programming — SQL-এর SELECT … WHERE … ORDER BY ঠিক এমন। আর o -> … হলো পর্ব ৩৪-এর lambda, Order::product একটি method reference — stream-এর প্রতিটি ধাপ একটি functional interface নেয়, তাই এই দুটো ছাড়া stream লেখা যায় না।

একটি গার্মেন্টস কারখানার উৎপাদন লাইন ভাবুন। কাপড় একদিক থেকে ঢোকে; প্রথম টেবিলে কাটা, দ্বিতীয়তে সেলাই, তৃতীয়তে মান যাচাই (QC) — ত্রুটিপূর্ণটা সরিয়ে দেওয়া, চতুর্থতে প্যাকিং। প্রতিটি টেবিল শুধু নিজের কাজ জানে। সুপারভাইজার লাইনটা সাজিয়ে দেন, কিন্তু প্রতিটি শার্ট নিজে হাতে টেবিল থেকে টেবিলে বয়ে নেন না। আর অর্ডার না এলে লাইন চালু হয় না। Stream হলো সেই লাইনের নকশা: উৎস হলো কাপড়ের গাঁট, filter হলো QC টেবিল, map হলো সেলাই, আর শেষ ধাপ (toList) হলো ক্রেতার অর্ডার — সেটা এলেই লাইন চলতে শুরু করে।

একটি pipeline-এর তিন অংশ

উৎস (source)

orders.stream(), Stream.of(...), IntStream.range(...) — উপাদানগুলো কোথা থেকে আসবে।

ঠিক একটি

মাঝের ধাপ

Intermediate operation: filter, map, sorted, limit… প্রতিটি একটি নতুন Stream ফেরত দেয়, তাই একটার পর একটা জোড়া যায়।

শূন্য বা অনেক — কিছুই চালায় না

শেষ ধাপ

Terminal operation: toList, count, sum, collect, forEach… এটি ফল দেয়, Stream নয়।

ঠিক একটি — এটাই সব চালু করে

তিনটি মিলে একটি stream pipeline. এর তিনটি স্বভাব পুরো পর্ব জুড়ে বারবার আসবে, তাই শুরুতেই মনে গেঁথে নিন:

মূল ধারণা — stream-এর তিন স্বভাব

১. ডেটা রাখে না। Stream কোনো collection নয়; উপাদানগুলো উৎসেই থাকে, stream শুধু তাদের ওপর দিয়ে বয়ে যাওয়ার পথ। filter মূল তালিকা থেকে কিছু মোছে না। ২. অলস (lazy). মাঝের ধাপগুলো শুধু বর্ণনা জমা করে; শেষ ধাপ না এলে একটি উপাদানও ছোঁয়া হয় না। ৩. একবারের। শেষ ধাপ চলে গেলে stream খরচ হয়ে যায়, আবার চালানো যায় না — নতুন করে orders.stream() ডাকতে হয়।

নামের বিভ্রান্তি। পর্ব ০৯-এ System.in, System.out-কে বলা হয়েছিল I/O stream — byte বা অক্ষর বয়ে নেওয়ার নল (java.io.InputStream). এই পর্বের java.util.stream.Stream সম্পূর্ণ আলাদা জিনিস, Java 8 (২০১৪)-এ আসা। নাম এক হলেও দুটির কোনো উত্তরাধিকার সম্পর্ক নেই। ভাবনার প্রশ্ন ২-এ এর ইতিহাস।

২ · Stream কোথা থেকে আসে — উৎস

প্রায় যেকোনো ক্রমিক ডেটা থেকেই stream বানানো যায়। সবচেয়ে প্রচলিত উৎসগুলো:

উৎসকোডকী পাবেন
যেকোনো Collectionorders.stream()Stream<Order> — List, Set সবার আছে (Map-এর নেই; map.entrySet().stream())
হাতে লেখা কয়েকটি মানStream.of("Dhaka", "Sylhet")Stream<String>
ArrayArrays.stream(marks)int[] হলে IntStream, String[] হলে Stream<String>
সংখ্যার পরিসরIntStream.range(1, 5)
IntStream.rangeClosed(1, 5)
1, 2, 3, 4 (শেষটা বাদ)
1 থেকে 5 (শেষটাসহ) — যোগফল 15
নিয়মে তৈরি অসীম ক্রমStream.iterate(1, x -> x * 2).limit(8)[1, 2, 4, 8, 16, 32, 64, 128] — limit ছাড়া কখনো শেষ হতো না
শর্তসহ ক্রম (Java 9 থেকে)Stream.iterate(1, x -> x <= 100, x -> x * 3)[1, 3, 9, 27, 81] — for লুপের মতো তিন অংশ
String-এর অক্ষর"banana".chars()IntStream (প্রতিটি অক্ষরের সংখ্যা-কোড); filter(c -> c == 'a').count() দেয় 3
String-এর লাইনtext.lines()Stream<String>, প্রতি লাইনে একটি — CSV বা লগ পড়তে
ফাইলের লাইনFiles.lines(path)লাইনগুলো দরকার মতো পড়ে, পুরো ফাইল মেমরিতে না তুলে — পর্ব ৩৭-এ

শেষ সারিটি নিয়ে একটি সতর্কতা: Files.lines ফাইলটি খোলা রাখে, তাই একে পর্ব ২৯-এর try-with-resources-এর ভেতরে ব্যবহার করতে হয় — try (Stream<String> lines = Files.lines(path)) { … }. সাধারণ collection-এর stream বন্ধ করার দরকার নেই।

৩ · মাঝের ধাপ — filter, map, flatMap, sorted আর অন্যরা

প্রতিটি মাঝের ধাপ একটি Stream নেয় আর একটি নতুন Stream দেয়। সবচেয়ে বেশি ব্যবহৃত দুটি দিয়ে শুরু:

  • filter(শর্ত) — শর্ত সত্য হলে উপাদানটি সামনে যায়, মিথ্যা হলে বাদ। সংখ্যা কমতে পারে, টাইপ একই থাকে। শর্তটি একটি Predicate<T>.
  • map(রূপান্তর) — প্রতিটি উপাদানকে অন্য কিছুতে বদলায়: Order থেকে পণ্যের নাম, নাম থেকে বড় হাতের নাম। সংখ্যা একই থাকে, টাইপ বদলাতে পারে (Stream<Order> → Stream<String>). রূপান্তরটি একটি Function<T, R>.
  • mapToInt, mapToDouble — map-এর মতোই, কিন্তু ফল সরাসরি int বা double-এর stream; যোগফল আর গড়ের জন্য (অংশ ৭)।

flatMap — তালিকার তালিকা থেকে একটি সমতল তালিকা

ধরুন একটি কোচিং সেন্টারের তিনটি ব্যাচ, প্রতিটি ব্যাচে কয়েকজন শিক্ষার্থী — List<List<String>>. আপনি সব শিক্ষার্থীর একটি তালিকা চান। map(List::stream) দিলে পাবেন তিনটি stream-এর একটি stream — বাক্সের ভেতরে বাক্স। flatMap প্রতিটি ভেতরের বাক্স খুলে উপাদানগুলো একটি লাইনে ঢেলে দেয়:

  1. batchesList<List<String>>৩টি ব্যাচ
  2. mapStream<Stream<String>>বাক্সে বাক্স
  3. flatMapStream<String>সমতল
  4. toListList<String>৫ জন
ধাপ ১উৎস
batches = [ [Rahim, Karim],
            [Ayesha],
            [Sakib, Nusrat] ]
বাইরের তালিকায় ৩টি উপাদান — প্রতিটি নিজেই একটি তালিকা
ধাপ ২map(List::stream) দিলে
Stream[ Stream[Rahim, Karim],
        Stream[Ayesha],
        Stream[Sakib, Nusrat] ]
count() → 3   (৫ নয়!)
ধাপ ৩flatMap(List::stream)
[Rahim, Karim]   → Rahim, Karim
[Ayesha]         → Ayesha
[Sakib, Nusrat]  → Sakib, Nusrat
একটি লাইনে: Rahim, Karim, Ayesha, Sakib, Nusrat
ধাপ ৪ফল
batches.stream().flatMap(List::stream).toList()
→ [Rahim, Karim, Ayesha, Sakib, Nusrat]
একই কৌশলে লাইন থেকে শব্দ:
lines.stream().flatMap(l -> Arrays.stream(l.split(" ")))
  1. তালিকার তালিকা। বাইরের তালিকায় তিনটি উপাদান, কিন্তু প্রতিটি উপাদান নিজেই একটি ছোট তালিকা। আমরা চাই ভেতরের পাঁচটি নাম।
  2. map যথেষ্ট নয়। map প্রতিটি উপাদানকে ঠিক একটি জিনিসে বদলায় — এখানে একটি ছোট stream-এ। ফলে পাই stream-এর stream, আর count() বলে ৩।
  3. flatMap খুলে ঢালে। প্রতিটি উপাদান থেকে একটি stream বানায়, তারপর সেই stream-এর উপাদানগুলো একে একে মূল লাইনে পাঠায়। একটি উপাদান থেকে শূন্য, এক বা অনেক উপাদান বেরোতে পারে।
  4. সমতল ফল। পাঁচজনের একটি তালিকা। বাস্তবে সবচেয়ে বেশি ব্যবহার: লাইনগুলো থেকে সব শব্দ — ["আমি ভাত খাই", "সে চা খায়"] থেকে [আমি, ভাত, খাই, সে, চা, খায়].

বাকি মাঝের ধাপগুলো

ধাপকাজউদাহরণ ও ফল
distinct()পুনরাবৃত্তি বাদ (equals দিয়ে তুলনা)Stream.of(5, 3, 5, 1, 4, 3)
.distinct().sorted().skip(1)
→ [3, 4, 5]
sorted()স্বাভাবিক ক্রমে সাজায় (Comparable)
skip(n)প্রথম n-টি বাদ
sorted(comparator)নিজের নিয়মে সাজায় — পর্ব ৩৩-এর Comparatorsorted(Comparator.comparingInt(Student::marks).reversed()) — বেশি নম্বর আগে
limit(n)প্রথম n-টির পর থামে“সেরা ৩টি পণ্য”, অসীম stream থামানো
peek(action)উপাদান না বদলে উঁকি দেয়শুধু debug-এর জন্য — অংশ ৪ আর ৭ দেখুন

sorted আর distinct একটু আলাদা জাতের। filter বা map প্রতিটি উপাদান একা একা দেখেই সিদ্ধান্ত নেয়। কিন্তু sorted সব উপাদান হাতে না পাওয়া পর্যন্ত বলতে পারে না কোনটা প্রথম, আর distinct-কে আগে দেখা সব মান মনে রাখতে হয়। এদের বলে stateful operation — এরা মেমরি বেশি নেয়, আর অসীম stream-এ sorted কখনো শেষ হয় না।

৪ · অলসতা — উপাদান একে একে পুরো পাইপ পেরোয়

বেশিরভাগ নতুন শিক্ষার্থী stream-কে এভাবে কল্পনা করেন: আগে filter পুরো তালিকায় চলে একটি নতুন তালিকা বানায়, তারপর map সেই তালিকায় চলে আরেকটি তালিকা বানায়, তারপর limit. অর্থাৎ ধাপে ধাপে, “অনুভূমিক”ভাবে। আসলে তা হয় না। Java stream চলে “উল্লম্ব”ভাবে: একটি উপাদান পুরো পাইপ পেরিয়ে যায় (বা মাঝপথে বাদ পড়ে), তারপর পরের উপাদান। আর শেষ ধাপ না আসা পর্যন্ত একটিও না। এর নাম lazy evaluation. নিজে দেখুন — প্রতিটি ধাপে একটি ছাপার লাইন বসানো আছে:

import java.util.List;
class Main {
    public static void main(String[] args) {
        List<String> names = List.of("Rahim", "Ayesha", "Karim", "Nusrat", "Tamim", "Sadia");
        var pipeline = names.stream()
            .filter(n -> { System.out.println("filter: " + n); return n.length() > 5; })
            .map(n -> { System.out.println("  map: " + n); return n.toUpperCase(); })
            .limit(2);
        System.out.println("pipeline তৈরি — এখনো কিছু চলেনি");
        List<String> result = pipeline.toList();      // terminal operation
        System.out.println("ফল: " + result);
        try {
            pipeline.count();                         // দ্বিতীয়বার
        } catch (IllegalStateException e) {
            System.out.println("আবার চালাতে গেলে: " + e.getMessage());
        }
    }
}

আউটপুটের প্রথম লাইন pipeline তৈরি — এখনো কিছু চলেনি — অথচ filter আর map কোডে তার আগে লেখা। প্রমাণ হলো, মাঝের ধাপগুলো শুধু নকশা জমা রেখেছিল। তারপর আসে filter: Rahim, filter: Ayesha, map: Ayesha, filter: Karim, filter: Nusrat, map: Nusrat আর ফল: [AYESHA, NUSRAT]. খেয়াল করুন: filter আর map পালাক্রমে ছাপা হচ্ছে, আর Tamim, Sadia-র নাম একবারও নেই — দুটি ফল পেয়েই limit(2) পুরো কাজ থামিয়ে দিয়েছে। শেষ লাইন: আবার চালাতে গেলে: stream has already been operated upon or closed — stream একবারের। (অনলাইন runner ফাইলটিকে অন্য নামে compile করে, তাই Main এখানে public নয়; নিজের প্রজেক্টে public class Main থাকে Main.java-তে।)

এবার একই ঘটনা ধীরে, ধাপে ধাপে:

1names.stream()            // উৎস: ৬টি নাম2  .filter(n -> n.length() > 5)3  .map(String::toUpperCase)4  .limit(2)5  .toList();                // terminal
এই মুহূর্তে
পাইপে কে
কী হলো
ফলের তালিকা
  1. শেষ ধাপই টান দেয়। toList() ডাকার আগে কিছুই ঘটেনি। এখন সে পেছনের ধাপের কাছে একটি উপাদান চায়, সেই ধাপ তার পেছনের কাছে — এভাবে চাহিদা উৎস পর্যন্ত পৌঁছায়। এটা কারখানার “অর্ডার এলে লাইন চালু”।
  2. Rahim বাদ। উৎস প্রথম নামটি পাঠায়। filter-এ দৈর্ঘ্য ৫, শর্ত (> ৫) মিথ্যা — Rahim এখানেই শেষ, map পর্যন্ত যায়ই না।
  3. Ayesha পুরো পথ পেরোয়। filter পাস, map বানায় AYESHA, limit গুনে রাখে ১টি, ফলের তালিকায় যোগ। খেয়াল করুন, Karim এখনো উৎস থেকে বেরোয়নি — একটি উপাদানের যাত্রা শেষ হলে তবেই পরেরটি।
  4. Karim বাদ। আবার দৈর্ঘ্য ৫। ধাপে ধাপে চললে আগে ছয়টি “filter” লাইন একসাথে ছাপা হতো; বাস্তব আউটপুটে filter আর map মিশে আছে — উল্লম্ব চলার প্রমাণ।
  5. Nusrat — দ্বিতীয় ফল। filter পাস, map, আর limit এখন ২/২ — যতগুলো চাওয়া হয়েছিল, সব পাওয়া গেছে।
  6. Short-circuit. limit আর কিছু চায় না, তাই উৎস থেকে Tamim আর Sadia কখনো বেরোয় না। দশ লাখ নামের তালিকা হলেও কাজ হতো ঠিক এই চারটি নামের। এই কারণেই Stream.iterate(...)-এর মতো অসীম stream-ও limit দিয়ে শেষ হয়।
  7. খরচ হয়ে গেছে। ফল তৈরি, stream বন্ধ। একই pipeline-এ আবার count() ডাকলে IllegalStateException: stream has already been operated upon or closed. নতুন করে names.stream() ডাকুন — তালিকাটি তো অক্ষত আছে।

এই অলসতা থেকে তিনটি বাস্তব লাভ: (ক) একটিমাত্র পাস — মাঝখানে কোনো অস্থায়ী তালিকা তৈরি হয় না; (খ) দরকারের বেশি কাজ নয় — limit, findFirst আগেভাগে থামায়; (গ) অসীম উৎস সম্ভব। তবে ব্যতিক্রম মনে রাখুন: sorted একটি দেয়াল। Stream.of(3, 1, 2).peek(…"before")… .sorted().peek(…"after") চালালে আগে তিনটি before 3, before 1, before 2, তারপর after 1, after 2, after 3 — সাজানোর জন্য সবাইকে আগে জমা হতে হয়।

একবারের স্বভাবের একটি ব্যবহারিক রূপ: stream-কে ভ্যারিয়েবলে রেখে দুবার ব্যবহার করবেন না।

Stream<String> s = Stream.of("Dhaka", "Sylhet");
s.forEach(System.out::println);
s.forEach(System.out::println);       // লাইন ৭
Dhaka
Sylhet
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.base/java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
	at java.base/java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:762)
	at Main.main(Main.java:7)

দরকার হলে stream নয়, উৎসটি (তালিকা) রেখে দিন, অথবা stream দেওয়ার একটি method লিখুন (Supplier<Stream<String>>).

৫ · শেষ ধাপ — ফল বের করা, short-circuit আর reduce

শেষ ধাপ (terminal operation) একটি Stream নয়, আসল ফল দেয় — একটি তালিকা, একটি সংখ্যা, একটি সত্য-মিথ্যা, বা কিছুই না (শুধু কাজ)।

তালিকায় ফেরা — toList() বনাম Collectors.toList()

Java 16 থেকে stream.toList() সরাসরি একটি তালিকা দেয়। এটি একটি unmodifiable collection — তাতে কিছু যোগ করতে গেলে:

List<Integer> marks = Stream.of(72, 28, 91).toList();
marks.add(50);                        // লাইন ৬
Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
	at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
	at Main.main(Main.java:6)

UnsupportedOperationException-এর কোনো বার্তা নেই, শুধু নাম — তাই চিনে রাখুন। পুরোনো কোডে দেখবেন .collect(Collectors.toList()) (Java 8 থেকে)। বর্তমান JDK-তে এটি একটি ArrayList দেয়, যাতে add চলে — কিন্তু documentation কোনো নির্দিষ্ট টাইপ বা পরিবর্তনযোগ্যতার প্রতিশ্রুতি দেয় না। নিয়ম: শুধু পড়ার জন্য তালিকা চাইলে toList(); পরে বদলাতে চাইলে স্পষ্ট করে .collect(Collectors.toCollection(ArrayList::new)).

গোনা, যোগ, গড় আর খোঁজা

ধাপফেরত দেয়নম্বর [72, 28, 91, 33, 39] দিয়ে
count()long5
mapToInt(...).sum()int263
mapToInt(...).average()OptionalDoubleOptionalDouble[52.6]; খালি হলে OptionalDouble.empty
max(comparator), min(...)Optional<T>Optional[Student[name=Ayesha, marks=91]]
filter(> 95).findFirst()Optional<T>Optional.empty — কেউ নেই
anyMatch / allMatch / noneMatchbooleanকেউ ফেল (< 33)? true · সবাই > 20? true · কেউ > 100? না — true
forEach(action)কিছু নাপ্রতিটির ওপর একটি কাজ — সাধারণত ছাপা

average, max, findFirst কেন সরাসরি সংখ্যা বা object দেয় না? কারণ stream খালি হতে পারে — শূন্যটি নম্বরের গড় কত? কোনো উত্তর নেই। পুরোনো Java হলে null ফেরত দিত আর কোথাও গিয়ে NullPointerException হতো। তার বদলে আসে একটি Optional — “মান থাকতেও পারে, না-ও পারে” লেখা একটি বাক্স। সেটা খোলার নিরাপদ উপায় (orElse, ifPresent, map) পরের পর্ব ৩৬-এর বিষয়; এখানে শুধু মনে রাখুন — findFirst().get() সরাসরি লিখবেন না।

anyMatch, allMatch, noneMatch, findFirst আর limit হলো short-circuiting operation: উত্তর জানা হয়ে গেলেই থামে। anyMatch(s -> s.marks() < 33) প্রথম ফেল-করা শিক্ষার্থী পেলেই true দেয়, বাকিদের দেখে না — ঠিক পর্ব ০৭-এর &&, ||-এর মতো।

reduce — অনেক থেকে এক

sum, count, max — সবই আসলে একটি সাধারণ ধারণার বিশেষ রূপ: অনেকগুলো উপাদানকে একটি নিয়মে বারবার জুড়ে একটি ফলে নামিয়ে আনা। একে বলে reduction, আর সাধারণ রূপটি হলো reduce(শুরুর মান, জোড়ার নিয়ম). পর্ব ১১-এর accumulator লুপের কথা মনে করুন — int total = 0; for (...) total += p; — reduce ঠিক সেটাই, শুধু লুপটা library চালায়:

1List<Integer> prices = List.of(120, 80, 250, 50);2int total = prices.stream()3    .reduce(0, (acc, p) -> acc + p);4System.out.println(total);
accumulator
acc (আগের ফল)
p (নতুন উপাদান)
acc + p
  1. শুরুর মান — identity. প্রথম argument 0 হলো শুরুর জমা। একে বলে identity value: যোগের জন্য 0, কারণ যেকোনো সংখ্যার সাথে 0 যোগ করলে সংখ্যাটি বদলায় না। গুণফল চাইলে identity হতো 1.
  2. প্রথম উপাদান। নিয়ম (acc, p) -> acc + p পায় আগের জমা (0) আর নতুন দাম (120); ফল 120 হয় নতুন জমা।
  3. দ্বিতীয় উপাদান। 120 + 80 = 200. প্রতিটি ধাপে নিয়মটি শুধু দুটি মান দেখে — আগের ফল আর নতুন উপাদান।
  4. তৃতীয় উপাদান। 200 + 250 = 450. কোনো বাইরের ভ্যারিয়েবল বদলানো হচ্ছে না; প্রতিটি ধাপ একটি নতুন মান ফেরত দেয়।
  5. শেষ উপাদান। 450 + 50 = 500. উপাদান ফুরিয়ে গেলে শেষ জমাই ফল।
  6. ফল, আর খালি হলে? 500 ছাপা হয়। তালিকা খালি হলে reduce সরাসরি identity, অর্থাৎ 0 ফেরত দেয় — তাই এখানে Optional নেই। Identity ছাড়া রূপ reduce(Integer::sum) দেয় Optional[500], কারণ খালি হলে তার ফেরত দেওয়ার মতো কিছু নেই। বাস্তবে যোগফলের জন্য mapToInt(Integer::intValue).sum() আরও সরল।

একটি শর্ত: জোড়ার নিয়মটি associative হতে হবে — (a + b) + c আর a + (b + c) একই ফল দিতে হবে — আর identity সত্যিই নিরপেক্ষ হতে হবে। যোগ, গুণ, max ঠিক আছে; বিয়োগ নয়। Sequential stream-এ ভুলটা চোখে পড়ে না, কিন্তু parallel stream টুকরোগুলো যেকোনো ক্রমে জোড়ে, তখন ভুল ফল আসে।

৬ · Collectors — দলে ভাগ, গোনা আর Map বানানো

collect(...) একটি সাধারণ শেষ ধাপ, যাকে বলে দিতে হয় উপাদানগুলো কীভাবে জমাতে হবে। সেই রেসিপিকে বলে collector, আর java.util.stream.Collectors class-এ তৈরি রেসিপির একটি ভাণ্ডার আছে:

  • toSet() — পুনরাবৃত্তিহীন সেট (ক্রমের নিশ্চয়তা নেই)।
  • joining(", ", "[", "]") — String-গুলো জোড়া: [Rahim, Karim, Ayesha, Sakib, Nusrat]. বিভাজক, শুরু আর শেষের চিহ্ন ঐচ্ছিক।
  • toMap(keyফাংশন, valueফাংশন) — প্রতিটি উপাদান থেকে একটি key-value জোড়া।
  • groupingBy, partitioningBy, teeing — নিচে বিস্তারিত।

toMap-এর ফাঁদ — একই key দুবার

জেলা থেকে বিক্রির পরিমাণের Map বানাতে চাইলেন, কিন্তু ঢাকার দুটি সারি আছে:

List<Row> rows = List.of(new Row("a", "Dhaka", 10), new Row("b", "Sylhet", 5),
                         new Row("c", "Dhaka", 7));
Map<String, Integer> m = rows.stream()
    .collect(Collectors.toMap(Row::district, Row::amount));    // লাইন ৭
Exception in thread "main" java.lang.IllegalStateException: Duplicate key Dhaka (attempted merging values 10 and 7)
	at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:135)
	…
	at Main.main(Main.java:7)

বার্তাটি স্পষ্ট: key Dhaka দুবার এসেছে, মান 10 আর 7 — কোনটা রাখব? এটা ভালো আচরণ; নীরবে একটি মান হারালে বিক্রির হিসাব ভুল হতো। সমাধান তৃতীয় argument — merge function, যা বলে দুটি মান কীভাবে মেলাতে হবে; চতুর্থ argument দিয়ে সাজানো Map: toMap(Row::district, Row::amount, Integer::sum, TreeMap::new) → {Dhaka=17, Sylhet=5}.

groupingBy — দলে ভাগ করা

“জেলাভিত্তিক অর্ডারের সংখ্যা” — SQL-এর GROUP BY. groupingBy তিনটি জিনিস নিতে পারে: কোন key দিয়ে ভাগ করবে, কোন ধরনের Map বানাবে, আর প্রতিটি দলের উপাদান নিয়ে কী করবে। শেষেরটিকে বলে downstream collector. শুধু key দিলে দুটি জিনিস ডিফল্ট হয়: প্রতিটি দলে একটি List, আর Map হয় একটি HashMap — যার ছাপার ক্রম অনিশ্চিত। নির্দিষ্ট, বর্ণানুক্রমিক আউটপুট চাইলে TreeMap চেয়ে নিন:

1orders.stream()2  .collect(groupingBy(Order::district,   // key3                      TreeMap::new,      // সাজানো Map4                      counting()));      // প্রতি দলে কী
এখন যে অর্ডার: পণ্য — জেলা
অর্ডার
Chattogram (toList হলে)
Dhaka (toList হলে)
Sylhet (toList হলে)
counting() হলে
  1. Key বের করা। প্রথম অর্ডারের district() — Dhaka. Map-এ এই key নেই, তাই নতুন একটি দল খোলা হয়, আর অর্ডারটি সেখানে যায়।
  2. নতুন জেলা, সাজানো জায়গায়। Chattogram নতুন key. TreeMap key-গুলো সবসময় সাজিয়ে রাখে, তাই সে Dhaka-র আগে বসে। HashMap হলে ক্রম কী হতো, তার কোনো নিশ্চয়তা নেই।
  3. তৃতীয় জেলা। Sylhet — আরেকটি নতুন দল। এখন তিনটি key.
  4. পুরোনো দলে যোগ। Lungi-র অর্ডারও Dhaka-র। দল আগেই আছে, তাই নতুন দল নয় — সেই দলের পাত্রে যোগ।
  5. আবার Dhaka. Saree — ঢাকার দলে এখন তিনটি অর্ডার।
  6. শেষ অর্ডার। Tea, Sylhet-এ। প্রতিটি অর্ডার ঠিক একবার দেখা হলো — একটিমাত্র পাস।
  7. Downstream ঠিক করে পাত্রে কী থাকবে। শুধু groupingBy(Order::district) হলে প্রতিটি দলে পুরো অর্ডারের তালিকা থাকত (ওপরের তিনটি সারি)। counting() দিলে তালিকা আদৌ বানানো হয় না — প্রতিটি দলে শুরু থেকেই শুধু একটি গণনা বাড়ে (শেষ সারি)। ফল: {Chattogram=1, Dhaka=3, Sylhet=2}.

Downstream collector-এর আরও কয়েকটি, একই CSV-ধাঁচের ডেটায় (Rahim–Dhaka–1500, Karim–Sylhet–900, Ayesha–Dhaka–2100):

groupingBy(Row::district, TreeMap::new, …)ফল
counting(){Dhaka=2, Sylhet=1}
summingInt(Row::amount){Dhaka=3600, Sylhet=900}
averagingDouble(Row::amount){Dhaka=1800.0, Sylhet=900.0}
mapping(Row::name, toList()){Dhaka=[Rahim, Ayesha], Sylhet=[Karim]}

এবার সব মিলিয়ে একটি দোকানের আসল হিসাব — মোট বিক্রি, সবচেয়ে বেশি আয় এনেছে এমন ৩টি পণ্য, আর জেলাভিত্তিক অর্ডার। চালিয়ে দেখুন:

import java.util.*;
import java.util.stream.Collectors;
record Order(String product, String district, int qty, int price) { int total() { return qty * price; } }
class Main {
    public static void main(String[] args) {
        List<Order> orders = List.of(
            new Order("Panjabi", "Dhaka", 2, 1500), new Order("Saree", "Chattogram", 1, 3200),
            new Order("Panjabi", "Sylhet", 1, 1500), new Order("Lungi", "Dhaka", 3, 450),
            new Order("Saree", "Dhaka", 1, 3200), new Order("Tea", "Sylhet", 5, 300));
        int revenue = orders.stream().mapToInt(Order::total).sum();
        System.out.println("মোট বিক্রি: " + revenue + " টাকা");
        Map<String, Integer> byProduct = orders.stream()
            .collect(Collectors.groupingBy(Order::product, Collectors.summingInt(Order::total)));
        List<String> top3 = byProduct.entrySet().stream()
            .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
            .limit(3).map(Map.Entry::getKey).toList();
        System.out.println("সেরা ৩ পণ্য: " + top3);
        Map<String, Long> perDistrict = orders.stream()
            .collect(Collectors.groupingBy(Order::district, TreeMap::new, Collectors.counting()));
        System.out.println("জেলাভিত্তিক অর্ডার: " + perDistrict);
    }
}

আউটপুট তিন লাইন: মোট বিক্রি: 13750 টাকা, সেরা ৩ পণ্য: [Saree, Panjabi, Tea] আর জেলাভিত্তিক অর্ডার: {Chattogram=1, Dhaka=3, Sylhet=2}. প্রথম pipeline mapToInt দিয়ে প্রতিটি অর্ডারের মোট দাম int হিসেবে নেয় আর যোগ করে — কোনো boxing নেই। দ্বিতীয়টি দুই ধাপে: আগে পণ্য অনুযায়ী আয় জমা (Saree 6400, Panjabi 4500, Tea 1500, Lungi 1350), তারপর সেই Map-এর entry-গুলোর ওপর নতুন stream — আয় অনুযায়ী উল্টো ক্রমে সাজিয়ে প্রথম তিনটির নাম। এখানে byProduct একটি HashMap, কিন্তু আমরা সাজিয়ে নিয়েছি, তাই আউটপুট নির্দিষ্ট। Map.Entry.<String, Integer>comparingByValue()-এ টাইপ স্পষ্ট লেখা লেগেছে, কারণ .reversed() জোড়ার পর compiler নিজে টাইপ অনুমান করতে পারে না। তৃতীয়টি ওপরের stepper-এর হুবহু রূপ।

partitioningBy — দুই ভাগ: পাস আর ফেল

ভাগ যখন মাত্র দুটি — শর্ত সত্য আর মিথ্যা — তখন partitioningBy. ফল সবসময় Map<Boolean, …>, যার দুটি key-ই থাকে, এমনকি কোনো দল খালি হলেও। নম্বর Rahim 72, Karim 28, Ayesha 91, Sakib 33, Nusrat 39; পাস নম্বর 33:

Map<Boolean, List<String>> result = students.stream()
    .collect(Collectors.partitioningBy(s -> s.marks() >= 33,
             Collectors.mapping(Student::name, Collectors.toList())));
// {false=[Karim], true=[Rahim, Ayesha, Sakib, Nusrat]}
// counting() দিলে: {false=1, true=4}

বাংলা লেখায় শব্দের গণনা

flatMap আর groupingBy একসাথে — একটি লেখার কোন শব্দ কতবার এসেছে। Search engine, spell checker আর ভাষা-মডেলের প্রাথমিক ধাপ এমন গণনা দিয়েই শুরু হয়:

String text = "আমি ভাত খাই আমি চা খাই সে চা খায় আমি";
Map<String, Long> freq = Arrays.stream(text.split("\\s+"))
    .collect(Collectors.groupingBy(w -> w, TreeMap::new, Collectors.counting()));
System.out.println(freq);
// {আমি=3, খাই=2, খায়=1, চা=2, ভাত=1, সে=1}

split("\\s+") এক বা একাধিক ফাঁকা জায়গায় ভাঙে। TreeMap বাংলা শব্দগুলো Unicode-এর অক্ষর-ক্রমে সাজায় — অভিধানের ক্রমের কাছাকাছি, তবে হুবহু নয়। অনেক লাইনের লেখা হলে আগে lines().flatMap(l -> Arrays.stream(l.split("\\s+"))).

CSV লাইন থেকে record

পর্ব ২৫-এর record আর stream মিলে ছোট ডেটা-পার্সার:

String csv = """
    Rahim,Dhaka,1500
    Karim,Sylhet,900
    Ayesha,Dhaka,2100
    """;
List<Row> rows = csv.lines()
    .map(line -> line.split(","))
    .map(p -> new Row(p[0], p[1], Integer.parseInt(p[2])))
    .toList();
// [Row[name=Rahim, district=Dhaka, amount=1500], Row[name=Karim, ...], ...]

বাস্তব CSV-তে কমার ভেতরে কমা, উদ্ধৃতিচিহ্ন বা ফাঁকা লাইন থাকে; তখন একটি CSV library ব্যবহার করুন। ধারণাটি একই থাকে: লাইন → টুকরো → object.

teeing — এক পাসে দুটি হিসাব (Java 12 থেকে)

Collectors.teeing(c1, c2, মেলানো) একই উপাদানগুলো দুটি collector-কে একসাথে পাঠায়, তারপর দুটি ফল মেলায় — যেমন সর্বনিম্ন আর সর্বোচ্চ একসাথে: teeing(minBy(cmp), maxBy(cmp), (mn, mx) -> mn.get() + ".." + mx.get()) নম্বর 72, 28, 91, 33-এর জন্য দেয় 28..91. নামটি সম্ভবত Unix-এর tee কমান্ডের (বা ইংরেজি T আকৃতির পাইপ-জোড়ার) কথা মনে করিয়ে দিতে রাখা — tee একটি প্রবাহকে দুই দিকে পাঠায়। কম ব্যবহৃত, কিন্তু চিনে রাখা ভালো।

৭ · ফাঁদ — boxing, side effect আর parallel

Primitive stream — boxing এড়ান

Stream<Integer>-এর প্রতিটি উপাদান একটি আলাদা Integer object (পর্ব ০৬-এর autoboxing). কয়েক লাখ সংখ্যার যোগে প্রতিবার boxing-unboxing আর heap-এ নতুন object — অকারণ খরচ। তাই Java-তে তিনটি primitive stream আছে: IntStream, LongStream, DoubleStream. এগুলো সরাসরি int, long, double বয়ে নেয়, আর এদের আছে সংখ্যার বিশেষ method: sum(), average(), max(), আর summaryStatistics() — এক পাসে সব: IntSummaryStatistics{count=5, sum=263, min=28, average=52.600000, max=91}.

  • object → সংখ্যা: mapToInt(Student::marks), mapToDouble(...)
  • সংখ্যা → object: boxed() — যেমন IntStream.range(1, 5).boxed().toList() দেয় [1, 2, 3, 4]
  • Stream<Integer>-এ সরাসরি sum() নেই — সাধারণ object-এর stream-এ “যোগ” অর্থহীন, তাই আগে mapToInt.

Side effect — stream-এর ভেতর থেকে বাইরের জিনিস বদলাবেন না

লুপের অভ্যাসে অনেকে এভাবে লেখেন:

List<Integer> evens = new ArrayList<>();
IntStream.rangeClosed(1, 10)
    .filter(x -> x % 2 == 0)
    .forEach(evens::add);          // বাইরের তালিকা বদলানো — এড়ান

এটি চলে ([2, 4, 6, 8, 10]), কিন্তু এটা stream-এর ছদ্মবেশে একটি লুপ। বাইরের ভ্যারিয়েবল বদলানোকে বলে side effect. Stream-এর নকশা ধরে নেয় যে প্রতিটি ধাপের lambda একটি pure method-এর মতো: শুধু argument দেখে ফল দেয়, বাইরের কিছু বদলায় না। কারণ তিনটি:

  1. Parallel করলে ভাঙে। কেউ পরে .parallel() জুড়ে দিলে একাধিক thread একসাথে একটি সাধারণ ArrayList-এ add করবে। ArrayList thread-safe নয় — উপাদান হারাতে পারে, ক্রম এলোমেলো হয়, এমনকি ArrayIndexOutOfBoundsException. প্রতিবার আলাদা ফল, তাই bug ধরাও কঠিন।
  2. চলবে কি না, তারও নিশ্চয়তা নেই। JDK-র stream অপ্রয়োজনীয় কাজ বাদ দিতে পারে। যেমন Java 9 থেকে উৎসের আকার জানা থাকলে count() পুরো pipeline চালায়ই না: Stream.of("a", "b", "c").map(s -> { System.out.println("map " + s); return s; }).count() ছাপে শুধু 3 — একটিও “map” লাইন নয়। map বা peek-এর ভেতরের side effect-এর ওপর ভরসা করা কোড এভাবে নীরবে ভেঙে যায়।
  3. পড়ার সুবিধা হারায়। Stream-এর মূল প্রতিশ্রুতি ছিল “কী চাই” লেখা। বাইরের তালিকা ভরানো আবার “কীভাবে”-তে ফিরে যাওয়া।

সঠিক রূপ: ফলটা stream নিজেই বানাক — List<Integer> evens = IntStream.rangeClosed(1, 10).filter(x -> x % 2 == 0).boxed().toList();. forEach রাখুন শেষ কাজের জন্য — ছাপা, লগ লেখা, পাঠানো। আর peek শুধু debug-এর সময় সাময়িকভাবে।

Parallel stream — এক লাইনে, কিন্তু ভেবেচিন্তে

orders.parallelStream() বা .parallel() লিখলেই কাজ ভাগ হয়ে একাধিক CPU core-এ চলে — এটাই parallel stream. শুনতে জাদুর মতো, কিন্তু বাস্তবে প্রায়ই ধীর বা ভুল: উৎসকে টুকরো করা, thread-এ পাঠানো আর ফল জোড়ার একটা খরচ আছে, যা ছোট ডেটায় লাভের চেয়ে বেশি। LinkedList বা Stream.iterate-এর মতো উৎস ভালোভাবে ভাগ হয় না; limit, findFirst, forEachOrdered-এর মতো ক্রম-নির্ভর ধাপ parallel-এ ব্যয়বহুল; আর ওপরের side effect সরাসরি ভুল ফল দেয়। Parallel stream ডিফল্টে সব প্রোগ্রামের ভাগাভাগি করা একটি thread pool ব্যবহার করে, তাই তার ভেতরে ধীর নেটওয়ার্ক বা ডেটাবেস কল দিলে অন্য কাজও আটকে যায়। নিয়ম: আগে sequential, তারপর মেপে দেখুন (benchmark); বড় ডেটা, প্রতি উপাদানে ভারী হিসাব, স্বাধীন কাজ আর ভালোভাবে ভাগ হয় এমন উৎস (ArrayList, array, IntStream.range) — চারটি একসাথে থাকলে তবেই ভাবুন। Thread, synchronization আর executor-এর ভিত্তি পর্ব ৪০–৪২-এ।

৮ · লুপ না stream? — পড়ার সুবিধা, পারফরম্যান্স আর বিচারবুদ্ধি

Stream লুপের বদলি নয়, একটি বিকল্প। লুপে লুপ চালানোর দায়িত্ব আপনার — একে বলে external iteration; stream-এ সেই দায়িত্ব library-র — internal iteration. কোনটা কখন ভালো, তার একটি ব্যবহারিক হিসাব:

পরিস্থিতিভালো পছন্দকেন
ছাঁকা → রূপান্তর → জমানো/গোনা/দলে ভাগstreamপ্রতিটি ধাপের নাম আছে; উদ্দেশ্য এক নজরে পড়া যায়
শুধু প্রতিটি উপাদান ছাপাfor-each (বা list.forEach)stream().forEach-এ কোনো বাড়তি লাভ নেই
জটিল অবস্থা রেখে মাঝপথে থামা — “তিনবার ভুল হলে বা মোট ১০,০০০ ছাড়ালে থামো”লুপbreak, continue, একাধিক ভ্যারিয়েবল লুপে স্বাভাবিক; stream-এ জোর করলে জট
ভেতরে checked exception ছোড়ে এমন কাজ (ফাইল পড়া)লুপ (প্রায়ই)lambda checked exception ছুড়তে পারে না — নিচে দেখুন
পাশের উপাদানের সাথে তুলনা (আগের দিনের চেয়ে দাম বেড়েছে কি না)লুপ বা index-ভিত্তিকstream প্রতিটি উপাদান একা দেখে
ডিবাগারে ধাপে ধাপে দেখালুপ সহজলম্বা শিকলে breakpoint আর stack trace কঠিন — শিকল ছোট রাখুন, ধাপগুলো নামযুক্ত method-এ দিন

Checked exception-এর সমস্যাটা দেখুন। প্রতিটি ফাইল পড়তে চাইলেন:

List<String> texts = files.stream()
    .map(f -> Files.readString(Path.of(f)))     // লাইন ৮
    .toList();
Main.java:8: error: unreported exception IOException; must be caught or declared to be thrown
            .map(f -> Files.readString(Path.of(f)))
                                      ^
1 error

map নেয় একটি Function, যার apply method কোনো checked exception ঘোষণা করে না — তাই পুরো method-এ throws IOException লিখলেও lambda-র ভেতরে চলে না। উপায় হলো lambda-র ভেতরে try-catch দিয়ে unchecked exception-এ মুড়ে দেওয়া, বা ছোট একটি সাহায্যকারী method লেখা; তিন-চার লাইনের কাজে একটি সাধারণ for লুপ প্রায়ই বেশি পরিষ্কার।

পারফরম্যান্স — সংক্ষেপে সত্যিটা

  • ছোট থেকে মাঝারি ডেটায় stream আর লুপের গতির পার্থক্য সাধারণত নগণ্য; JIT দুটোকেই ভালোভাবে অপ্টিমাইজ করে। খুব গরম, ছোট লুপে (কনটেস্ট, সংখ্যা-গণনা) হাতে লেখা লুপ বা int[] একটু দ্রুত হতে পারে — সন্দেহ হলে মাপুন।
  • বড় খরচ সাধারণত আসে boxing (Stream<Integer>), অপ্রয়োজনীয় sorted, আর একই তালিকায় বারবার নতুন stream চালানো থেকে — একটি লুপে যা এক পাসে হয়, তা পাঁচটি আলাদা stream-এ পাঁচ পাস।
  • অলসতা আর short-circuit-এর কারণে filter আগে, ভারী map পরে, আর limit যত আগে সম্ভব — এতে কাজ কমে।
  • সবচেয়ে বড় লাভ গতিতে নয়, পড়ায়: একজন সহকর্মী (বা ছয় মাস পরের আপনি) কত তাড়াতাড়ি বুঝবেন কোডটি কী করছে।
C/C++/Python জানলে

C-তে এমন কিছু নেই — সব লুপে। C++20-এর ranges আর views (v | views::filter(...) | views::transform(...)) Java stream-এর সবচেয়ে কাছের আত্মীয়: সেগুলোও অলস আর পাইপের মতো জোড়া যায়, তবে view সাধারণত বারবার ঘোরা যায়, Java stream একবারের। Python-এর list comprehension [o.product for o in orders if o.total > 1000] সাথে সাথে পুরো তালিকা বানায়; অলস রূপ হলো generator expression (… for … in …) — সেটাও Java stream-এর মতো একবার ঘোরার পর ফুরিয়ে যায়। তুলনার জন্য দেখুন C++-এর ranges ও views আর Python-এর comprehension পর্ব।

পরীক্ষা ও ইন্টারভিউতে
  • Intermediate আর terminal operation-এর পার্থক্য? — intermediate (filter, map) নতুন Stream দেয় আর অলস; terminal (collect, count) ফল দেয় আর pipeline চালু করে।
  • map আর flatMap-এর পার্থক্য? — map এক উপাদান থেকে এক উপাদান; flatMap এক উপাদান থেকে একটি stream বানিয়ে সব জোড়া দেয় — তালিকার তালিকা সমতল করে।
  • একটি stream দুবার ব্যবহার করলে? — IllegalStateException: stream has already been operated upon or closed.
  • Collection আর Stream-এর পার্থক্য? — collection ডেটা রাখে আর বারবার ঘোরা যায়; stream ডেটা রাখে না, অলস, একবারের, আর উৎসকে বদলায় না।
  • findFirst() কী ফেরত দেয়? — Optional<T>, কারণ stream খালি হতে পারে।

৯ · AI-এর যুগে — লম্বা শিকল আর অন্ধ parallel()

AI stream খুব পছন্দ করে — দেখতে আধুনিক লাগে, আর এক লাইনে অনেক কাজ। সমস্যা হলো, “এক লাইনে অনেক কাজ” আর “পরিষ্কার কোড” এক জিনিস নয়। AI-এর লেখা Java stream কোডে বারবার যে ভুলগুলো আসে:

AI যা করেকেন সমস্যাআপনি যা করবেন
১৫-২০ ধাপের একটি শিকল, ভেতরে বহু-লাইনের lambda, nested collectপড়া, ডিবাগ আর রিভিউ প্রায় অসম্ভবমাঝের ফল নামযুক্ত ভ্যারিয়েবলে রাখুন; বড় lambda-কে নামযুক্ত method বানিয়ে method reference দিন
forEach(list::add) বা forEach(x -> map.put(...)) দিয়ে বাইরের collection ভরানোside effect; parallel হলে ভুল ফলtoList(), toMap, groupingBy
“দ্রুত করতে” .parallel() বা parallelStream()ছোট ডেটায় ধীর; shared state থাকলে ভুল; common pool আটকায়sequential রাখুন; মেপে প্রমাণ পেলে তবেই
সাধারণ লুপের জায়গায় stream — যেমন IntStream.range(0, n).forEach(i -> …) দিয়ে array-তে লেখাকোনো লাভ নেই, শুধু জটিলতাসাধারণ for লুপ
findFirst().get(), max(...).get()খালি হলে NoSuchElementExceptionorElse, orElseThrow বা ifPresent — পর্ব ৩৬
ক্রম দরকার এমন আউটপুটে শুধু groupingBy(key)HashMap-এর ক্রম অনিশ্চিত — টেস্ট মাঝে মাঝে ফেলgroupingBy(key, TreeMap::new, …) বা ছাপার আগে সাজানো
duplicate key সম্ভব এমন ডেটায় merge function ছাড়া toMapআসল ডেটায় IllegalStateException: Duplicate keyতৃতীয় argument দিন, বা groupingBy

Stream কোড লেখাতে বা রিভিউ করাতে এই কাঠামো ব্যবহার করুন — শর্তগুলো AI-কে “চতুর এক লাইন”-এর বদলে পড়ার মতো কোড লিখতে বাধ্য করে:

আমি Java 21 শিখছি; lambda, method reference আর Stream API-র মূল ধাপ জানি। কাজ: [যেমন — অর্ডারের তালিকা থেকে প্রতিটি জেলার মোট বিক্রি, বেশি থেকে কম ক্রমে] শর্ত: ১. একটি stream শিকলে ৬টির বেশি ধাপ দেবে না; বেশি লাগলে মাঝের ফল নামযুক্ত ভ্যারিয়েবলে রাখবে। ২. কোনো lambda দুই লাইনের বেশি হলে সেটাকে নামযুক্ত private method বানিয়ে method reference দেবে। ৩. forEach বা peek-এর ভেতরে বাইরের কোনো collection বা ভ্যারিয়েবল বদলাবে না; ফল collect/toList/groupingBy দিয়ে বানাবে। ৪. parallel() ব্যবহার করবে না। ৫. Optional ফেরত এলে get() ডাকবে না; খালি হলে কী হবে স্পষ্ট করবে। ৬. আউটপুটের ক্রম নির্দিষ্ট হতে হবে — HashMap ছাপবে না; TreeMap বা সাজানো ব্যবহার করবে। ৭. শেষে বলো, এই কাজটি সাধারণ for লুপে লিখলে বেশি পরিষ্কার হতো কি না, আর কেন।

AI-এর stream কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১filter, map, reduce — এই ধারণাগুলো Java ২০১৪ সালে আবিষ্কার করেনি। এগুলো কোথা থেকে এল?

Stream-এর শিকড় দুটি — একটি গণিত ও functional ভাষার জগতে, আরেকটি অপারেটিং সিস্টেমের shell-এ।

Functional ভাষার শিকড়। ১৯৫০-এর দশকের শেষে John McCarthy-র LISP-এ একটি তালিকার প্রতিটি উপাদানে একটি function প্রয়োগ করার ধারণা ছিল — আজকের map-এর পূর্বপুরুষ। Kenneth Iverson-এর APL (১৯৬০-এর দশক) পুরো array-র ওপর এক চিহ্নে কাজ করত, আর তার “reduce” অপারেটর (+/ মানে সব যোগ) সরাসরি আজকের reduce-এর ধারণা। পরে ML আর Haskell-এ map, filter, fold হয়ে ওঠে প্রতিদিনের হাতিয়ার। “Stream” নামটিও এই জগতের: Abelson আর Sussman-এর বিখ্যাত বই Structure and Interpretation of Computer Programs (১৯৮৫)-এ stream মানে দেরিতে হিসাব হওয়া (delayed) তালিকা — অসীম ক্রমও যেখানে সম্ভব। Haskell-এর তালিকা ডিফল্টেই অলস। Java stream-এর অলসতা আর Stream.iterate-এর অসীম ক্রম এই ঐতিহ্যেরই উত্তরসূরি।

Unix pipe-এর শিকড়। Bell Labs-এ Doug McIlroy ১৯৬০-এর দশকেই প্রোগ্রামগুলো বাগানের পাইপের মতো জোড়ার কথা লিখেছিলেন; ১৯৭৩ সালে Ken Thompson Unix-এ pipe যোগ করেন। cat log.txt | grep ERROR | sort | uniq -c | head — প্রতিটি প্রোগ্রাম একটি ছোট কাজ করে, পরেরটির হাতে দেয়। এটাই Unix philosophy, আর Java stream-এর filter → sorted → limit তার হুবহু প্রতিধ্বনি। Unix pipe-ও এক অর্থে অলস: head দশ লাইন পেয়ে থেমে গেলে আগের প্রোগ্রামগুলোও থামে — ঠিক limit-এর short-circuit.

Java-তে আসা। ২০০০-এর দশকে C# LINQ (২০০৭) আর Scala-র collection library মূলধারার প্রোগ্রামারদের কাছে এই ধাঁচ জনপ্রিয় করে। Java-তে lambda না থাকায় এমন API লেখা ছিল যন্ত্রণাদায়ক (প্রতিটি ধাপে anonymous class). Brian Goetz-এর নেতৃত্বে JSR 335 (Project Lambda) lambda আর Stream API একসাথে নকশা করে, আর দুটিই আসে Java 8-এ, মার্চ ২০১৪-এ। অর্থাৎ lambda আনা হয়েছিল অনেকটা stream সম্ভব করার জন্যই।

মূল উপলব্ধি: Stream API একটি নতুন আবিষ্কার নয়, অর্ধশতাব্দীর পরীক্ষিত দুটি ধারণার — functional ভাষার map/filter/reduce আর Unix-এর pipe — Java-র টাইপ-ব্যবস্থার ভেতরে নিরাপদ মিলন।

প্র ০২Java-তে দুটি সম্পূর্ণ আলাদা জিনিসের নাম “stream” — java.io আর java.util.stream. এই বিভ্রান্তি কেন, আর দুটি কোথায় মেলে?

পুরোনো stream. Java 1.0 (১৯৯৬) থেকেই java.io.InputStream আর OutputStream আছে। এখানে stream মানে byte-এর একটানা প্রবাহ — ফাইল, নেটওয়ার্ক সংযোগ বা কীবোর্ড থেকে আসা ডেটার নল। এই অর্থটি C আর Unix-এর ঐতিহ্য থেকে: C-র standard library-তে stdin, stdout-কে stream বলা হয়, আর C++-এর iostream-ও একই ধারণা। System.out একটি PrintStream. এই stream-এর কাজ হলো ডেটা সরানো — পড়া, লেখা, buffer করা।

নতুন stream. ২০১৪ সালে Java 8-এর java.util.stream.Stream একই শব্দ নিল একটি ভিন্ন অর্থে — উপাদানের ওপর হিসাবের একটি অলস pipeline. এটি byte নয়, যেকোনো object বয়ে নেয়; এর কাজ ডেটা সরানো নয়, রূপান্তর আর জমানো। দুটির মধ্যে কোনো উত্তরাধিকার সম্পর্ক নেই — InputStream কোনো Stream নয়।

নাম কেন রাখা হলো? “stream” শব্দটি functional ভাষার জগতে (প্র ০১) ঠিক এই অর্থেই প্রচলিত ছিল — একের পর এক আসা, দরকার মতো হিসাব হওয়া উপাদানের ক্রম। দুই ঐতিহ্যের মিলনবিন্দুতে বসে Java দল সেই পরিচিত শব্দটিই বেছে নেয়, আর ভিন্ন package (java.util.stream) দিয়ে আলাদা রাখে। ফল: আজও শিক্ষার্থী আর ইন্টারভিউ প্রার্থীরা দুটি গুলিয়ে ফেলেন।

যেখানে দুটি মেলে। মিলনটা সবচেয়ে স্পষ্ট Files.lines(path) আর BufferedReader.lines()-এ: নিচে একটি I/O stream ফাইল থেকে byte পড়ে অক্ষরে বদলায়, আর ওপরে একটি Stream<String> লাইনগুলো দরকার মতো একে একে দেয়। অলসতার কারণে কয়েক GB-র লগ ফাইলও পুরোটা মেমরিতে না তুলে filter করা যায়। কিন্তু এখানে I/O-র নিয়মও চলে আসে: ফাইল খোলা থাকে, তাই এই stream বন্ধ করতে হয়। সেজন্য Stream interface AutoCloseable-কে extend করে (BaseStream-এর মাধ্যমে), আর try-with-resources-এ বসানো যায়। সাধারণ collection-এর stream-এ close()-এর দরকার পড়ে না।

মূল উপলব্ধি: I/O stream ডেটা সরায়, Stream API ডেটা নিয়ে হিসাব করে; নাম এক কারণ দুটিই “একের পর এক আসা প্রবাহ” — আর Files.lines-এ দুটি এক হয়, তাই সেখানে দুই জগতের নিয়মই মানতে হয়।

প্র ০৩Brian Goetz stream-কে “internal iteration” বলে ব্যাখ্যা করেছেন। লুপ চালানোর দায়িত্ব কার হাতে — এই প্রশ্নটা এত গুরুত্বপূর্ণ কেন?

পুরোনো একটি পার্থক্য। ১৯৯৪ সালের বিখ্যাত Design Patterns বই (Gamma, Helm, Johnson, Vlissides — “Gang of Four”)-এর Iterator pattern অধ্যায়েই প্রশ্নটা তোলা হয়েছিল: iteration কে নিয়ন্ত্রণ করে? যদি client নিজে পরের উপাদান চায় (hasNext(), next()), সেটা external iterator; যদি client শুধু একটি কাজ দেয় আর collection নিজে প্রতিটি উপাদানে সেটা চালায়, সেটা internal iterator. Smalltalk-এ collection do: [:x | …] ছিল internal iteration-এর স্বাভাবিক রূপ। Java শুরু থেকে external-এর পথে গেছে — Iterator, তারপর Java 5-এর for-each.

Goetz-এর যুক্তি। Project Lambda-র নকশা-নথি “State of the Lambda”-তে Brian Goetz যুক্তি দেন যে external iteration স্বভাবতই ধারাবাহিক: আপনার লুপ একটার পর একটা উপাদান চায়, ক্রম আপনি ঠিক করেছেন, তাই library কিছু বদলাতে পারে না। দায়িত্ব library-র হাতে দিলে library স্বাধীন হয় — উপাদানগুলো কোন ক্রমে, কতগুলো একসাথে, কোন thread-এ, আদৌ সবগুলো কি না, তা নিজে ঠিক করতে পারে। এই স্বাধীনতা থেকেই আসে তিনটি সুবিধা: অলসতা (দরকারের বেশি নয়), ধাপগুলো এক পাসে জোড়া (fusion), আর parallel. লুপে এগুলো পেতে হলে প্রতিবার হাতে লিখতে হতো।

“কী” বনাম “কীভাবে”. একটি ভালো তুলনা হলো রেস্তোরাঁ। External iteration মানে রান্নাঘরে ঢুকে নিজে রাঁধা — প্রতিটি ধাপ আপনার হাতে, সব নিয়ন্ত্রণ আপনার, কিন্তু রাঁধুনির দক্ষতার সুবিধা নেই। Internal iteration মানে মেনু থেকে অর্ডার — আপনি বলেন কী চান, রান্নাঘর নিজের সেরা উপায়ে বানায়। SQL ডেটাবেস ঠিক এই কারণেই শক্তিশালী: আপনি WHERE লেখেন, query optimizer ঠিক করে index ব্যবহার করবে কি না।

দাম। নিয়ন্ত্রণ ছেড়ে দিলে কিছু জিনিস কঠিন হয়: মাঝপথে জটিল শর্তে থামা, checked exception, ডিবাগারে ধাপে ধাপে দেখা — অংশ ৮-এর টেবিল। এজন্য Java দুটোই রেখেছে; for লুপ বাদ দেওয়া হয়নি।

মূল উপলব্ধি: Stream-এর আসল উদ্ভাবন সিনট্যাক্স নয়, দায়িত্বের হস্তান্তর — লুপ library-র হাতে দিলে library অলসতা, fusion আর parallel-এর মতো অপ্টিমাইজেশন করতে পারে, বিনিময়ে আপনি কিছু নিয়ন্ত্রণ হারান।

প্র ০৪Stream কেন অলস আর কেন মাত্র একবার ব্যবহার করা যায়? Spliterator-এর নকশা এর পেছনে কী বলে?

অলসতা কেন। ধরুন প্রতিটি মাঝের ধাপ সাথে সাথে চলত আর একটি নতুন তালিকা বানাত। তাহলে filter → map → limit(2)-এ দশ লাখ উপাদানের দুটি অস্থায়ী তালিকা তৈরি হতো, শুধু দুটি ফলের জন্য; অসীম stream অসম্ভব হতো; আর findFirst সব উপাদান দেখে তবে প্রথমটি দিত। অলস নকশায় মাঝের ধাপগুলো শুধু একটি শিকল (pipeline-এর stage-এর linked list) তৈরি করে। Terminal operation এলে Java পুরো শিকলটা দেখে, প্রতিটি stage-কে একটি “sink”-এ মুড়ে একটার ভেতরে আরেকটা বসায়, তারপর উৎস থেকে উপাদান একে একে সেই মোড়ানো sink-এ ঠেলে দেয়। ফলে এক পাস, কোনো অস্থায়ী তালিকা নেই, আর short-circuit ধাপ চাইলে বাকি কাজ বাতিল করতে পারে। পুরো শিকল আগে দেখার আরেকটি সুবিধা: অপ্টিমাইজেশন — যেমন অংশ ৭-এ দেখেছেন, Java 9 থেকে আকার জানা থাকলে count() উপাদানই ঘোরে না।

Spliterator. প্রতিটি stream-এর নিচে থাকে একটি Spliterator — “splittable iterator”, Java 8-এ আসা। এর তিনটি মূল ক্ষমতা: tryAdvance (একটি উপাদান দাও), trySplit (নিজেকে দুই ভাগ করো — parallel-এর জন্য), আর characteristics (উৎস সম্পর্কে তথ্য: SIZED — আকার জানা, ORDERED — ক্রম আছে, DISTINCT, SORTED ইত্যাদি)। এই তথ্য দেখে stream সিদ্ধান্ত নেয়: SORTED উৎসে sorted() বাদ দেওয়া যায়, SIZED উৎসে count() সরাসরি আকার দেয়। ArrayList-এর spliterator মাঝখান থেকে সমান দুই ভাগ করতে পারে; LinkedList-এর পারে না — parallel-এ পার্থক্য এখান থেকেই।

একবারের কেন। Iterator-এর মতোই spliterator এক দিকে এগোয় আর ফেরত যায় না। কিছু উৎস প্রকৃতিগতভাবেই একবারের: নেটওয়ার্ক থেকে আসা ডেটা, ফাইলের লাইন, Stream.generate-এর এলোমেলো সংখ্যা — দ্বিতীয়বার পড়লে একই জিনিস আসবে না। Java দল চেয়েছিল Stream সব ধরনের উৎসের জন্য একই নিয়মে চলুক, তাই সবচেয়ে কঠোর নিয়মটি বেছে নেয়: প্রতিটি stream একবার। পুনরায় ব্যবহারের ক্ষমতা উৎসের দায়িত্ব — collection বারবার stream() দিতে পারে। আর একবার-ব্যবহার একটি ভুলও ধরিয়ে দেয়: একই stream দুই জায়গায় ভাগ করে ব্যবহার করলে কে কতটুকু পাবে, তা অনির্দিষ্ট হতো; তার বদলে আসে স্পষ্ট IllegalStateException.

মূল উপলব্ধি: অলসতা Java-কে পুরো pipeline আগে দেখে এক পাসে আর কম কাজে চালানোর সুযোগ দেয়; আর একবারের নিয়মটা সব ধরনের উৎসকে — ফাইল, নেটওয়ার্ক, অসীম ক্রম — একই, নিরাপদ চুক্তিতে বাঁধে।

প্র ০৫parallelStream()-এর নিচে Doug Lea-র Fork/Join framework. এটি কীভাবে কাজ করে, আর কখন parallel stream আসলে ধীর হয়?

Doug Lea আর java.util.concurrent. নিউ ইয়র্কের SUNY Oswego-র অধ্যাপক Doug Lea Java concurrency-র অন্যতম স্থপতি; তাঁর লেখা library থেকেই Java 5-এর java.util.concurrent (JSR 166) এসেছে। ২০০০ সালে তিনি “A Java Fork/Join Framework” নামে একটি গবেষণাপত্র প্রকাশ করেন। সেই নকশা পরে Java 7 (২০১১)-এ ForkJoinPool হিসেবে JDK-তে ঢোকে — এটাই Fork/Join framework. Java 8-এ যোগ হয় একটি ভাগাভাগি করা “common pool”, আর parallel stream ডিফল্টে সেটাই ব্যবহার করে।

কীভাবে কাজ করে। নীতিটি divide and conquer: কাজ বড় হলে দুই ভাগ করো (fork), প্রতিটি ভাগ আবার ভাঙো, যতক্ষণ না ভাগগুলো ছোট হয়; তারপর ছোট ভাগগুলো সরাসরি হিসাব করে ফলগুলো জোড়া দাও (join). Parallel stream-এ ভাগ করার কাজ করে spliterator-এর trySplit (প্র ০৪), আর জোড়ার কাজ করে reduce-এর combiner বা collector-এর merge. Thread-গুলো ব্যস্ত রাখার কৌশল হলো work stealing: প্রতিটি worker thread-এর নিজের কাজের সারি (deque) থাকে; নিজের সারি খালি হলে সে অন্য thread-এর সারির উল্টো প্রান্ত থেকে কাজ “চুরি” করে। এই ধারণার শিকড় MIT-র Cilk প্রকল্পে।

কখন ধীর। (১) ছোট ডেটা — ভাগ করা, thread-এ পাঠানো আর জোড়ার নির্দিষ্ট খরচ হিসাবের চেয়ে বেশি। (২) প্রতি উপাদানে হালকা কাজ — শুধু যোগ করা হলে লাভ কম; Goetz আর Lea-র আলোচনায় একটি মোটা দাগের নিয়ম আসে: উপাদানের সংখ্যা (N) আর প্রতি উপাদানের কাজ (Q)-এর গুণফল যথেষ্ট বড় না হলে parallel লাভজনক নয়। (৩) খারাপভাবে ভাগ হয় এমন উৎস — LinkedList, Stream.iterate, I/O-ভিত্তিক stream. (৪) ক্রম-নির্ভর ধাপ — ordered stream-এ limit, findFirst, forEachOrdered-এর জন্য thread-গুলোকে একে অপরের অপেক্ষা করতে হয়। (৫) boxing আর merge-এর খরচ — groupingBy-এ প্রতিটি thread আলাদা Map বানায়, পরে সেগুলো মেলাতে হয়। (৬) common pool-এ ব্লকিং কাজ — নেটওয়ার্ক কল দিলে pool-এর thread আটকে থাকে, আর একই pool ব্যবহারকারী অন্য সব parallel কাজ ধীর হয়। Common pool-এর thread সংখ্যা ডিফল্টে মোটামুটি CPU core-এর সংখ্যা বিয়োগ এক, কারণ ডাকা thread নিজেও কাজে যোগ দেয়।

ব্যবহারিক পরামর্শ। Web server-এ প্রতিটি request ইতিমধ্যে আলাদা thread-এ চলে, তাই সেখানে parallel stream প্রায়ই লাভের বদলে প্রতিযোগিতা বাড়ায়। Parallel-এর সঠিক জায়গা বড় ব্যাচ-হিসাব: লাখো ছবির প্রক্রিয়া, বড় সংখ্যা-গণনা — আর সেখানেও JMH-এর মতো টুল দিয়ে মেপে।

মূল উপলব্ধি: .parallel() একটি শব্দ, কিন্তু তার নিচে Fork/Join, work stealing আর spliterator-এর পুরো যন্ত্র চলে; সেই যন্ত্রের খরচ শুধু বড়, ভারী, স্বাধীন আর ভালোভাবে ভাগ হয় এমন কাজেই উসুল হয়।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অলসতা আর ধাপের ক্রম নিয়ে ভাবাই আসল কাজ।

  1. আউটপুট অনুমান করুন: Stream.of(3, 8, 5, 12, 7).filter(x -> { System.out.println("f " + x); return x % 2 == 0; }).map(x -> { System.out.println("m " + x); return x * 10; }).findFirst() — কোন লাইনগুলো ছাপা হবে, কোন ক্রমে, আর ফল কী?

    ছাপা হয় মাত্র তিনটি লাইন: f 3, f 8, m 8; ফল Optional[80]. 3 বিজোড়, filter-এ বাদ। 8 জোড় — filter পাস, map বানায় 80, আর findFirst প্রথম ফল পেয়েই থামে। 5, 12, 7 কখনো উৎস থেকে বেরোয় না, তাই 12 জোড় হলেও তার “f” বা “m” ছাপা হয় না। ধাপে ধাপে চললে আগে পাঁচটি “f” লাইন আসত — আসে না, কারণ stream উল্লম্বভাবে আর অলসভাবে চলে, আর findFirst short-circuiting.

  2. লুপ থেকে stream: নিচের লুপটি একটি stream দিয়ে লিখুন: int s = 0; for (int i = 1; i <= 10; i++) if (i % 2 == 0) s += i * i; — ১ থেকে ১০-এর জোড় সংখ্যাগুলোর বর্গের যোগফল।
    int s = IntStream.rangeClosed(1, 10)
        .filter(i -> i % 2 == 0)
        .map(i -> i * i)
        .sum();                 // 220

    ফল 220 (4 + 16 + 36 + 64 + 100). rangeClosed কারণ ১০-ও লাগবে। IntStream হওয়ায় কোনো boxing নেই, আর sum() সরাসরি আছে। এখানে IntStream-এর map int থেকে int দেয়। Stream.of(1, 2, …) দিয়ে শুরু করলে আগে mapToInt লাগত।

  3. Bug খুঁজুন: একজন সহকর্মী লিখেছেন Map<String, Integer> stock = new HashMap<>(); items.parallelStream().forEach(i -> stock.merge(i.category(), i.qty(), Integer::sum)); System.out.println(stock); — ছোট টেস্টে ঠিক চলে, production-এ মাঝে মাঝে মোট পরিমাণ কম আসে আর ছাপার ক্রম প্রতিবার আলাদা। কেন, আর ঠিক রূপ কী?

    দুটি সমস্যা। (ক) Side effect + parallel: একাধিক thread একসাথে একটি সাধারণ HashMap-এ merge করছে; HashMap thread-safe নয়, তাই একটি thread-এর আপডেট আরেকটি মুছে দিতে পারে — মোট কম আসে, আর ভুলটা প্রতিবার আসে না বলে ধরা কঠিন। (খ) HashMap-এর ছাপার ক্রম অনিশ্চিত। ঠিক রূপ: ফলটা collector-কে বানাতে দিন, আর parallel বাদ দিন যদি না মেপে লাভ প্রমাণিত হয় — Map<String, Integer> stock = items.stream().collect(Collectors.groupingBy(Item::category, TreeMap::new, Collectors.summingInt(Item::qty)));. Collector parallel-এও নিরাপদ, কারণ প্রতিটি thread নিজের আলাদা পাত্রে জমায় আর শেষে stream সেগুলো মেলায়।

  4. লুপ না stream — সিদ্ধান্ত নিন: প্রতিটির জন্য বলুন কোনটা ভালো আর কেন। (ক) শিক্ষার্থীদের বিভাগ অনুযায়ী গড় নম্বর। (খ) একটি ফাইলের তালিকা থেকে প্রতিটি ফাইল পড়ে লাইন গোনা, কোনো ফাইল না থাকলে সেই নাম লগ করে পরেরটিতে যাওয়া। (গ) একটি ব্যাংক হিসাবের লেনদেন একে একে প্রয়োগ, আর ব্যালান্স শূন্যের নিচে গেলেই থামা। (ঘ) পণ্যের তালিকা থেকে স্টকে নেই এমন পণ্যের নাম, বর্ণানুক্রমে।

    (ক) stream — groupingBy(Student::dept, TreeMap::new, averagingDouble(Student::marks)); দলে ভাগ আর গড় এক লাইনে, লুপে লাগত একটি Map আর দুটি গণনা। (খ) লুপ — ফাইল পড়া checked IOException ছোড়ে, আর প্রতিটির জন্য আলাদা try-catch আর লগ লুপে স্বাভাবিক; stream-এ lambda-র ভেতরে মোড়ানো লাগত। (গ) লুপ — ব্যালান্স একটি চলমান অবস্থা, প্রতিটি লেনদেন আগেরটির ফলের ওপর নির্ভর করে, আর শর্তে মাঝপথে থামতে হয়; break-সহ for লুপ সবচেয়ে পরিষ্কার। (ঘ) stream — filter(p -> p.stock() == 0).map(Product::name).sorted().toList(); ঠিক সেই ধাঁচ, যার জন্য stream তৈরি।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমJava-তে list.stream().filter(...).map(...).toList() চলার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🧺 list.stream() উৎস থেকে একটি Stream তৈরি করে
  2. 📝 filter আর map শুধু ধাপের বর্ণনা জমা রাখে, কিছুই চলে না
  3. ▶️ terminal operation toList() ডাকা হয়, pipeline চালু হয়
  4. 🔁 উপাদানগুলো একে একে filter → map পেরিয়ে ফলের তালিকায় যায়
  5. 🔒 তালিকা ফেরত আসে; stream খরচ হয়ে যায়, আর ব্যবহার করা যায় না

Java-তে Stream.of(1, 2, 3).peek(x -> System.out.println(x)).map(x -> x * 2); — শুধু এই একটি statement চালালে কী ছাপা হবে?

  • 1, 2, 3
  • 2, 4, 6
  • কিছুই না
  • compile error: terminal operation নেই

Stream<String> s = list.stream(); s.count(); s.count(); — Java-তে দ্বিতীয় count() ডাকলে কী হয়?

  • আবার একই সংখ্যা ফেরত দেয়
  • IllegalStateException ছোড়ে
  • 0 ফেরত দেয়
  • compile error

একটি তালিকায় দুটি উপাদানের key একই। Collectors.toMap(keyFn, valueFn) — merge function ছাড়া — দিয়ে collect করলে Java-তে কী হয়?

  • IllegalStateException: Duplicate key
  • পরের মানটি আগেরটিকে নীরবে বদলে দেয়
  • প্রথম মানটি রেখে পরেরটি বাদ দেয়
  • দুটি মান একটি List-এ রাখে

List<Integer> marks = Stream.of(72, 28, 91).toList(); marks.add(50); — Java 16 বা পরের সংস্করণে কী ঘটে?

  • তালিকা হয় [72, 28, 91, 50]
  • compile error: add method নেই
  • ConcurrentModificationException
  • UnsupportedOperationException

Java-তে IntStream.of(72, 28, 91).average() কোন টাইপ ফেরত দেয়?

  • double
  • OptionalDouble
  • int
  • Optional<Double>

List.of(120, 80, 250, 50).stream().reduce(0, (a, b) -> a + b) — Java-তে ফল কী, আর তালিকা খালি হলে কী?

  • Optional[500]; খালি হলে Optional.empty
  • 500; খালি হলে NoSuchElementException
  • 500; খালি হলে 0
  • 500; খালি হলে null

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Stream = উৎস → মাঝের ধাপ → একটি শেষ ধাপ: আপনি লেখেন কী চাই (filter, map, sorted, toList); stream ডেটা রাখে না, উৎস বদলায় না।
  2. অলস ও একবারের: শেষ ধাপ না এলে কিছুই চলে না; উপাদান একে একে পুরো পাইপ পেরোয়; limit, findFirst, anyMatch আগেভাগে থামে; একই stream দুবার চালালে IllegalStateException.
  3. Collectors: groupingBy(key, TreeMap::new, counting()), partitioningBy, joining; toMap-এ duplicate key হলে exception, তাই merge function; toList()-এর তালিকা বদলানো যায় না।
  4. সংখ্যায় IntStream, খালির জন্য Optional: mapToInt(...).sum(); average() → OptionalDouble; max/findFirst → Optional; reduce(identity, নিয়ম) খালি হলে identity দেয়।
  5. বিচারবুদ্ধি: lambda-র ভেতরে side effect নয়; parallel() শুধু মেপে প্রমাণ পেলে; মাঝপথে জটিল থামা, checked exception বা চলমান অবস্থা থাকলে লুপই ভালো।

পরের পর্বে: Optional. এই পর্বে findFirst, max, average বারবার একটি “বাক্স” ফেরত দিয়েছে — এবার জানবেন সেটা খোলার নিরাপদ উপায়, orElse আর orElseGet-এর সূক্ষ্ম পার্থক্য, Optional-এর ওপর map আর flatMap, আর কোথায় Optional ব্যবহার করা উচিত নয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান