Modern Java

Collectors, Grouping, and Reduction

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

আগের lessons-এ আমরা Stream দিয়ে data:

filter করেছি
transform করেছি
flatten করেছি
sort করেছি
match করেছি

Example:

courses.stream()
        .filter(
                Course::published
        )
        .map(
                Course::title
        )
        .toList();

এখানে final result ছিল:

List<String>

কিন্তু বাস্তব application-এ সবসময় List দরকার হয় না।

আমাদের প্রয়োজন হতে পারে:

unique values-এর Set
key-value Map
category অনুযায়ী grouping
true/false অনুযায়ী partition
comma-separated String
frequency count
total amount
একটি combined result

এই ধরনের result তৈরির জন্য Stream API-তে গুরুত্বপূর্ণ দুইটি concept হলো:

collect()
reduce()

এবং collect()-এর সাথে commonly ব্যবহার হয়:

Collectors

এই lesson-এ আমরা শিখব:

  • collect() কী
  • Collector concept
  • Collectors.toList()
  • Collectors.toSet()
  • joining()
  • toMap()
  • Duplicate key handling
  • groupingBy()
  • Downstream collectors
  • counting()
  • mapping()
  • partitioningBy()
  • reduce()
  • Identity value
  • Accumulator
  • Sum এবং product reduction
  • Object reduction
  • collect() বনাম reduce()
  • Common mistakes

Why Do We Need Collectors?

Suppose:

List<Course> courses

থেকে published Course-এর titles চাই।

আমরা already জানি:

List<String> titles =
        courses.stream()
                .filter(
                        Course::published
                )
                .map(
                        Course::title
                )
                .toList();

কিন্তু যদি চাই:

unique titles

তাহলে Set দরকার।

যদি চাই:

Course code → Course

তাহলে Map দরকার।

যদি চাই:

status → courses

তাহলে grouping দরকার।

অর্থাৎ:

Stream elements
→ কোনো structured result

এই transformation-এর জন্য collect() খুব powerful।


What Is collect()?

collect() হলো একটি terminal Stream operation।

এটি Stream-এর elements নিয়ে একটি result container বা structured result তৈরি করতে পারে।

Conceptually:

Stream elements
↓
Collector
↓
Result

Example:

List<String> titles =
        courses.stream()
                .map(
                        Course::title
                )
                .collect(
                        Collectors.toList()
                );

collect() Is a Terminal Operation

এই pipeline:

courses.stream()
        .map(
                Course::title
        )
        .collect(
                Collectors.toList()
        );

এখানে:

stream()
→ source

map()
→ intermediate

collect()
→ terminal

collect() pipeline consume করে result তৈরি করে।


Collectors

Java utility class:

java.util.stream.Collectors

অনেক predefined collectors দেয়।

Import:

import java.util.stream.Collectors;

Common collectors:

Collectors.toList()
Collectors.toSet()
Collectors.toMap()
Collectors.joining()
Collectors.groupingBy()
Collectors.partitioningBy()
Collectors.counting()
Collectors.mapping()

Collectors.toList()

Example:

List<String> titles =
        courses.stream()
                .map(
                        Course::title
                )
                .collect(
                        Collectors.toList()
                );

এটি Stream elements একটি List-এ collect করে।


toList() vs Collectors.toList()

Modern Java-তে আমরা সরাসরি লিখতে পারি:

stream.toList();

তাই simple List result-এর জন্য:

courses.stream()
        .map(
                Course::title
        )
        .toList();

সাধারণত cleaner।

তবে Collectors.toList() জানা important কারণ:

Collectors API-এর অন্যান্য collectors-এর সাথে একই mental model ব্যবহার হয়

এবং older/common Java codebases-এ এটিও frequently দেখা যায়।


Do Not Assume Exact List Implementation

এই code:

.collect(
        Collectors.toList()
)

থেকে result কোন exact List implementation হবে তা application code-এর assumption হওয়া উচিত নয়।

যদি explicitly mutable ArrayList দরকার হয়, পরিষ্কারভাবে create করুন।

Example:

List<String> titles =
        courses.stream()
                .map(
                        Course::title
                )
                .collect(
                        Collectors.toCollection(
                                ArrayList::new
                        )
                );

Collectors.toSet()

Unique values দরকার হলে:

Set<String> topics =
        courses.stream()
                .flatMap(
                        course ->
                                course.topics()
                                        .stream()
                )
                .collect(
                        Collectors.toSet()
                );

Result:

Set<String>

Duplicate topics থাকবে না।


distinct().toList() vs toSet()

দুইটির intent আলাদা।

If requirement:

unique values as a List

use:

stream.distinct()
        .toList();

If requirement:

result itself should be a Set

use:

.collect(
        Collectors.toSet()
);

Set Ordering

Collectors.toSet() থেকে কোনো specific iteration order assume করা উচিত নয়।

যদি insertion order specifically দরকার হয়:

.collect(
        Collectors.toCollection(
                LinkedHashSet::new
        )
);

যদি sorted Set দরকার হয়:

.collect(
        Collectors.toCollection(
                TreeSet::new
        )
);

joining()

Suppose titles:

Java Foundation
Backend Development
System Design

আমরা চাই:

Java Foundation, Backend Development, System Design

Use:

String titles =
        courses.stream()
                .map(
                        Course::title
                )
                .collect(
                        Collectors.joining(
                                ", "
                        )
                );

joining() Mental Model

String elements
↓
join using delimiter
↓
one String

Prefix and Suffix

joining() prefix এবং suffix-ও নিতে পারে।

String titles =
        courses.stream()
                .map(
                        Course::title
                )
                .collect(
                        Collectors.joining(
                                ", ",
                                "[",
                                "]"
                        )
                );

Result:

[Java Foundation, Backend Development, System Design]

toMap()

Suppose আমাদের দরকার:

Course code → Course

Use:

Map<String, Course> byCode =
        courses.stream()
                .collect(
                        Collectors.toMap(
                                Course::code,
                                course ->
                                        course
                        )
                );

Key Mapper and Value Mapper

toMap() এখানে দুইটি behavior নিচ্ছে।

Course::code

defines:

Course
→ key

And:

course -> course

defines:

Course
→ value

Function.identity()

এই Lambda:

course ->
        course

means:

input যেটা
output সেটাই

Java already provides:

Function.identity()

So:

Map<String, Course> byCode =
        courses.stream()
                .collect(
                        Collectors.toMap(
                                Course::code,
                                Function.identity()
                        )
                );

Import

import java.util.function.Function;

Map of Code to Title

If value হিসেবে পুরো Course দরকার না হয়:

Map<String, String> titleByCode =
        courses.stream()
                .collect(
                        Collectors.toMap(
                                Course::code,
                                Course::title
                        )
                );

Result conceptually:

JAVA → Java Foundation
BACKEND → Backend Development
SYSTEM-DESIGN → System Design

Duplicate Keys

toMap() ব্যবহার করার সময় duplicate key খুব important।

Suppose:

JAVA → first Course
JAVA → second Course

এবং:

Collectors.toMap(
        Course::code,
        Function.identity()
)

use করি।

Duplicate key থাকলে collection operation fail করতে পারে।

কারণ Java জানে না:

কোন value রাখবে?

Duplicate Key Is Often a Domain Error

যদি Course code unique হওয়া উচিত, duplicate data silently overwrite করা ঠিক নাও হতে পারে।

এই ক্ষেত্রে failure useful।

Example business invariant:

Each Course code must be unique.

তাহলে duplicate key detect হওয়া উচিত।


Merge Function

কখনো duplicate key expected।

Suppose একই word-এর latest value রাখতে চাই।

toMap() merge function নিতে পারে।

Example:

Map<String, Course> byCode =
        courses.stream()
                .collect(
                        Collectors.toMap(
                                Course::code,
                                Function.identity(),
                                (
                                        first,
                                        second
                                ) -> second
                        )
                );

Meaning:

duplicate key হলে
second value রাখো

Keep First

(
        first,
        second
) -> first

Never Add Merge Logic Without Business Meaning

Duplicate key exception এড়ানোর জন্য blindly:

(first, second) -> second

লিখবেন না।

প্রথমে প্রশ্ন করুন:

Duplicate কেন সম্ভব?

Which value should win?

Duplicate itself কি error?

Data correctness first।


groupingBy()

এখন Collectors-এর সবচেয়ে useful operations-এর একটিতে আসি।

Suppose Course-এর status আছে:

enum CourseStatus {
    DRAFT,
    PUBLISHED,
    ARCHIVED
}

Model:

record Course(
        String code,
        String title,
        CourseStatus status
) {
}

আমরা চাই:

DRAFT
→ draft courses

PUBLISHED
→ published courses

ARCHIVED
→ archived courses

Use:

Map<CourseStatus, List<Course>> byStatus =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Course::status
                        )
                );

groupingBy() Mental Model

Each element-এর জন্য একটি grouping key বের করা হয়।

Course
↓
status
↓
same status-এর Course একই group

Final result:

Map<Key, List<Element>>

Example Result

Conceptually:

DRAFT
→ [Course A, Course B]

PUBLISHED
→ [Course C, Course D]

ARCHIVED
→ [Course E]

Group by Price Category

Grouping key সবসময় existing field হতে হবে না।

Example:

Map<String, List<Course>> byPriceType =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                course ->
                                        course.priceInPaisa()
                                                == 0
                                                ? "FREE"
                                                : "PAID"
                        )
                );

But if this category is meaningful domain logic, an enum or named method may be better than raw strings।


Better Domain Category

enum PriceType {
    FREE,
    PAID
}

Then:

static PriceType priceType(
        Course course
) {
    return course.priceInPaisa()
            == 0
            ? PriceType.FREE
            : PriceType.PAID;
}

Group:

Map<PriceType, List<Course>> byPriceType =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Main::priceType
                        )
                );

Grouping Is More Than Lists

Default:

groupingBy(
        classifier
)

gives:

Map<K, List<T>>

কিন্তু আমরা প্রতিটি group-এর জন্য অন্য collector-ও ব্যবহার করতে পারি।

এটাকে বলা হয়:

downstream collector

Count Per Group

Suppose আমরা Course list না, count চাই।

Desired:

DRAFT → 3
PUBLISHED → 10
ARCHIVED → 2

Use:

Map<CourseStatus, Long> counts =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Course::status,
                                Collectors.counting()
                        )
                );

counting()

Collectors.counting() elements count করে এবং:

Long

result দেয়।

Grouping-এর সাথে খুব useful।


Grouping Titles Instead of Courses

Suppose:

status → List<String title>

Need:

Collectors.mapping(...)

mapping()

Example:

Map<CourseStatus, List<String>> titlesByStatus =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Course::status,
                                Collectors.mapping(
                                        Course::title,
                                        Collectors.toList()
                                )
                        )
                );

What Is Happening?

Outer collector:

groupingBy(
        Course::status,
        ...
)

groups by status।

Inside each group:

mapping(
        Course::title,
        toList()
)

means:

Course
→ title
→ List<String>

Downstream Collector Mental Model

groupingBy
→ group তৈরি করো

downstream collector
→ প্রতিটি group-এর ভিতরে result কী হবে?

Default:

List<Course>

Could be:

Long count
List<String> titles
Set<String> codes

Grouping Codes into Set

Map<CourseStatus, Set<String>> codesByStatus =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Course::status,
                                Collectors.mapping(
                                        Course::code,
                                        Collectors.toSet()
                                )
                        )
                );

partitioningBy()

Sometimes grouping key শুধু:

true
false

এই দুইটি।

Example:

Course published কি না

Use:

Map<Boolean, List<Course>> partition =
        courses.stream()
                .collect(
                        Collectors.partitioningBy(
                                Course::published
                        )
                );

Result

true
→ published courses

false
→ unpublished courses

partitioningBy() vs groupingBy()

partitioningBy() specifically boolean condition-এর জন্য।

true / false

groupingBy() arbitrary category key-এর জন্য।

Examples:

status
country
category
price range

Example — Paid vs Free

Map<Boolean, List<Course>> paid =
        courses.stream()
                .collect(
                        Collectors.partitioningBy(
                                course ->
                                        course.priceInPaisa()
                                        > 0
                        )
                );

Then:

paid.get(
        true
);

contains paid Courses।

But raw boolean key may sometimes reduce readability।

If:

FREE
PAID

is a real domain category, enum grouping can be clearer।


partitioningBy() with Counting

Map<Boolean, Long> counts =
        courses.stream()
                .collect(
                        Collectors.partitioningBy(
                                Course::published,
                                Collectors.counting()
                        )
                );

Result conceptually:

true → 8
false → 3

Reduction

এখন reduce() নিয়ে কথা বলি।

Reduction মানে অনেক values combine করে একটি smaller result বা single value তৈরি করা।

Example:

1
2
3
4

reduce with addition
↓
10

Simple Sum

int total =
        numbers.stream()
                .reduce(
                        0,
                        (
                                sum,
                                value
                        ) -> sum + value
                );

reduce() Parameters

এখানে:

0

হলো:

identity

আর:

(
        sum,
        value
) -> sum + value

হলো:

accumulator

Identity

Identity হলো initial value যেটি operation-এর neutral starting value।

Addition-এর জন্য:

0

কারণ:

0 + x = x

Multiplication-এর জন্য identity:

1

কারণ:

1 × x = x

Sum Example

Input:

10
20
30

Start:

sum = 0

Then:

0 + 10
→ 10

10 + 20
→ 30

30 + 30
→ 60

Final:

60

Method Reference

Instead of:

(
        sum,
        value
) -> sum + value

we can write:

Integer::sum

So:

int total =
        numbers.stream()
                .reduce(
                        0,
                        Integer::sum
                );

Product

int product =
        numbers.stream()
                .reduce(
                        1,
                        (
                                result,
                                value
                        ) ->
                                result * value
                );

Maximum Without Identity

Suppose:

List<Integer> numbers

Maximum খুঁজতে arbitrary identity choose করা dangerous হতে পারে।

Example:

0

identity দিলে negative-only data-তে wrong result হতে পারে।

Instead:

Optional<Integer> maximum =
        numbers.stream()
                .reduce(
                        Integer::max
                );

Why Optional?

Empty Stream হলে maximum নেই।

So:

reduce(
        Integer::max
)

returns:

Optional<Integer>

Minimum

Optional<Integer> minimum =
        numbers.stream()
                .reduce(
                        Integer::min
                );

তবে Stream-এর dedicated:

min(...)
max(...)

operations অনেক সময় intent clearer করে।

আমরা general reduction concept বোঝার জন্য reduce() দেখছি।


Sum of Course Prices

Suppose Course prices:

long totalPrice =
        courses.stream()
                .map(
                        Course::priceInPaisa
                )
                .reduce(
                        0L,
                        Long::sum
                );

Type flow:

Course
→ Long
→ one Long total

Primitive Streams Can Be Better for Numeric Aggregation

Java provides:

mapToInt()
mapToLong()
mapToDouble()

For prices:

long totalPrice =
        courses.stream()
                .mapToLong(
                        Course::priceInPaisa
                )
                .sum();

এটি numeric aggregation-এর intent আরও clearly express করে।

So if simple sum দরকার:

sum()

use করা preferable হতে পারে।


reduce() Is for Combining Values

Good mental model:

Many values
↓
combine
↓
one result

Examples:

sum
product
combined value
maximum
minimum

Do Not Use reduce() to Mutate a Collection

Bad idea:

List<String> result =
        stream.reduce(
                new ArrayList<>(),
                (
                        list,
                        value
                ) -> {
                    list.add(
                            value
                    );

                    return list;
                }
        );

এটি mutable collection accumulation-এর জন্য reduce() misuse।

Use:

collect(...)

or:

toList()

Why collect() for Mutable Containers?

collect() specifically mutable result containers-এর accumulation support করার জন্য designed।

Examples:

List
Set
Map
StringBuilder-like accumulation
grouped structures

Reduction conceptually better fits immutable-style value combination।


collect() vs reduce()

A useful practical distinction:

collect()
→ many elements into a mutable/structured container

reduce()
→ many values into one combined value

Examples:

List<String>
→ collect

Map<String, Course>
→ collect

Grouped Map
→ collect

Sum
→ reduce or numeric sum()

Product
→ reduce

Single combined object/value
→ reduce may fit

Do Not Force reduce()

Suppose need count।

Could technically build a reduction।

But:

stream.count()

is clearer।

Need sum:

mapToLong(...).sum()

can be clearer।

Need max:

stream.max(...)

can be clearer।

Use specialized APIs when they communicate intent directly।


Counting Published Courses

Simple version:

long count =
        courses.stream()
                .filter(
                        Course::published
                )
                .count();

No need:

reduce(...)

Grouping Example — Courses by Status

Complete example:

Map<CourseStatus, List<Course>> grouped =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Course::status
                        )
                );

Then:

List<Course> published =
        grouped.get(
                CourseStatus.PUBLISHED
        );

Missing Group

যদি কোনো status-এর Course না থাকে:

grouped.get(
        CourseStatus.ARCHIVED
)

may return:

null

because the Map may not contain that key।

Safer:

List<Course> archived =
        grouped.getOrDefault(
                CourseStatus.ARCHIVED,
                List.of()
        );

Grouping Does Not Automatically Create Every Enum Key

Suppose enum:

DRAFT
PUBLISHED
ARCHIVED

কিন্তু data-তে archived Course নেই।

groupingBy() necessarily:

ARCHIVED → []

entry তৈরি করবে না।

Only encountered groups expect করুন।


Frequency Counting with Grouping

Suppose tags:

Java
Backend
Java
OOP
Java
Backend

We can count:

Map<String, Long> frequencies =
        tags.stream()
                .collect(
                        Collectors.groupingBy(
                                Function.identity(),
                                Collectors.counting()
                        )
                );

Result:

Java → 3
Backend → 2
OOP → 1

Compare with HashMap merge()

আগের lesson-এ frequency counter লিখেছিলাম:

Map<String, Integer> counts =
        new HashMap<>();

for (
        String tag
        : tags
) {
    counts.merge(
            tag,
            1,
            Integer::sum
    );
}

Stream version:

Map<String, Long> counts =
        tags.stream()
                .collect(
                        Collectors.groupingBy(
                                Function.identity(),
                                Collectors.counting()
                        )
                );

দুইটিই valid।


Which Is Better?

যদি simple counting algorithm এবং mutable state explicit দেখতে চান:

HashMap + loop

খুব clear।

যদি existing Stream transformation-এর অংশ হিসেবে grouping/counting হয়:

groupingBy + counting

natural হতে পারে।


Complex Collector Pipelines

Collector APIs powerful হওয়ায় nested expressions দ্রুত difficult হয়ে যেতে পারে।

Example:

Collectors.groupingBy(
        Course::status,
        Collectors.mapping(
                Course::title,
                Collectors.toSet()
        )
)

এটি এখনো readable।

কিন্তু যদি nesting অনেক বেড়ে যায়:

grouping
mapping
filtering
collectingAndThen
another nested collector

তাহলে code split বা named method consider করুন।


Clear Code Beats Clever Collector Code

Goal:

Reader যেন বুঝতে পারে result কী।

Not:

এক expression-এ সব Collectors ব্যবহার করা।

Practical Example — Titles by Status

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Main {

    public static void main(String[] args) {
        List<Course> courses =
                List.of(
                        new Course(
                                "JAVA",
                                "Java Foundation",
                                CourseStatus.PUBLISHED
                        ),
                        new Course(
                                "BACKEND",
                                "Backend Development",
                                CourseStatus.PUBLISHED
                        ),
                        new Course(
                                "SYSTEM",
                                "System Design",
                                CourseStatus.DRAFT
                        )
                );

        Map<CourseStatus, List<String>> titlesByStatus =
                courses.stream()
                        .collect(
                                Collectors.groupingBy(
                                        Course::status,
                                        Collectors.mapping(
                                                Course::title,
                                                Collectors.toList()
                                        )
                                )
                        );

        System.out.println(
                titlesByStatus
        );
    }

    enum CourseStatus {
        DRAFT,
        PUBLISHED,
        ARCHIVED
    }

    record Course(
            String code,
            String title,
            CourseStatus status
    ) {
    }
}

Practical Example — Course Index

Map<String, Course> coursesByCode =
        courses.stream()
                .collect(
                        Collectors.toMap(
                                Course::code,
                                Function.identity()
                        )
                );

Use:

Course course =
        coursesByCode.get(
                "JAVA"
        );

Practical Example — Unique Topics

Set<String> topics =
        courses.stream()
                .flatMap(
                        course ->
                                course.topics()
                                        .stream()
                )
                .collect(
                        Collectors.toSet()
                );

Practical Example — Display String

String titles =
        courses.stream()
                .map(
                        Course::title
                )
                .collect(
                        Collectors.joining(
                                " | "
                        )
                );

Result:

Java Foundation | Backend Development | System Design

Practical Example — Total Price

long total =
        courses.stream()
                .mapToLong(
                        Course::priceInPaisa
                )
                .sum();

যদিও lesson reduction নিয়ে, simple numeric sum-এর জন্য এই versionই clearer।


Practical Example — Manual Reduction

long total =
        courses.stream()
                .map(
                        Course::priceInPaisa
                )
                .reduce(
                        0L,
                        Long::sum
                );

Same conceptual result।


Common Mistake 1 — Using toMap() Without Considering Duplicate Keys

If keys may duplicate:

Collectors.toMap(...)

এর behavior আগে decide করুন।

Duplicate:

error?
first wins?
last wins?
combine values?

Business rule explicit করুন।


Common Mistake 2 — Assuming Grouping Has Every Possible Key

No data:

ARCHIVED

means Map-এ:

ARCHIVED

key necessarily থাকবে না।


Common Mistake 3 — Raw Boolean Maps Everywhere

Map<Boolean, List<Course>>

technically fine।

কিন্তু domain concept যদি:

FREE
PAID

হয়, enum map clearer হতে পারে।


Common Mistake 4 — Using reduce() for Lists

Do not manually mutate an ArrayList inside reduce()

Use:

toList()
collect()

Common Mistake 5 — Using collect() for Simple Sum

Possible হলেও unnecessary abstraction হতে পারে।

Prefer:

mapToLong(...)
        .sum()

when appropriate।


Common Mistake 6 — Incorrect Identity

Suppose multiplication:

.reduce(
        0,
        (
                result,
                value
        ) -> result * value
)

Everything becomes:

0

because multiplication identity should be:

1

Common Mistake 7 — Bad Maximum Identity

For negative values:

.reduce(
        0,
        Integer::max
)

can produce wrong result।

Example input:

-10
-5

Result would incorrectly include:

0

Use no-identity reduction or dedicated:

max(...)

Common Mistake 8 — Using Collectors Without Understanding Result Type

Always reason about:

Stream<T>
↓
Collector
↓
Result type

Example:

groupingBy(
        Course::status
)

produces conceptually:

Map<CourseStatus, List<Course>>

Common Mistake 9 — Huge Nested Collector Expressions

If collector expression takes significant effort to decode, break it into:

named functions
named classifiers
named downstream collectors

or a normal loop।


Common Mistake 10 — Assuming Functional Means No Complexity

This:

groupingBy(...)

still needs:

memory
hashing
element processing

Collector syntax does not remove algorithmic cost।


Practice 1 — Collect to Set

Convert:

Stream<String>

to unique Set<String>

Solution

Set<String> result =
        stream.collect(
                Collectors.toSet()
        );

Practice 2 — Join Names

Given:

Stream<String>

produce:

Sakib, Subu, Sumu

Solution

String result =
        stream.collect(
                Collectors.joining(
                        ", "
                )
        );

Practice 3 — Build Map

Given Course, build:

code → title

Solution

Map<String, String> result =
        courses.stream()
                .collect(
                        Collectors.toMap(
                                Course::code,
                                Course::title
                        )
                );

Practice 4 — Duplicate Keys

Two Courses have the same code।

Should you automatically keep the last one?

Answer

Not unless business rules explicitly say so।

Duplicate Course code may indicate invalid data and should often fail rather than silently overwrite।


Practice 5 — Group by Status

Solution

Map<CourseStatus, List<Course>> result =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Course::status
                        )
                );

Practice 6 — Count by Status

Solution

Map<CourseStatus, Long> result =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Course::status,
                                Collectors.counting()
                        )
                );

Practice 7 — Titles by Status

Solution

Map<CourseStatus, List<String>> result =
        courses.stream()
                .collect(
                        Collectors.groupingBy(
                                Course::status,
                                Collectors.mapping(
                                        Course::title,
                                        Collectors.toList()
                                )
                        )
                );

Practice 8 — Partition

Split numbers into:

even
not even

Solution

Map<Boolean, List<Integer>> result =
        numbers.stream()
                .collect(
                        Collectors.partitioningBy(
                                number ->
                                        number % 2
                                        == 0
                        )
                );

Practice 9 — Sum with Reduce

Solution

int total =
        numbers.stream()
                .reduce(
                        0,
                        Integer::sum
                );

Practice 10 — Product

Solution

int product =
        numbers.stream()
                .reduce(
                        1,
                        (
                                result,
                                value
                        ) ->
                                result * value
                );

Practice 11 — Maximum

Why may this be wrong?

numbers.stream()
        .reduce(
                0,
                Integer::max
        );

Answer

If all values are negative, 0 becomes an artificial candidate।

Better:

numbers.stream()
        .reduce(
                Integer::max
        );

which returns an Optional<Integer>


Practice 12 — Collect or Reduce?

Need:

Course code → Course

Answer

collect()

with:

Collectors.toMap(...)

Practice 13 — Collect or Reduce?

Need product of all integers।

Answer

reduce()

is a natural choice।


Practice 14 — Best API

Need total price of all courses।

Which is clearer?

reduce()
mapToLong().sum()

Answer

Usually:

courses.stream()
        .mapToLong(
                Course::priceInPaisa
        )
        .sum();

because intent is explicitly numeric summation।


True or False

  1. collect() is a terminal operation.
  2. Collectors.toSet() can remove duplicate values.
  3. joining() produces a String.
  4. toMap() can encounter duplicate-key problems.
  5. Duplicate keys should always silently overwrite old values.
  6. groupingBy() commonly produces a Map of groups.
  7. Default groupingBy() groups values into Lists.
  8. counting() can be used as a downstream collector.
  9. mapping() can transform values inside each group.
  10. partitioningBy() groups by arbitrary String keys.
  11. reduce() can combine many values into one result.
  12. Addition identity is 0.
  13. Multiplication identity is 1.
  14. reduce() is the preferred way to mutate and build an ArrayList.
  15. Specialized APIs such as sum() may be clearer than generic reduce().

Answers

1. True
2. True
3. True
4. True
5. False
6. True
7. True
8. True
9. True
10. False
11. True
12. True
13. True
14. False
15. True

Knowledge Check

Question 1

collect() কী করে?

Question 2

Collectors.toSet() কখন useful?

Question 3

joining() কী result তৈরি করে?

Question 4

toMap()-এ duplicate key কেন important?

Question 5

groupingBy() কী করে?

Question 6

Downstream collector কী?

Question 7

counting() এবং mapping() grouping-এর সাথে কীভাবে useful?

Question 8

partitioningBy() এবং groupingBy()-এর difference কী?

Question 9

Reduction কী?

Question 10

reduce()-এ identity value কী?

Question 11

কেন incorrect identity wrong result দিতে পারে?

Question 12

collect() এবং reduce() কখন ব্যবহার করবেন?


Knowledge Check Answers

Answer 1

collect() Stream-এর elements নিয়ে একটি structured result তৈরি করে।

Examples:

List
Set
Map
Grouped Map
String

Answer 2

যখন result একটি unique-value Set হওয়া উচিত।

Example:

unique course topics

Answer 3

একাধিক String element delimiter ব্যবহার করে combine করে একটি single String তৈরি করে।

Answer 4

Map-এ একটি key-এর জন্য একটি value থাকে। Duplicate key এলে decide করতে হয় duplicate invalid, প্রথম value রাখা হবে, দ্বিতীয় value রাখা হবে, নাকি values combine হবে।

Answer 5

প্রতিটি element-এর জন্য একটি classification key বের করে একই key-এর elements একই group-এ collect করে।

Default result conceptually:

Map<K, List<T>>

Answer 6

groupingBy()-এর প্রতিটি group-এর elements কীভাবে final result-এ collect হবে সেটি downstream collector define করে।

Example:

List তৈরি
count করা
titles map করা
Set তৈরি

Answer 7

counting() প্রতিটি group-এর element count করতে পারে।

mapping() group-এর original values অন্য type-এ transform করে তারপর downstream collector-এ পাঠায়।

Answer 8

partitioningBy() একটি boolean condition অনুযায়ী:

true
false

দুইটি partition তৈরি করে।

groupingBy() arbitrary classification key অনুযায়ী multiple groups তৈরি করতে পারে।

Answer 9

অনেক values combine করে একটি smaller বা single result তৈরি করার process হলো reduction।

Example:

sum
product
maximum

Answer 10

Identity হলো reduction-এর starting neutral value।

Example:

Addition:
0

Multiplication:
1

Answer 11

Identity operation-এর neutral value না হলে এটি actual input-এর বাইরে একটি artificial value হিসেবে result পরিবর্তন করতে পারে।

Answer 12

Structured mutable/container-style result-এর জন্য সাধারণত:

collect()

আর values combine করে single result-এর জন্য:

reduce()

useful।

তবে specialized operations:

sum()
count()
min()
max()

থাকলে সেগুলো আরও expressive হতে পারে।


Practical Collector Selection Guide

Need List:

toList()

অথবা:

Collectors.toList()

Need Set:

Collectors.toSet()

Need String:

Collectors.joining(...)

Need key-value index:

Collectors.toMap(...)

Need groups:

Collectors.groupingBy(...)

Need true/false split:

Collectors.partitioningBy(...)

Need count per group:

Collectors.groupingBy(
        keyFunction,
        Collectors.counting()
)

Need transformed values inside groups:

Collectors.mapping(...)

Need one combined value:

reduce(...)

Core Mental Model

Stream processing-এর শেষে নিজেকে প্রশ্ন করুন:

আমি final result হিসেবে কী চাই?

If answer:

List
Set
Map
Grouped Map
String

think:

collect

If answer:

একটি combined value

think:

reduction

Example:

Courses
→ group by status
→ Map<CourseStatus, List<Course>>

Topics
→ unique
→ Set<String>

Titles
→ join
→ String

Prices
→ add together
→ long total

Lesson Summary

এই lesson-এ আমরা Stream results aggregate এবং organize করার গুরুত্বপূর্ণ techniques শিখেছি।

আমরা শিখেছি:

  • collect() একটি terminal operation
  • Collectors predefined collection strategies দেয়
  • Collectors.toList() List result তৈরি করতে পারে
  • Collectors.toSet() Set result তৈরি করে
  • joining() multiple Strings combine করে
  • toMap() key-value structure তৈরি করে
  • toMap()-এর duplicate-key behavior carefully design করতে হয়
  • Function.identity() input value unchanged return করে
  • groupingBy() values category অনুযায়ী group করে
  • Default grouping result সাধারণত Map<K, List<T>>
  • Downstream collectors group result customize করে
  • counting() group size calculate করতে পারে
  • mapping() grouped values transform করতে পারে
  • partitioningBy() boolean condition অনুযায়ী দুইটি partition তৈরি করে
  • reduce() many values combine করে single result তৈরি করতে পারে
  • Reduction-এর identity neutral value হওয়া প্রয়োজন
  • Wrong identity wrong result দিতে পারে
  • reduce() mutable List-building-এর জন্য appropriate নয়
  • collect() structured result accumulation-এর জন্য better fit
  • Numeric aggregation-এর জন্য sum()-এর মতো specialized APIs clearer হতে পারে
  • Powerful collector expressions readability নষ্ট করলে simpler code prefer করা উচিত

সবচেয়ে গুরুত্বপূর্ণ distinction:

collect()
→ structure বানাও

reduce()
→ values combine করো

আর practical rule:

Use the most specific API
that clearly communicates the result you want.

Next Lesson

পরবর্তী lesson:

Optional and Modern Null Handling

আমরা শিখব:

  • null কেন problematic হতে পারে
  • Optional<T> কী
  • Optional.empty()
  • Optional.of()
  • Optional.ofNullable()
  • isPresent()
  • isEmpty()
  • ifPresent()
  • map()
  • flatMap()
  • filter()
  • orElse()
  • orElseGet()
  • orElseThrow()
  • or()
  • Optional chaining
  • Optional return type কখন appropriate
  • Optional field বা parameter হিসেবে blindly ব্যবহার করা কেন ভালো নয়
  • Optional.get() কেন avoid করা উচিত
  • Modern null-handling strategies