Back to Sorting & Searching
Q3Question 3 of 22

Java 8 Practical: Stream API with Employee/Trade model (grouping, sorting, aggregation)

Key Concepts
  • A stream pipeline has a source, zero or more lazy intermediate operations, and exactly one terminal operation.
  • groupingBy produces a Map keyed by a classifier; a downstream collector summarises each group.
  • partitioningBy splits data into exactly two groups based on a boolean predicate.
  • Use BigDecimal with reducing for money arithmetic; never use summingDouble or averagingDouble for financial balances.
  • sorted with a reversed Comparator combined with limit answers top-N queries efficiently.
  • Streams are single-use: once a terminal operation runs, the stream is consumed and cannot be reused.
  • toList() from Java 16 returns an unmodifiable list; use toCollection(ArrayList::new) when mutation is needed.

Definition

A stream is a lazy pipeline where intermediate steps like map and filter build the recipe and a terminal step like collect or count cooks the result.
  • Stream pipeline – a sequence of operations: source, lazy intermediate operations (map, filter, sorted), and one terminal operation (collect, count, reduce).
  • Collectors – ready-made reductions: groupingBy, partitioningBy, counting, joining, and reducing.
  • Lazy evaluation – intermediate operations do nothing until a terminal operation triggers execution.
  • Single-use – a stream is consumed after the terminal operation and cannot be reused.
  • Money safety – always use BigDecimal with reducing for financial calculations, never summingDouble.

Imagine the back office at National Bank of India has a huge pile of trade slips on a conveyor belt. Instead of writing a loop to manually sort them, the Stream API lets you describe what you want: group these slips by account, count how many are for each stock, put BUY slips in one tray and SELL slips in another, give me the three biggest trades. You describe the outcome, and the stream does the walking for you.

Understanding the Concepts

What is a stream pipeline?

A stream pipeline has three parts: a source (such as trades.stream()), zero or more intermediate operations, and exactly one terminal operation. Intermediate operations are lazy, meaning nothing runs until the terminal operation is called.

JAVA
package com.bank.stream;

import java.math.BigDecimal;
import java.util.List;

public class StreamBasics {

    record Trade(String id, String accountId, String symbol,
                 String side, BigDecimal amount) {}

    public static void main(String[] args) {
        List<Trade> trades = List.of(
            new Trade("T1", "ACC001", "HDFC", "BUY", new BigDecimal("50000")),
            new Trade("T2", "ACC001", "ICICI", "SELL", new BigDecimal("30000")),
            new Trade("T3", "ACC002", "HDFC", "BUY", new BigDecimal("75000")),
            new Trade("T4", "ACC002", "SBI", "BUY", new BigDecimal("40000")),
            new Trade("T5", "ACC001", "HDFC", "SELL", new BigDecimal("60000"))
        );

        // Pipeline: source -> filter -> map -> terminal
        long buyCount = trades.stream()
            .filter(t -> "BUY".equals(t.side()))
            .count();
        System.out.println("BUY trades: " + buyCount);
    }
}
TEXT
BUY trades: 3

How does groupingBy work?

The groupingBy collector turns a stream into a Map keyed by a classifier function. You can pass a second (downstream) collector to summarise each group instead of collecting raw lists.

JAVA
package com.bank.stream;

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

public class GroupingExample {

    record Trade(String id, String accountId, String symbol, BigDecimal amount) {}

    public static void main(String[] args) {
        List<Trade> trades = List.of(
            new Trade("T1", "ACC001", "HDFC", new BigDecimal("50000")),
            new Trade("T2", "ACC001", "ICICI", new BigDecimal("30000")),
            new Trade("T3", "ACC002", "HDFC", new BigDecimal("75000")),
            new Trade("T4", "ACC002", "SBI", new BigDecimal("40000"))
        );

        // Group by account
        Map<String, List<Trade>> byAccount = trades.stream()
            .collect(Collectors.groupingBy(Trade::accountId));
        byAccount.forEach((k, v) ->
            System.out.println(k + ": " + v.size() + " trades"));

        // Group by symbol, count per group
        Map<String, Long> countBySymbol = trades.stream()
            .collect(Collectors.groupingBy(Trade::symbol, Collectors.counting()));
        System.out.println("Count by symbol: " + countBySymbol);

        // Group by account, total amount using BigDecimal
        Map<String, BigDecimal> totalByAccount = trades.stream()
            .collect(Collectors.groupingBy(Trade::accountId,
                Collectors.reducing(BigDecimal.ZERO, Trade::amount, BigDecimal::add)));
        System.out.println("Total by account: " + totalByAccount);
    }
}
TEXT
ACC001: 2 trades
ACC002: 2 trades
Count by symbol: {SBI=1, HDFC=2, ICICI=1}
Total by account: {ACC001=80000, ACC002=115000}

How does partitioningBy work?

The partitioningBy collector splits the stream into exactly two groups based on a boolean predicate. It always returns a Map> with both keys present.

JAVA
package com.bank.stream;

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

public class PartitionExample {

    record Trade(String id, String side, int amount) {}

    public static void main(String[] args) {
        List<Trade> trades = List.of(
            new Trade("T1", "BUY", 50000),
            new Trade("T2", "SELL", 30000),
            new Trade("T3", "BUY", 75000),
            new Trade("T4", "SELL", 40000)
        );

        Map<Boolean, List<Trade>> partition = trades.stream()
            .collect(Collectors.partitioningBy(t -> "BUY".equals(t.side())));

        System.out.println("BUY:  " + partition.get(true).size());
        System.out.println("SELL: " + partition.get(false).size());
    }
}
TEXT
BUY:  2
SELL: 2

How do you sort and limit for top-N queries?

Combine sorted with a reversed comparator and limit to find the top-N elements. This is the stream equivalent of "show me the three largest trades."

JAVA
package com.bank.stream;

import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;

public class TopNTrades {

    record Trade(String id, BigDecimal amount) {}

    public static void main(String[] args) {
        List<Trade> trades = List.of(
            new Trade("T1", new BigDecimal("50000")),
            new Trade("T2", new BigDecimal("90000")),
            new Trade("T3", new BigDecimal("30000")),
            new Trade("T4", new BigDecimal("80000")),
            new Trade("T5", new BigDecimal("70000"))
        );

        System.out.println("Top 3 trades:");
        trades.stream()
            .sorted(Comparator.comparing(Trade::amount).reversed())
            .limit(3)
            .forEach(t -> System.out.println("  " + t.id() + ": " + t.amount()));
    }
}
TEXT
Top 3 trades:
  T2: 90000
  T4: 80000
  T5: 70000

Why must money use BigDecimal with reducing?

The summingDouble and averagingDouble collectors lose precision because double cannot represent all decimal fractions exactly. In banking, always use BigDecimal with the reducing collector for totals.

JAVA
package com.bank.stream;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

public class MoneyPrecision {
    public static void main(String[] args) {
        List<BigDecimal> amounts = List.of(
            new BigDecimal("0.10"),
            new BigDecimal("0.20"),
            new BigDecimal("0.30")
        );

        // WRONG: double loses precision
        double doubleSum = amounts.stream()
            .collect(Collectors.summingDouble(BigDecimal::doubleValue));
        System.out.println("Double sum:     " + doubleSum);

        // CORRECT: BigDecimal reducing
        BigDecimal bdSum = amounts.stream()
            .reduce(BigDecimal.ZERO, BigDecimal::add);
        System.out.println("BigDecimal sum: " + bdSum);
    }
}
TEXT
Double sum:     0.6000000000000001
BigDecimal sum: 0.60

How do you use joining to build string output?

The joining collector concatenates strings with a separator, useful for building human-readable output from IDs or names.

JAVA
package com.bank.stream;

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

public class JoiningExample {
    public static void main(String[] args) {
        List<String> tradeIds = List.of("T1", "T2", "T3", "T4");
        String result = tradeIds.stream()
            .collect(Collectors.joining(", ", "Trades: [", "]"));
        System.out.println(result);
    }
}
TEXT
Trades: [T1, T2, T3, T4]

What about distinct, map, and flatMap?

The distinct operation removes duplicates, map transforms each element, and flatMap flattens nested structures.

JAVA
package com.bank.stream;

import java.util.List;

public class MapAndDistinct {

    record Trade(String id, String symbol) {}

    public static void main(String[] args) {
        List<Trade> trades = List.of(
            new Trade("T1", "HDFC"), new Trade("T2", "ICICI"),
            new Trade("T3", "HDFC"), new Trade("T4", "SBI")
        );

        List<String> uniqueSymbols = trades.stream()
            .map(Trade::symbol)
            .distinct()
            .sorted()
            .toList();
        System.out.println("Unique symbols: " + uniqueSymbols);
    }
}
TEXT
Unique symbols: [HDFC, ICICI, SBI]

What are the common mistakes with streams?

The most common mistakes are: reusing a consumed stream, using summingDouble for money, and mutating the source collection during stream processing.

JAVA
package com.bank.stream;

import java.util.List;
import java.util.stream.Stream;

public class StreamMistakes {
    public static void main(String[] args) {
        Stream<String> s = List.of("A", "B", "C").stream();
        s.count(); // first terminal op: fine
        try {
            s.count(); // second terminal op: EXCEPTION
        } catch (IllegalStateException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}
TEXT
Error: stream has already been operated upon or closed

When should a senior engineer use streams versus loops?

Streams are best for declarative bulk transformations like grouping, filtering, and aggregating. Loops are better when you need early exit with break, need to modify external state, or when the logic is complex enough that a stream pipeline becomes hard to read. For large datasets, push heavy aggregation into the database with GROUP BY and use streams only to shape the final result.

Visual Overview

DIAGRAM
  Stream Pipeline:

  Source              Intermediate (lazy)         Terminal
  ┌────────┐   ┌──────┐  ┌──────┐  ┌──────┐   ┌─────────┐
  │ trades │──►│filter│──►│ map  │──►│sorted│──►│ collect │──► Result
  │.stream()│  │(BUY) │  │(amt) │  │(desc)│   │(toList) │
  └────────┘   └──────┘  └──────┘  └──────┘   └─────────┘
                 Nothing runs until collect() is called

  Collectors:
  ┌──────────────────┬─────────────────────────────────┐
  │ groupingBy       │ Map<K, List<V>>                  │
  │ groupingBy+count │ Map<K, Long>                     │
  │ partitioningBy   │ Map<Boolean, List<V>>            │
  │ reducing         │ Single aggregate (BigDecimal)    │
  │ joining          │ String concatenation             │
  │ toList           │ Unmodifiable list (Java 16+)     │
  └──────────────────┴─────────────────────────────────┘

Key Takeaway

The Java 8 Stream API is a declarative pipeline for processing collections. A pipeline has a source, lazy intermediate operations like filter, map, and sorted, and one terminal operation like collect or count that triggers execution. Because intermediates are lazy, the JVM can fuse them into a single pass.

The Collectors utility provides powerful aggregations: groupingBy buckets data by a key, partitioningBy splits on a boolean, counting and reducing summarise groups, and joining builds readable strings. For money arithmetic, always use BigDecimal with reducing, never summingDouble, because double arithmetic loses precision.

In a banking system at the National Bank of India, streams power reporting: grouping trades by account, counting by symbol, totalling amounts with BigDecimal, finding the top three trades by value, and partitioning BUY versus SELL. The one senior-level caution is that streams are single-use and in-memory, so for very large datasets push heavy aggregation into the database.

Streams should be favoured for readability of bulk transformations, but not forced when a simple loop is clearer. The goal is clean, declarative code that a teammate can read and maintain.

Remember one sentence: a stream is a lazy pipeline where intermediates build the recipe and the terminal operation cooks the result, with Collectors doing the grouping, counting, and joining.

---

Share
Use ← → arrow keys to navigate