Java – Stream

1. What is the Stream API?

Imagine an assembly line in a factory. Raw materials enter at one end, pass through a series of workstations — each performing a specific operation like cutting, painting, or inspecting — and a finished product comes out the other end. The assembly line does not store the materials; it processes them as they flow through.

The Java Stream API, introduced in Java 8, works exactly like that assembly line. A Stream is a sequence of elements that supports a pipeline of operations to process data declaratively — you describe what you want, not how to do it step by step.

Key characteristics of Streams:

  • Not a data structure — A Stream does not store elements. It pulls elements from a source (like a List or array) and pushes them through a pipeline of operations.
  • Pipeline of operations — You chain multiple operations together. Each operation takes input, transforms it, and passes the result to the next operation.
  • Lazy evaluation — Intermediate operations are not executed until a terminal operation is invoked. This allows the Stream to optimize the processing (e.g., short-circuiting).
  • Does not modify the source — Streaming over a List does not add, remove, or change elements in that List. The original data remains untouched.
  • Can only be consumed once — Once a terminal operation is called, the Stream is spent. To process the data again, you must create a new Stream.
  • Supports parallelism — You can switch to parallel execution with a single method call, leveraging multi-core processors.

Stream Pipeline Structure

Every Stream pipeline has three parts:

Part Description Example
Source Where the data comes from list.stream(), Arrays.stream(arr)
Intermediate operations Transform the stream (lazy, return a new Stream) filter(), map(), sorted()
Terminal operation Produces a result or side effect (triggers execution) collect(), forEach(), count()
import java.util.Arrays;
import java.util.List;

public class StreamIntro {
    public static void main(String[] args) {
        List names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

        // Stream pipeline: source -> intermediate ops -> terminal op
        long count = names.stream()          // Source: create stream from list
                         .filter(n -> n.length() > 3)  // Intermediate: keep names longer than 3 chars
                         .map(String::toUpperCase)      // Intermediate: convert to uppercase
                         .count();                      // Terminal: count remaining elements

        System.out.println("Count: " + count);
        // Output: Count: 3

        // The original list is unchanged
        System.out.println("Original: " + names);
        // Output: Original: [Alice, Bob, Charlie, David, Eve]
    }
}

2. Creating Streams

Before you can process data with the Stream API, you need to create a Stream. Java provides multiple ways to do this depending on your data source.

2.1 From Collections

The most common way. Every class that implements Collection (List, Set, Queue) has a stream() method.

import java.util.*;

public class StreamFromCollections {
    public static void main(String[] args) {
        // From a List
        List list = List.of("Java", "Python", "Go");
        list.stream().forEach(System.out::println);

        // From a Set
        Set set = Set.of(1, 2, 3, 4, 5);
        set.stream().filter(n -> n % 2 == 0).forEach(System.out::println);

        // From a Map (via entrySet, keySet, or values)
        Map map = Map.of("Alice", 90, "Bob", 85);
        map.entrySet().stream()
           .filter(e -> e.getValue() > 87)
           .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));
        // Output: Alice: 90
    }
}

2.2 From Arrays

import java.util.Arrays;
import java.util.stream.Stream;

public class StreamFromArrays {
    public static void main(String[] args) {
        String[] colors = {"Red", "Green", "Blue"};

        // Using Arrays.stream()
        Arrays.stream(colors).forEach(System.out::println);

        // Partial array: from index 1 (inclusive) to 3 (exclusive)
        Arrays.stream(colors, 1, 3).forEach(System.out::println);
        // Output: Green, Blue

        // Using Stream.of()
        Stream.of("One", "Two", "Three").forEach(System.out::println);

        // From a primitive array -- returns IntStream, not Stream
        int[] numbers = {10, 20, 30};
        int sum = Arrays.stream(numbers).sum();
        System.out.println("Sum: " + sum); // Output: Sum: 60
    }
}

2.3 Stream Factory Methods

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

public class StreamFactoryMethods {
    public static void main(String[] args) {
        // Stream.empty() -- useful as a return value instead of null
        Stream empty = Stream.empty();
        System.out.println("Empty count: " + empty.count()); // Output: Empty count: 0

        // Stream.of() -- create from individual elements
        Stream languages = Stream.of("Java", "Python", "Go");

        // Stream.generate() -- infinite stream from a Supplier
        // MUST use limit() or it runs forever!
        Stream.generate(Math::random)
              .limit(3)
              .forEach(n -> System.out.printf("%.2f%n", n));

        // Stream.iterate() -- infinite stream with a seed and unary operator
        // Java 8 style (no predicate -- must use limit)
        Stream.iterate(1, n -> n * 2)
              .limit(5)
              .forEach(System.out::println);
        // Output: 1, 2, 4, 8, 16

        // Java 9+ style (with predicate -- like a for loop)
        Stream.iterate(1, n -> n <= 100, n -> n * 2)
              .forEach(System.out::println);
        // Output: 1, 2, 4, 8, 16, 32, 64

        // IntStream.range() and rangeClosed()
        IntStream.range(1, 5).forEach(System.out::println);       // 1, 2, 3, 4
        IntStream.rangeClosed(1, 5).forEach(System.out::println);  // 1, 2, 3, 4, 5
    }
}

2.4 Streams from Files

You can create a Stream of lines from a file using Files.lines(). This is memory-efficient because it reads lines lazily rather than loading the entire file into memory.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.stream.Stream;

public class StreamFromFiles {
    public static void main(String[] args) {
        // Files.lines() returns a Stream -- one element per line
        // Use try-with-resources because the stream must be closed
        try (Stream lines = Files.lines(Paths.get("data.txt"))) {
            lines.filter(line -> !line.isBlank())
                 .map(String::trim)
                 .forEach(System.out::println);
        } catch (IOException e) {
            System.err.println("Error reading file: " + e.getMessage());
        }
    }
}

Stream Creation Summary

Method Returns Use Case
collection.stream() Stream<T> Most common — stream from any Collection
Arrays.stream(array) Stream<T> or IntStream Stream from an array
Stream.of(a, b, c) Stream<T> Stream from individual values
Stream.empty() Stream<T> Empty stream (null-safe return)
Stream.generate(supplier) Stream<T> Infinite stream from a Supplier
Stream.iterate(seed, op) Stream<T> Infinite stream with iterative computation
IntStream.range(a, b) IntStream Range of ints [a, b)
IntStream.rangeClosed(a, b) IntStream Range of ints [a, b]
Files.lines(path) Stream<String> Lazy line-by-line file reading

3. Intermediate Operations

Intermediate operations transform a Stream into another Stream. They are lazy — nothing happens until a terminal operation triggers the pipeline. You can chain as many intermediate operations as you need.

3.1 filter()

filter(Predicate<T>) keeps only the elements that match the given condition. Think of it as a sieve — elements that pass the test go through; those that do not are discarded.

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

public class FilterExample {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Keep only even numbers
        List evens = numbers.stream()
                                     .filter(n -> n % 2 == 0)
                                     .collect(Collectors.toList());
        System.out.println("Evens: " + evens);
        // Output: Evens: [2, 4, 6, 8, 10]

        // Chaining multiple filters (equivalent to && in the predicate)
        List result = numbers.stream()
                                      .filter(n -> n > 3)
                                      .filter(n -> n < 8)
                                      .collect(Collectors.toList());
        System.out.println("Between 3 and 8: " + result);
        // Output: Between 3 and 8: [4, 5, 6, 7]

        // Filter with objects
        List names = List.of("Alice", "Bob", "Charlie", "Ana", "Albert");
        List aNames = names.stream()
                                   .filter(name -> name.startsWith("A"))
                                   .collect(Collectors.toList());
        System.out.println("A-names: " + aNames);
        // Output: A-names: [Alice, Ana, Albert]
    }
}

3.2 map()

map(Function<T, R>) transforms each element from type T to type R. It applies the given function to every element and produces a new Stream of the results. This is one of the most frequently used operations.

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

public class MapExample {
    public static void main(String[] args) {
        List names = List.of("alice", "bob", "charlie");

        // Transform: String -> String (uppercase)
        List upper = names.stream()
                                  .map(String::toUpperCase)
                                  .collect(Collectors.toList());
        System.out.println(upper);
        // Output: [ALICE, BOB, CHARLIE]

        // Transform: String -> Integer (get length)
        List lengths = names.stream()
                                     .map(String::length)
                                     .collect(Collectors.toList());
        System.out.println(lengths);
        // Output: [5, 3, 7]

        // Transform: Integer -> String
        List numbers = List.of(1, 2, 3);
        List labels = numbers.stream()
                                     .map(n -> "Item #" + n)
                                     .collect(Collectors.toList());
        System.out.println(labels);
        // Output: [Item #1, Item #2, Item #3]
    }
}

3.3 flatMap()

flatMap(Function<T, Stream<R>>) is used when each element maps to multiple elements (a stream of values). It “flattens” nested structures into a single stream. This is essential when you have lists of lists, or when a mapping function returns a collection for each element.

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

public class FlatMapExample {
    public static void main(String[] args) {
        // Problem: We have a list of lists and want a single flat list
        List> nested = List.of(
            List.of("Java", "Kotlin"),
            List.of("Python", "Ruby"),
            List.of("Go", "Rust")
        );

        // Using map() -- gives Stream>, NOT what we want
        // Using flatMap() -- gives Stream, flattened!
        List flat = nested.stream()
                                  .flatMap(List::stream)  // Each list becomes a stream, all merged
                                  .collect(Collectors.toList());
        System.out.println(flat);
        // Output: [Java, Kotlin, Python, Ruby, Go, Rust]

        // Real-world: extracting all words from sentences
        List sentences = List.of("Hello World", "Java Streams are powerful");
        List words = sentences.stream()
                                      .flatMap(s -> List.of(s.split(" ")).stream())
                                      .collect(Collectors.toList());
        System.out.println(words);
        // Output: [Hello, World, Java, Streams, are, powerful]

        // Real-world: customers with multiple orders
        // Each customer has a list of orders; we want all orders in one stream
        // customer.stream().flatMap(c -> c.getOrders().stream())
    }
}

3.4 sorted()

sorted() sorts elements in natural order (for types implementing Comparable). You can also pass a custom Comparator for complex sorting.

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

public class SortedExample {
    public static void main(String[] args) {
        // Natural order (ascending)
        List numbers = List.of(5, 3, 8, 1, 9, 2);
        List sorted = numbers.stream()
                                      .sorted()
                                      .collect(Collectors.toList());
        System.out.println(sorted);
        // Output: [1, 2, 3, 5, 8, 9]

        // Reverse order
        List descending = numbers.stream()
                                          .sorted(Comparator.reverseOrder())
                                          .collect(Collectors.toList());
        System.out.println(descending);
        // Output: [9, 8, 5, 3, 2, 1]

        // Sorting strings by length
        List names = List.of("Charlie", "Bob", "Alice", "Eve");
        List byLength = names.stream()
                                     .sorted(Comparator.comparingInt(String::length))
                                     .collect(Collectors.toList());
        System.out.println(byLength);
        // Output: [Bob, Eve, Alice, Charlie]

        // Sorting by length, then alphabetically for ties
        List byLengthThenAlpha = names.stream()
            .sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
            .collect(Collectors.toList());
        System.out.println(byLengthThenAlpha);
        // Output: [Bob, Eve, Alice, Charlie]
    }
}

3.5 distinct()

distinct() removes duplicate elements from the stream. It relies on the equals() and hashCode() methods to determine equality. For custom objects, you must override these methods for distinct() to work correctly.

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

public class DistinctExample {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 2, 4, 3, 5, 1);
        List unique = numbers.stream()
                                      .distinct()
                                      .collect(Collectors.toList());
        System.out.println(unique);
        // Output: [1, 2, 3, 4, 5]

        // With strings (equals/hashCode already implemented)
        List words = List.of("hello", "world", "hello", "java", "world");
        List uniqueWords = words.stream()
                                        .distinct()
                                        .collect(Collectors.toList());
        System.out.println(uniqueWords);
        // Output: [hello, world, java]
    }
}

3.6 peek()

peek(Consumer<T>) allows you to perform a side effect on each element without modifying the stream. Its primary use is debugging — inspecting elements at a certain stage of the pipeline. Avoid using peek() for business logic; it may not execute if the pipeline is optimized away.

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

public class PeekExample {
    public static void main(String[] args) {
        List result = List.of("one", "two", "three", "four")
            .stream()
            .filter(s -> s.length() > 3)
            .peek(s -> System.out.println("After filter: " + s))
            .map(String::toUpperCase)
            .peek(s -> System.out.println("After map: " + s))
            .collect(Collectors.toList());

        // Output:
        // After filter: three
        // After map: THREE
        // After filter: four
        // After map: FOUR

        System.out.println("Result: " + result);
        // Output: Result: [THREE, FOUR]
    }
}

3.7 limit() and skip()

limit(n) truncates the stream to at most n elements. skip(n) discards the first n elements. Together, they form a powerful pagination pattern.

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

public class LimitSkipExample {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // First 3 elements
        List firstThree = numbers.stream()
                                          .limit(3)
                                          .collect(Collectors.toList());
        System.out.println("First 3: " + firstThree);
        // Output: First 3: [1, 2, 3]

        // Skip first 7 elements
        List lastThree = numbers.stream()
                                         .skip(7)
                                         .collect(Collectors.toList());
        System.out.println("Last 3: " + lastThree);
        // Output: Last 3: [8, 9, 10]

        // Pagination pattern: page 2, page size 3 (items 4, 5, 6)
        int pageSize = 3;
        int pageNumber = 2; // 1-based
        List page = numbers.stream()
                                    .skip((long) (pageNumber - 1) * pageSize)
                                    .limit(pageSize)
                                    .collect(Collectors.toList());
        System.out.println("Page 2: " + page);
        // Output: Page 2: [4, 5, 6]
    }
}

3.8 mapToInt(), mapToLong(), mapToDouble()

These operations convert a Stream<T> to a primitive stream (IntStream, LongStream, DoubleStream). Primitive streams avoid autoboxing overhead and provide specialized methods like sum(), average(), and max().

import java.util.List;

public class MapToPrimitiveExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "David");

        // mapToInt: get lengths as IntStream
        int totalChars = names.stream()
                              .mapToInt(String::length)
                              .sum();
        System.out.println("Total characters: " + totalChars);
        // Output: Total characters: 20

        // average returns OptionalDouble
        names.stream()
             .mapToInt(String::length)
             .average()
             .ifPresent(avg -> System.out.printf("Average length: %.1f%n", avg));
        // Output: Average length: 5.0

        // mapToDouble: useful for decimal calculations
        List prices = List.of(100, 200, 300);
        double totalWithTax = prices.stream()
                                    .mapToDouble(p -> p * 1.08)
                                    .sum();
        System.out.printf("Total with tax: %.2f%n", totalWithTax);
        // Output: Total with tax: 648.00
    }
}

Intermediate Operations Summary

Operation Input Output Purpose
filter(Predicate) Stream<T> Stream<T> Keep elements matching condition
map(Function) Stream<T> Stream<R> Transform each element
flatMap(Function) Stream<T> Stream<R> Flatten nested streams
sorted() Stream<T> Stream<T> Sort elements
distinct() Stream<T> Stream<T> Remove duplicates
peek(Consumer) Stream<T> Stream<T> Debug / inspect
limit(long) Stream<T> Stream<T> Truncate to n elements
skip(long) Stream<T> Stream<T> Skip first n elements
mapToInt(Function) Stream<T> IntStream Convert to primitive int stream

4. Terminal Operations

Terminal operations are the final step of a stream pipeline. They trigger the execution of all intermediate operations and produce a result (a value, a collection, or a side effect). Once a terminal operation is called, the stream is consumed and cannot be reused.

4.1 forEach()

forEach(Consumer<T>) performs an action on each element. It is the stream equivalent of a for-each loop. Note that forEach does not guarantee order when used with parallel streams. Use forEachOrdered() if order matters.

import java.util.List;

public class ForEachExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie");

        // Simple forEach
        names.stream().forEach(System.out::println);
        // Output: Alice, Bob, Charlie

        // forEach with lambda
        names.stream().forEach(name -> System.out.println("Hello, " + name + "!"));
        // Output:
        // Hello, Alice!
        // Hello, Bob!
        // Hello, Charlie!

        // Warning: forEach on parallel stream -- order NOT guaranteed
        names.parallelStream().forEach(System.out::println);
        // Output: order may vary!

        // Use forEachOrdered to maintain encounter order
        names.parallelStream().forEachOrdered(System.out::println);
        // Output: Alice, Bob, Charlie (guaranteed order)
    }
}

4.2 collect()

collect() is the most versatile terminal operation. It transforms the stream elements into a collection, string, or other summary result using a Collector. The Collectors utility class provides dozens of ready-made collectors.

import java.util.*;
import java.util.stream.Collectors;

public class CollectExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "Alice", "David");

        // Collect to List
        List list = names.stream()
                                 .filter(n -> n.length() > 3)
                                 .collect(Collectors.toList());
        System.out.println("List: " + list);
        // Output: List: [Alice, Charlie, Alice, David]

        // Collect to Set (removes duplicates)
        Set set = names.stream()
                               .collect(Collectors.toSet());
        System.out.println("Set: " + set);
        // Output: Set: [Bob, Alice, Charlie, David]

        // Collect to unmodifiable List (Java 10+)
        List immutable = names.stream()
                                      .collect(Collectors.toUnmodifiableList());

        // Collect to Map (name -> length)
        Map nameToLength = names.stream()
            .distinct()
            .collect(Collectors.toMap(
                name -> name,           // key mapper
                String::length          // value mapper
            ));
        System.out.println("Map: " + nameToLength);
        // Output: Map: {Alice=5, Bob=3, Charlie=7, David=5}

        // Joining strings
        String joined = names.stream()
                             .distinct()
                             .collect(Collectors.joining(", "));
        System.out.println("Joined: " + joined);
        // Output: Joined: Alice, Bob, Charlie, David

        // Joining with prefix and suffix
        String formatted = names.stream()
                                .distinct()
                                .collect(Collectors.joining(", ", "[", "]"));
        System.out.println("Formatted: " + formatted);
        // Output: Formatted: [Alice, Bob, Charlie, David]
    }
}

4.3 reduce()

reduce() combines all elements of a stream into a single result by repeatedly applying a binary operation. It is the building block behind sum(), max(), and count() — those are all specialized reductions.

import java.util.List;
import java.util.Optional;

public class ReduceExample {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 4, 5);

        // With identity value: returns int (never empty)
        int sum = numbers.stream()
                         .reduce(0, Integer::sum);
        System.out.println("Sum: " + sum);
        // Output: Sum: 15

        // Without identity: returns Optional (might be empty)
        Optional product = numbers.stream()
                                           .reduce((a, b) -> a * b);
        product.ifPresent(p -> System.out.println("Product: " + p));
        // Output: Product: 120

        // Finding the maximum
        Optional max = numbers.stream()
                                       .reduce(Integer::max);
        System.out.println("Max: " + max.orElse(0));
        // Output: Max: 5

        // String concatenation with reduce
        List words = List.of("Java", "Stream", "API");
        String sentence = words.stream()
                               .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b);
        System.out.println(sentence);
        // Output: Java Stream API

        // How reduce works step-by-step for sum:
        // Step 1: identity(0) + 1 = 1
        // Step 2: 1 + 2 = 3
        // Step 3: 3 + 3 = 6
        // Step 4: 6 + 4 = 10
        // Step 5: 10 + 5 = 15
    }
}

4.4 count(), findFirst(), findAny()

import java.util.List;
import java.util.Optional;

public class CountFindExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "David", "Eve");

        // count() -- number of elements
        long count = names.stream()
                          .filter(n -> n.length() > 3)
                          .count();
        System.out.println("Names longer than 3: " + count);
        // Output: Names longer than 3: 3

        // findFirst() -- first element in encounter order, returns Optional
        Optional first = names.stream()
                                      .filter(n -> n.startsWith("C"))
                                      .findFirst();
        System.out.println("First C-name: " + first.orElse("none"));
        // Output: First C-name: Charlie

        // findAny() -- any matching element (useful in parallel streams)
        Optional any = names.parallelStream()
                                    .filter(n -> n.length() == 3)
                                    .findAny();
        System.out.println("Any 3-letter name: " + any.orElse("none"));
        // Output: Any 3-letter name: Bob (or Eve in parallel)
    }
}

4.5 anyMatch(), allMatch(), noneMatch()

These are short-circuiting terminal operations that return a boolean. They stop processing as soon as the answer is determined.

import java.util.List;

public class MatchExample {
    public static void main(String[] args) {
        List numbers = List.of(2, 4, 6, 8, 10);

        // anyMatch: is there at least one element > 7?
        boolean hasLarge = numbers.stream().anyMatch(n -> n > 7);
        System.out.println("Any > 7? " + hasLarge);
        // Output: Any > 7? true

        // allMatch: are ALL elements even?
        boolean allEven = numbers.stream().allMatch(n -> n % 2 == 0);
        System.out.println("All even? " + allEven);
        // Output: All even? true

        // noneMatch: are there NO negative numbers?
        boolean noNegatives = numbers.stream().noneMatch(n -> n < 0);
        System.out.println("No negatives? " + noNegatives);
        // Output: No negatives? true
    }
}

4.6 min(), max(), and toArray()

import java.util.Comparator;
import java.util.List;
import java.util.Optional;

public class MinMaxToArrayExample {
    public static void main(String[] args) {
        List names = List.of("Charlie", "Bob", "Alice", "David");

        // min -- requires a Comparator
        Optional shortest = names.stream()
                                         .min(Comparator.comparingInt(String::length));
        System.out.println("Shortest: " + shortest.orElse("none"));
        // Output: Shortest: Bob

        // max
        Optional longest = names.stream()
                                        .max(Comparator.comparingInt(String::length));
        System.out.println("Longest: " + longest.orElse("none"));
        // Output: Longest: Charlie

        // toArray -- convert stream to array
        String[] nameArray = names.stream()
                                  .filter(n -> n.length() > 3)
                                  .toArray(String[]::new);
        System.out.println("Array length: " + nameArray.length);
        // Output: Array length: 3
    }
}

Terminal Operations Summary

Operation Return Type Purpose
forEach(Consumer) void Perform action on each element
collect(Collector) R Accumulate into a collection or summary
reduce(identity, BinaryOp) T Combine all elements into one value
count() long Count elements
findFirst() Optional<T> First element (encounter order)
findAny() Optional<T> Any element (optimized for parallel)
anyMatch(Predicate) boolean At least one matches?
allMatch(Predicate) boolean All match?
noneMatch(Predicate) boolean None match?
min(Comparator) Optional<T> Minimum element
max(Comparator) Optional<T> Maximum element
toArray() Object[] or T[] Convert to array

5. Collectors In-Depth

The Collectors class is the powerhouse of the Stream API. Beyond basic toList() and toSet(), it provides sophisticated collectors for grouping, partitioning, summarizing, and more. Mastering these collectors will dramatically improve the expressiveness of your code.

5.1 groupingBy()

groupingBy() groups stream elements by a classification function, producing a Map<K, List<T>>. This is the stream equivalent of SQL's GROUP BY.

import java.util.*;
import java.util.stream.Collectors;

public class GroupingByExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "Anna", "Ben", "Chris");

        // Group by first letter
        Map> byFirstLetter = names.stream()
            .collect(Collectors.groupingBy(name -> name.charAt(0)));
        System.out.println(byFirstLetter);
        // Output: {A=[Alice, Anna], B=[Bob, Ben], C=[Charlie, Chris]}

        // Group by string length
        Map> byLength = names.stream()
            .collect(Collectors.groupingBy(String::length));
        System.out.println(byLength);
        // Output: {3=[Bob, Ben], 4=[Anna], 5=[Alice, Chris], 7=[Charlie]}

        // groupingBy with downstream collector: count per group
        Map countByLetter = names.stream()
            .collect(Collectors.groupingBy(
                name -> name.charAt(0),
                Collectors.counting()
            ));
        System.out.println(countByLetter);
        // Output: {A=2, B=2, C=2}

        // groupingBy with downstream collector: join names per group
        Map joinedByLetter = names.stream()
            .collect(Collectors.groupingBy(
                name -> name.charAt(0),
                Collectors.joining(", ")
            ));
        System.out.println(joinedByLetter);
        // Output: {A=Alice, Anna, B=Bob, Ben, C=Charlie, Chris}

        // Multi-level grouping: group by length, then by first letter
        Map>> multiLevel = names.stream()
            .collect(Collectors.groupingBy(
                String::length,
                Collectors.groupingBy(name -> name.charAt(0))
            ));
        System.out.println(multiLevel);
        // Output: {3={B=[Bob, Ben]}, 4={A=[Anna]}, 5={A=[Alice], C=[Chris]}, 7={C=[Charlie]}}
    }
}

5.2 partitioningBy()

partitioningBy() is a special case of groupingBy() that splits elements into exactly two groups based on a Predicate -- a true group and a false group. The result is always Map<Boolean, List<T>>.

import java.util.*;
import java.util.stream.Collectors;

public class PartitioningByExample {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Partition into even and odd
        Map> evenOdd = numbers.stream()
            .collect(Collectors.partitioningBy(n -> n % 2 == 0));
        System.out.println("Even: " + evenOdd.get(true));
        System.out.println("Odd: " + evenOdd.get(false));
        // Output:
        // Even: [2, 4, 6, 8, 10]
        // Odd: [1, 3, 5, 7, 9]

        // Partition with downstream collector: count each group
        Map counts = numbers.stream()
            .collect(Collectors.partitioningBy(
                n -> n > 5,
                Collectors.counting()
            ));
        System.out.println("Greater than 5: " + counts.get(true));
        System.out.println("5 or less: " + counts.get(false));
        // Output:
        // Greater than 5: 5
        // 5 or less: 5
    }
}

5.3 toMap() with Merge Function

When collecting to a Map, duplicate keys cause an IllegalStateException. You must provide a merge function to handle collisions.

import java.util.*;
import java.util.stream.Collectors;

public class ToMapMergeExample {
    public static void main(String[] args) {
        List words = List.of("hello", "world", "hello", "java", "world");

        // Problem: duplicate keys without merge function throws exception
        // Solution: provide a merge function
        Map wordCount = words.stream()
            .collect(Collectors.toMap(
                word -> word,              // key: the word itself
                word -> 1,                 // value: count of 1
                Integer::sum               // merge: add counts for duplicate keys
            ));
        System.out.println(wordCount);
        // Output: {hello=2, world=2, java=1}

        // Collecting to a specific Map implementation (LinkedHashMap preserves order)
        Map orderedCount = words.stream()
            .collect(Collectors.toMap(
                word -> word,
                word -> 1,
                Integer::sum,
                LinkedHashMap::new          // supplier for the Map type
            ));
        System.out.println(orderedCount);
        // Output: {hello=2, world=2, java=1}
    }
}

5.4 summarizingInt() and Other Summarizers

summarizingInt(), summarizingLong(), and summarizingDouble() collect comprehensive statistics in a single pass -- count, sum, min, max, and average.

import java.util.*;
import java.util.stream.Collectors;

public class SummarizingExample {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "David", "Eve");

        IntSummaryStatistics stats = names.stream()
            .collect(Collectors.summarizingInt(String::length));

        System.out.println("Count: " + stats.getCount());     // 5
        System.out.println("Sum: " + stats.getSum());         // 24
        System.out.println("Min: " + stats.getMin());         // 3
        System.out.println("Max: " + stats.getMax());         // 7
        System.out.printf("Average: %.1f%n", stats.getAverage()); // 4.8
    }
}

6. Parallel Streams

Parallel streams split the data into multiple chunks and process them simultaneously on different threads using the ForkJoinPool. This can significantly speed up processing of large datasets on multi-core machines -- but parallelism is not free and can hurt performance when used incorrectly.

Creating Parallel Streams

import java.util.List;
import java.util.stream.IntStream;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Method 1: parallelStream() from collection
        long sum1 = numbers.parallelStream()
                           .mapToLong(Integer::longValue)
                           .sum();

        // Method 2: .parallel() on an existing stream
        long sum2 = numbers.stream()
                           .parallel()
                           .mapToLong(Integer::longValue)
                           .sum();

        System.out.println("Sum1: " + sum1 + ", Sum2: " + sum2);
        // Output: Sum1: 55, Sum2: 55

        // Demonstrating parallel execution with thread names
        System.out.println("--- Sequential ---");
        IntStream.range(1, 5).forEach(i ->
            System.out.println(i + " on " + Thread.currentThread().getName()));

        System.out.println("--- Parallel ---");
        IntStream.range(1, 5).parallel().forEach(i ->
            System.out.println(i + " on " + Thread.currentThread().getName()));
        // Parallel output shows different thread names (ForkJoinPool.commonPool-worker-*)
    }
}

When to Use (and Not Use) Parallel Streams

Use Parallel When Avoid Parallel When
Large datasets (100,000+ elements) Small datasets (overhead > benefit)
CPU-intensive operations per element I/O-bound operations (network, file)
Operations are independent (no shared state) Operations depend on encounter order
Source is easy to split (ArrayList, arrays) Source is hard to split (LinkedList, Stream.iterate)
Stateless intermediate operations Stateful operations (sorted, distinct, limit)

Common mistake: Using parallel streams with shared mutable state. This leads to race conditions and incorrect results.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ParallelStreamDanger {
    public static void main(String[] args) {
        // WRONG: modifying a shared list from a parallel stream
        List unsafeList = new ArrayList<>();
        IntStream.range(0, 1000)
                 .parallel()
                 .forEach(unsafeList::add);  // Race condition!
        System.out.println("Unsafe size: " + unsafeList.size());
        // Output: might be less than 1000 or throw ArrayIndexOutOfBoundsException!

        // RIGHT: use collect() instead
        List safeList = IntStream.range(0, 1000)
                                          .parallel()
                                          .boxed()
                                          .collect(Collectors.toList());
        System.out.println("Safe size: " + safeList.size());
        // Output: Safe size: 1000
    }
}

7. Optional with Streams

Many terminal stream operations return an Optional -- a container that may or may not hold a value. This forces you to handle the "no result" case explicitly, preventing NullPointerException.

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

public class OptionalWithStreams {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie");

        // findFirst returns Optional
        Optional first = names.stream()
                                      .filter(n -> n.startsWith("Z"))
                                      .findFirst();

        // Handle the Optional
        String result = first.orElse("No match found");
        System.out.println(result);
        // Output: No match found

        // ifPresent -- only execute if a value exists
        names.stream()
             .filter(n -> n.startsWith("C"))
             .findFirst()
             .ifPresent(name -> System.out.println("Found: " + name));
        // Output: Found: Charlie

        // map on Optional -- transform the value if present
        Optional length = names.stream()
            .filter(n -> n.startsWith("A"))
            .findFirst()
            .map(String::length);
        System.out.println("Length: " + length.orElse(0));
        // Output: Length: 5

        // orElseThrow -- throw exception if empty
        // names.stream().filter(n -> n.startsWith("Z")).findFirst()
        //       .orElseThrow(() -> new IllegalArgumentException("No Z names"));

        // Java 9+: Optional.stream() -- converts Optional to a 0-or-1 element stream
        // Useful for flatMapping a stream of Optionals
        List> optionals = List.of(
            Optional.of("Hello"),
            Optional.empty(),
            Optional.of("World")
        );
        List values = optionals.stream()
                                       .flatMap(Optional::stream)
                                       .toList();
        System.out.println(values);
        // Output: [Hello, World]
    }
}

8. Primitive Streams

Java provides three specialized stream types for primitives: IntStream, LongStream, and DoubleStream. These avoid the overhead of autoboxing (converting int to Integer and back) and provide specialized methods like sum(), average(), and summaryStatistics().

import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.stream.IntStream;
import java.util.stream.DoubleStream;
import java.util.List;

public class PrimitiveStreamExample {
    public static void main(String[] args) {
        // IntStream creation
        IntStream range = IntStream.rangeClosed(1, 10);

        // sum, average, min, max
        int sum = IntStream.rangeClosed(1, 10).sum();
        System.out.println("Sum 1-10: " + sum);
        // Output: Sum 1-10: 55

        OptionalDouble avg = IntStream.of(85, 90, 78, 92, 88).average();
        System.out.println("Average: " + avg.orElse(0));
        // Output: Average: 86.6

        OptionalInt max = IntStream.of(85, 90, 78, 92, 88).max();
        System.out.println("Max: " + max.orElse(0));
        // Output: Max: 92

        // summaryStatistics() -- all stats in one pass
        var stats = IntStream.of(85, 90, 78, 92, 88).summaryStatistics();
        System.out.println("Count: " + stats.getCount());
        System.out.println("Sum: " + stats.getSum());
        System.out.println("Min: " + stats.getMin());
        System.out.println("Max: " + stats.getMax());
        System.out.printf("Avg: %.1f%n", stats.getAverage());

        // boxed() -- convert IntStream to Stream
        List boxedList = IntStream.rangeClosed(1, 5)
                                           .boxed()
                                           .toList();
        System.out.println("Boxed: " + boxedList);
        // Output: Boxed: [1, 2, 3, 4, 5]

        // mapToObj -- convert each int to an object
        List labels = IntStream.rangeClosed(1, 3)
                                       .mapToObj(i -> "Item " + i)
                                       .toList();
        System.out.println("Labels: " + labels);
        // Output: Labels: [Item 1, Item 2, Item 3]

        // Converting between Stream and primitive streams
        List names = List.of("Alice", "Bob", "Charlie");
        IntStream lengths = names.stream().mapToInt(String::length);
        System.out.println("Total chars: " + lengths.sum());
        // Output: Total chars: 15
    }
}

9. Common Patterns

This section demonstrates practical, real-world patterns you will use repeatedly in production code. These patterns solve common data-processing problems elegantly with streams.

9.1 Filtering and Collecting

import java.util.*;
import java.util.stream.Collectors;

public class CommonPatterns {
    record Product(String name, String category, double price) {}

    public static void main(String[] args) {
        List products = List.of(
            new Product("Laptop", "Electronics", 999.99),
            new Product("Headphones", "Electronics", 79.99),
            new Product("Coffee Maker", "Kitchen", 49.99),
            new Product("Blender", "Kitchen", 39.99),
            new Product("Monitor", "Electronics", 349.99),
            new Product("Toaster", "Kitchen", 29.99)
        );

        // Filter by category and sort by price
        List electronics = products.stream()
            .filter(p -> p.category().equals("Electronics"))
            .sorted(Comparator.comparingDouble(Product::price))
            .collect(Collectors.toList());
        electronics.forEach(p -> System.out.println(p.name() + " $" + p.price()));
        // Output:
        // Headphones $79.99
        // Monitor $349.99
        // Laptop $999.99

        // Find the top 2 most expensive products
        List topTwo = products.stream()
            .sorted(Comparator.comparingDouble(Product::price).reversed())
            .limit(2)
            .map(Product::name)
            .collect(Collectors.toList());
        System.out.println("Top 2: " + topTwo);
        // Output: Top 2: [Laptop, Monitor]

        // Group by category and calculate average price per category
        Map avgByCategory = products.stream()
            .collect(Collectors.groupingBy(
                Product::category,
                Collectors.averagingDouble(Product::price)
            ));
        avgByCategory.forEach((cat, avg) ->
            System.out.printf("%s avg: $%.2f%n", cat, avg));
        // Output:
        // Electronics avg: $476.66
        // Kitchen avg: $39.99

        // Create a comma-separated string of product names
        String productList = products.stream()
            .map(Product::name)
            .collect(Collectors.joining(", "));
        System.out.println("Products: " + productList);
        // Output: Products: Laptop, Headphones, Coffee Maker, Blender, Monitor, Toaster

        // Convert to a Map: name -> price
        Map priceMap = products.stream()
            .collect(Collectors.toMap(Product::name, Product::price));
        System.out.println("Laptop price: $" + priceMap.get("Laptop"));
        // Output: Laptop price: $999.99
    }
}

9.2 Flattening Nested Collections

import java.util.*;
import java.util.stream.Collectors;

public class FlatteningPattern {
    record Student(String name, List courses) {}

    public static void main(String[] args) {
        List students = List.of(
            new Student("Alice", List.of("Math", "Physics", "CS")),
            new Student("Bob", List.of("CS", "English", "Math")),
            new Student("Charlie", List.of("Biology", "Chemistry"))
        );

        // Get all unique courses offered
        Set allCourses = students.stream()
            .flatMap(s -> s.courses().stream())
            .collect(Collectors.toSet());
        System.out.println("All courses: " + allCourses);
        // Output: All courses: [Biology, CS, Chemistry, English, Math, Physics]

        // Find students taking "CS"
        List csStudents = students.stream()
            .filter(s -> s.courses().contains("CS"))
            .map(Student::name)
            .collect(Collectors.toList());
        System.out.println("CS students: " + csStudents);
        // Output: CS students: [Alice, Bob]
    }
}

10. Stream vs Loop

Streams are not always better than loops, and loops are not always better than streams. Understanding when to use each is a sign of a mature Java developer.

Criteria Stream Traditional Loop
Readability Excellent for data transformations (filter, map, collect) Better for simple iterations with side effects
Debugging Harder -- stack traces are less clear, peek() helps Easier -- set breakpoints, inspect variables
Performance Slight overhead for small datasets; parallel() helps with large Generally faster for simple operations on small data
Mutability Encourages immutability (functional style) Naturally works with mutable state
Short-circuiting Built-in (findFirst, anyMatch, limit) Manual (break, return)
Parallelism Trivial -- just call parallel() Complex -- manual thread management
State management Stateless operations preferred Stateful iteration is natural

The Same Problem: Both Ways

import java.util.*;
import java.util.stream.Collectors;

public class StreamVsLoop {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie", "David", "Eve", "Frank");

        // Task: Get uppercase names that are longer than 3 characters

        // --- Loop approach ---
        List resultLoop = new ArrayList<>();
        for (String name : names) {
            if (name.length() > 3) {
                resultLoop.add(name.toUpperCase());
            }
        }
        System.out.println("Loop: " + resultLoop);

        // --- Stream approach ---
        List resultStream = names.stream()
            .filter(n -> n.length() > 3)
            .map(String::toUpperCase)
            .collect(Collectors.toList());
        System.out.println("Stream: " + resultStream);

        // Both output: [ALICE, CHARLIE, DAVID, FRANK]
        // Stream is more readable here -- the intent is clear at a glance
    }
}

Rule of thumb: Use streams for data transformation pipelines (filter, map, collect, group). Use loops when you need to maintain complex local state, perform multiple related side effects, or when the logic is inherently imperative (like building a graph or managing indices).

11. Common Mistakes

These are mistakes that even experienced developers make when working with the Stream API. Understanding them will save you hours of debugging.

Mistake 1: Reusing a Stream

A stream can only be consumed once. Attempting to reuse it throws an IllegalStateException.

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

public class ReuseStreamMistake {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie");
        Stream stream = names.stream().filter(n -> n.length() > 3);

        // First use -- works fine
        long count = stream.count();
        System.out.println("Count: " + count);

        // Second use -- THROWS IllegalStateException!
        // stream.forEach(System.out::println);
        // java.lang.IllegalStateException: stream has already been operated upon or closed

        // Fix: create a new stream each time
        long count2 = names.stream().filter(n -> n.length() > 3).count();
        names.stream().filter(n -> n.length() > 3).forEach(System.out::println);
    }
}

Mistake 2: Side Effects in Intermediate Operations

Intermediate operations like map() and filter() should be stateless and free of side effects. Modifying external state from these operations leads to unpredictable behavior, especially with parallel streams.

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

public class SideEffectMistake {
    public static void main(String[] args) {
        List names = List.of("Alice", "Bob", "Charlie");

        // WRONG: modifying external state inside map()
        List sideEffectList = new ArrayList<>();
        names.stream()
             .map(n -> {
                 sideEffectList.add(n);  // Side effect! Don't do this.
                 return n.toUpperCase();
             })
             .collect(Collectors.toList());

        // RIGHT: use collect() to gather results
        List upper = names.stream()
                                  .map(String::toUpperCase)
                                  .collect(Collectors.toList());
    }
}

Mistake 3: Infinite Streams Without limit()

import java.util.stream.Stream;

public class InfiniteStreamMistake {
    public static void main(String[] args) {
        // WRONG: this runs forever and causes OutOfMemoryError
        // Stream.generate(Math::random).forEach(System.out::println);

        // RIGHT: always use limit() with generate() or iterate()
        Stream.generate(Math::random)
              .limit(5)
              .forEach(n -> System.out.printf("%.2f%n", n));

        // Or use the Java 9+ iterate with a predicate
        Stream.iterate(1, n -> n <= 100, n -> n * 2)
              .forEach(System.out::println);
    }
}

Mistake 4: Modifying the Source During Streaming

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

public class ModifySourceMistake {
    public static void main(String[] args) {
        List names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));

        // WRONG: modifying the source while streaming -- ConcurrentModificationException!
        // names.stream()
        //      .filter(n -> n.startsWith("A"))
        //      .forEach(n -> names.remove(n));

        // RIGHT: collect results, then modify
        List toRemove = names.stream()
            .filter(n -> n.startsWith("A"))
            .collect(Collectors.toList());
        names.removeAll(toRemove);
        System.out.println(names);
        // Output: [Bob, Charlie]

        // Or use removeIf() which is simpler
        // names.removeIf(n -> n.startsWith("A"));
    }
}

Mistake 5: Performance Traps

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

public class PerformanceTrapMistake {
    public static void main(String[] args) {
        List numbers = List.of(1, 2, 3, 4, 5);

        // SLOW: unnecessary boxing -- Stream instead of IntStream
        int sum1 = numbers.stream()
                          .map(n -> n * 2)       // boxes/unboxes Integer repeatedly
                          .reduce(0, Integer::sum);

        // FAST: use primitive stream
        int sum2 = numbers.stream()
                          .mapToInt(n -> n * 2)  // works with primitive int
                          .sum();

        // WASTEFUL: sorting the entire stream just to find the max
        // numbers.stream().sorted(Comparator.reverseOrder()).findFirst();

        // EFFICIENT: use max() directly
        // numbers.stream().max(Comparator.naturalOrder());

        System.out.println("Sum: " + sum2);
        // Output: Sum: 30
    }
}

Common Mistakes Summary

Mistake Symptom Fix
Reusing a consumed stream IllegalStateException Create a new stream each time
Side effects in map/filter Unpredictable results in parallel Use collect() for results, keep lambdas pure
Infinite stream without limit Program hangs or OutOfMemoryError Always use limit() with generate()/iterate()
Modifying source during stream ConcurrentModificationException Collect first, then modify; or use removeIf()
Unnecessary boxing Poor performance Use mapToInt()/mapToLong()/mapToDouble()
Sorting just to get min/max O(n log n) instead of O(n) Use min()/max() directly

12. Best Practices

Following these best practices will help you write stream code that is clean, efficient, and maintainable.

1. Keep Operations Simple

Each lambda in a stream pipeline should do one thing. If your lambda is more than 2-3 lines, extract it into a named method.

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

public class BestPractices {

    // POOR: complex inline lambda
    // list.stream().filter(e -> e.getAge() > 18 && e.getSalary() > 50000
    //     && e.getDepartment().equals("Engineering")).collect(Collectors.toList());

    // BETTER: extract to a method
    static boolean isSeniorEngineer(Employee e) {
        return e.age > 18
            && e.salary > 50000
            && e.department.equals("Engineering");
    }

    record Employee(String name, int age, double salary, String department) {}

    public static void main(String[] args) {
        List employees = List.of(
            new Employee("Alice", 30, 85000, "Engineering"),
            new Employee("Bob", 25, 45000, "Marketing")
        );

        List seniors = employees.stream()
            .filter(BestPractices::isSeniorEngineer)
            .collect(Collectors.toList());
    }
}

2. Use Method References When Possible

Method references are more concise and communicate intent better than equivalent lambdas.

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

public class MethodReferencePractice {
    public static void main(String[] args) {
        List names = List.of("alice", "bob", "charlie");

        // Lambda (works but verbose)
        List upper1 = names.stream()
            .map(s -> s.toUpperCase())
            .collect(Collectors.toList());

        // Method reference (cleaner)
        List upper2 = names.stream()
            .map(String::toUpperCase)
            .collect(Collectors.toList());

        // More examples:
        // s -> System.out.println(s)  ->  System.out::println
        // s -> s.length()             ->  String::length
        // s -> Integer.parseInt(s)    ->  Integer::parseInt
        // () -> new ArrayList<>()     ->  ArrayList::new
    }
}

3. Format Multi-Line Streams for Readability

Each operation in a stream pipeline should be on its own line, with consistent indentation. This makes the pipeline easy to read and modify.

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

public class FormattingPractice {
    record Employee(String name, String dept, double salary) {}

    public static void main(String[] args) {
        List employees = List.of(
            new Employee("Alice", "Engineering", 95000),
            new Employee("Bob", "Marketing", 65000),
            new Employee("Charlie", "Engineering", 85000)
        );

        // POOR: all on one line
        // List result = employees.stream().filter(e -> e.dept().equals("Engineering")).map(Employee::name).sorted().collect(Collectors.toList());

        // GOOD: one operation per line, aligned at the dot
        List result = employees.stream()
            .filter(e -> e.dept().equals("Engineering"))
            .map(Employee::name)
            .sorted()
            .collect(Collectors.toList());
        System.out.println(result);
        // Output: [Alice, Charlie]
    }
}

Best Practices Summary

# Practice Why
1 Keep lambdas short; extract complex logic to named methods Readability, testability, reusability
2 Use method references (String::toUpperCase) Cleaner, more concise
3 Avoid side effects in intermediate operations Predictable behavior, safe parallelism
4 Use primitive streams for numbers (mapToInt) Avoids autoboxing overhead
5 Do not over-use streams; simple loops are fine Not everything benefits from streams
6 Format one operation per line Readability, easy to add/remove steps
7 Prefer collect() over forEach() + mutation Thread-safe, functional style
8 Use Optional results properly (orElse, ifPresent) Avoid NullPointerException
9 Use parallel streams only when justified Parallelism has overhead; profile first
10 Favor toList() (Java 16+) over collect(Collectors.toList()) Shorter and returns unmodifiable list

13. Complete Practical Example: Employee Analytics System

Let us tie everything together with a real-world example. We will build an Employee analytics system that uses streams to answer common business questions: filtering by department, calculating salary statistics, grouping, partitioning, finding top performers, and generating a report.

import java.util.*;
import java.util.stream.Collectors;

public class EmployeeAnalytics {

    // --- Employee class ---
    static class Employee {
        private final String name;
        private final String department;
        private final double salary;
        private final int yearsOfExperience;

        public Employee(String name, String department, double salary, int yearsOfExperience) {
            this.name = name;
            this.department = department;
            this.salary = salary;
            this.yearsOfExperience = yearsOfExperience;
        }

        public String getName()              { return name; }
        public String getDepartment()        { return department; }
        public double getSalary()            { return salary; }
        public int getYearsOfExperience()    { return yearsOfExperience; }
        public boolean isSenior()            { return yearsOfExperience >= 5; }

        @Override
        public String toString() {
            return String.format("%-12s | %-12s | $%,10.2f | %2d yrs",
                name, department, salary, yearsOfExperience);
        }
    }

    public static void main(String[] args) {
        // --- Sample data ---
        List employees = List.of(
            new Employee("Alice",    "Engineering",  120000, 8),
            new Employee("Bob",      "Engineering",   95000, 3),
            new Employee("Charlie",  "Engineering",  110000, 6),
            new Employee("Diana",    "Marketing",     85000, 10),
            new Employee("Eve",      "Marketing",     72000, 2),
            new Employee("Frank",    "Sales",         78000, 5),
            new Employee("Grace",    "Sales",         82000, 7),
            new Employee("Henry",    "HR",            68000, 4),
            new Employee("Ivy",      "HR",            71000, 6),
            new Employee("Jack",     "Engineering",  135000, 12)
        );

        System.out.println("=== EMPLOYEE ANALYTICS REPORT ===\n");

        // -------------------------------------------------------
        // 1. FILTER: Engineers earning above $100K
        // -------------------------------------------------------
        System.out.println("--- 1. High-Earning Engineers (>$100K) ---");
        List highEarningEngineers = employees.stream()
            .filter(e -> e.getDepartment().equals("Engineering"))
            .filter(e -> e.getSalary() > 100000)
            .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
            .collect(Collectors.toList());

        highEarningEngineers.forEach(System.out::println);
        // Output:
        // Jack         | Engineering  | $135,000.00 | 12 yrs
        // Alice        | Engineering  | $120,000.00 |  8 yrs
        // Charlie      | Engineering  | $110,000.00 |  6 yrs

        // -------------------------------------------------------
        // 2. MAP + COLLECT: Average salary per department
        // -------------------------------------------------------
        System.out.println("\n--- 2. Average Salary by Department ---");
        Map avgSalaryByDept = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.averagingDouble(Employee::getSalary)
            ));

        avgSalaryByDept.entrySet().stream()
            .sorted(Map.Entry.comparingByValue().reversed())
            .forEach(e -> System.out.printf("  %-12s $%,.2f%n", e.getKey(), e.getValue()));
        // Output:
        //   Engineering  $115,000.00
        //   Sales        $80,000.00
        //   Marketing    $78,500.00
        //   HR           $69,500.00

        // -------------------------------------------------------
        // 3. GROUPING: Employees grouped by department
        // -------------------------------------------------------
        System.out.println("\n--- 3. Employees by Department ---");
        Map> namesByDept = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.mapping(Employee::getName, Collectors.toList())
            ));

        namesByDept.forEach((dept, names) ->
            System.out.printf("  %-12s %s%n", dept, names));
        // Output:
        //   Engineering  [Alice, Bob, Charlie, Jack]
        //   Marketing    [Diana, Eve]
        //   Sales        [Frank, Grace]
        //   HR           [Henry, Ivy]

        // -------------------------------------------------------
        // 4. REDUCE: Highest-paid employee
        // -------------------------------------------------------
        System.out.println("\n--- 4. Highest Paid Employee ---");
        employees.stream()
            .max(Comparator.comparingDouble(Employee::getSalary))
            .ifPresent(e -> System.out.println("  " + e));
        // Output:
        //   Jack         | Engineering  | $135,000.00 | 12 yrs

        // -------------------------------------------------------
        // 5. PARTITIONING: Senior vs Junior (5+ years = senior)
        // -------------------------------------------------------
        System.out.println("\n--- 5. Senior vs Junior ---");
        Map> seniorPartition = employees.stream()
            .collect(Collectors.partitioningBy(Employee::isSenior));

        System.out.println("  Senior (" + seniorPartition.get(true).size() + "):");
        seniorPartition.get(true).forEach(e -> System.out.println("    " + e.getName()));

        System.out.println("  Junior (" + seniorPartition.get(false).size() + "):");
        seniorPartition.get(false).forEach(e -> System.out.println("    " + e.getName()));
        // Output:
        //   Senior (6):
        //     Alice, Charlie, Diana, Frank, Grace, Ivy
        //   Junior (4):
        //     Bob, Eve, Henry, Jack... wait, Jack has 12 years!
        //     Actually: Bob, Eve, Henry

        // -------------------------------------------------------
        // 6. STATISTICS: Salary summary
        // -------------------------------------------------------
        System.out.println("\n--- 6. Salary Statistics ---");
        DoubleSummaryStatistics salaryStats = employees.stream()
            .mapToDouble(Employee::getSalary)
            .summaryStatistics();

        System.out.printf("  Count:   %d%n", salaryStats.getCount());
        System.out.printf("  Total:   $%,.2f%n", salaryStats.getSum());
        System.out.printf("  Min:     $%,.2f%n", salaryStats.getMin());
        System.out.printf("  Max:     $%,.2f%n", salaryStats.getMax());
        System.out.printf("  Average: $%,.2f%n", salaryStats.getAverage());
        // Output:
        //   Count:   10
        //   Total:   $916,000.00
        //   Min:     $68,000.00
        //   Max:     $135,000.00
        //   Average: $91,600.00

        // -------------------------------------------------------
        // 7. TOP N: Top 3 highest salaries
        // -------------------------------------------------------
        System.out.println("\n--- 7. Top 3 Highest Salaries ---");
        employees.stream()
            .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
            .limit(3)
            .forEach(e -> System.out.println("  " + e));
        // Output:
        //   Jack         | Engineering  | $135,000.00 | 12 yrs
        //   Alice        | Engineering  | $120,000.00 |  8 yrs
        //   Charlie      | Engineering  | $110,000.00 |  6 yrs

        // -------------------------------------------------------
        // 8. STRING JOINING: Department roster
        // -------------------------------------------------------
        System.out.println("\n--- 8. Department Roster ---");
        Map rosters = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.mapping(
                    Employee::getName,
                    Collectors.joining(", ")
                )
            ));
        rosters.forEach((dept, roster) ->
            System.out.printf("  %-12s %s%n", dept, roster));
        // Output:
        //   Engineering  Alice, Bob, Charlie, Jack
        //   Marketing    Diana, Eve
        //   Sales        Frank, Grace
        //   HR           Henry, Ivy

        // -------------------------------------------------------
        // 9. COMPLEX: Department with highest average salary
        // -------------------------------------------------------
        System.out.println("\n--- 9. Highest-Paying Department ---");
        employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.averagingDouble(Employee::getSalary)
            ))
            .entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .ifPresent(e -> System.out.printf("  %s with avg $%,.2f%n", e.getKey(), e.getValue()));
        // Output:
        //   Engineering with avg $115,000.00

        // -------------------------------------------------------
        // 10. BOOLEAN CHECKS: Quick analytics
        // -------------------------------------------------------
        System.out.println("\n--- 10. Quick Checks ---");
        boolean anyOver130K = employees.stream()
            .anyMatch(e -> e.getSalary() > 130000);
        System.out.println("  Anyone earning >$130K? " + anyOver130K);  // true

        boolean allAbove50K = employees.stream()
            .allMatch(e -> e.getSalary() > 50000);
        System.out.println("  All earning >$50K? " + allAbove50K);      // true

        long totalExperience = employees.stream()
            .mapToInt(Employee::getYearsOfExperience)
            .sum();
        System.out.println("  Total years of experience: " + totalExperience); // 63

        System.out.println("\n=== END OF REPORT ===");
    }
}

Concepts Demonstrated in the Practical Example

# Concept Where Used
1 filter() Section 1 -- filtering by department and salary
2 sorted() with Comparator Sections 1, 2, 7 -- sorting by salary
3 collect(Collectors.toList()) Sections 1, 3 -- gathering results
4 groupingBy() Sections 2, 3, 8, 9 -- grouping by department
5 averagingDouble() Sections 2, 9 -- average salary
6 mapping() downstream Sections 3, 8 -- extracting names within groups
7 max() with Comparator Section 4 -- highest-paid employee
8 partitioningBy() Section 5 -- senior vs junior split
9 summaryStatistics() Section 6 -- comprehensive salary stats
10 limit() Section 7 -- top 3
11 Collectors.joining() Section 8 -- comma-separated roster
12 Chained stream operations Section 9 -- collect then stream the result
13 anyMatch(), allMatch() Section 10 -- boolean checks
14 mapToInt() + sum() Section 10 -- total experience

Quick Reference

Category Operation Type Returns
Create collection.stream() Source Stream<T>
Create Stream.of(a, b, c) Source Stream<T>
Create IntStream.rangeClosed(1, 10) Source IntStream
Transform filter(Predicate) Intermediate Stream<T>
Transform map(Function) Intermediate Stream<R>
Transform flatMap(Function) Intermediate Stream<R>
Transform sorted() Intermediate Stream<T>
Transform distinct() Intermediate Stream<T>
Transform limit(n) / skip(n) Intermediate Stream<T>
Collect collect(Collectors.toList()) Terminal List<T>
Collect collect(Collectors.toSet()) Terminal Set<T>
Collect collect(Collectors.toMap(...)) Terminal Map<K,V>
Collect collect(Collectors.groupingBy(...)) Terminal Map<K,List<T>>
Collect collect(Collectors.joining(...)) Terminal String
Reduce reduce(identity, BinaryOp) Terminal T
Reduce count() Terminal long
Reduce min(Comparator) / max(Comparator) Terminal Optional<T>
Search findFirst() / findAny() Terminal Optional<T>
Match anyMatch / allMatch / noneMatch Terminal boolean
Action forEach(Consumer) Terminal void



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *