Collectors class

1. What is the Collectors Class?

When you use the Stream API, the collect() terminal operation is how you transform a stream back into a concrete data structure — a List, a Set, a Map, a String, or even a custom container. But collect() does not know how to build these structures on its own. That is where Collectors come in.

The java.util.stream.Collectors class is a utility class (introduced in Java 8) that provides dozens of factory methods for creating Collector objects. Each Collector defines three things:

  • Supplier — how to create the result container (e.g., new ArrayList<>())
  • Accumulator — how to add an element to the container (e.g., list.add(element))
  • Combiner — how to merge two containers (needed for parallel streams)

You rarely need to think about these three parts because the pre-built Collectors handle it. Here is the big picture:

Category Collectors
Collection builders toList(), toSet(), toMap(), toCollection(), toUnmodifiableList()
String builders joining()
Grouping groupingBy(), partitioningBy()
Aggregation counting(), summingInt(), averagingDouble(), summarizingInt()
Reduction reducing(), mapping(), flatMapping(), filtering()
Min/Max minBy(), maxBy()
Custom Collector.of()
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

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

        // Without Collectors -- manual collect (verbose)
        List manual = names.stream()
            .filter(n -> n.length() > 3)
            .collect(
                () -> new java.util.ArrayList<>(),  // supplier
                (list, item) -> list.add(item),     // accumulator
                (list1, list2) -> list1.addAll(list2) // combiner
            );

        // With Collectors -- one method call
        List easy = names.stream()
            .filter(n -> n.length() > 3)
            .collect(Collectors.toList());

        System.out.println("Manual: " + manual);
        System.out.println("Easy:   " + easy);
        // Both output: [Alice, Charlie, Alice, David]
    }
}

2. Basic Collectors: toList(), toSet(), toCollection()

These are the workhorses of the Collectors class — used in the vast majority of stream pipelines.

Collector Returns Duplicates Order
toList() List<T> Allowed Preserved
toSet() Set<T> Removed Not guaranteed
toUnmodifiableList() (Java 10+) List<T> Allowed Preserved, immutable
toUnmodifiableSet() (Java 10+) Set<T> Removed Not guaranteed, immutable
toCollection(Supplier) Any Collection Depends on type Depends on type
import java.util.*;
import java.util.stream.Collectors;

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

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

        // toSet() -- HashSet (no duplicates)
        Set set = names.stream()
            .collect(Collectors.toSet());
        System.out.println("toSet: " + set);
        // Output: toSet: [Alice, Bob, Charlie, David] (order may vary)

        // toUnmodifiableList() -- Java 10+ (immutable)
        List immutable = names.stream()
            .filter(n -> n.length() > 3)
            .collect(Collectors.toUnmodifiableList());
        System.out.println("toUnmodifiableList: " + immutable);
        // immutable.add("Fail"); // throws UnsupportedOperationException

        // toUnmodifiableSet() -- Java 10+ (immutable, no duplicates)
        Set immutableSet = names.stream()
            .collect(Collectors.toUnmodifiableSet());
        System.out.println("toUnmodifiableSet: " + immutableSet);

        // toCollection() -- specify the exact collection type
        TreeSet treeSet = names.stream()
            .collect(Collectors.toCollection(TreeSet::new));
        System.out.println("TreeSet (sorted): " + treeSet);
        // Output: TreeSet (sorted): [Alice, Bob, Charlie, David]

        LinkedList linkedList = names.stream()
            .collect(Collectors.toCollection(LinkedList::new));
        System.out.println("LinkedList: " + linkedList);

        // Java 16+: Stream.toList() shorthand (returns unmodifiable list)
        List java16List = names.stream()
            .filter(n -> n.startsWith("A"))
            .toList();
        System.out.println("Stream.toList(): " + java16List);
        // Output: Stream.toList(): [Alice, Alice]
    }
}

3. toMap()

Collectors.toMap() builds a Map from stream elements. It requires two functions: one to extract the key, and one to extract the value. It has three overloaded forms to handle duplicate keys and specify the Map implementation.

3.1 Basic toMap

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

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

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

        // Map from index to value
        List colors = List.of("Red", "Green", "Blue");
        Map indexed = new HashMap<>();
        for (int i = 0; i < colors.size(); i++) {
            indexed.put(i, colors.get(i));
        }
        // Or with streams using an AtomicInteger
        java.util.concurrent.atomic.AtomicInteger counter = new java.util.concurrent.atomic.AtomicInteger(0);
        Map indexedStream = colors.stream()
            .collect(Collectors.toMap(
                color -> counter.getAndIncrement(),
                color -> color
            ));
        System.out.println("Indexed: " + indexedStream);
        // Output: Indexed: {0=Red, 1=Green, 2=Blue}
    }
}

3.2 Handling Duplicate Keys (Merge Function)

If two elements produce the same key, toMap() throws IllegalStateException by default. To handle duplicates, provide a merge function as the third argument.

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

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

        // Group by first letter -- FAILS if duplicate keys
        // Map bad = names.stream()
        //     .collect(Collectors.toMap(n -> n.charAt(0), n -> n));
        // IllegalStateException: Duplicate key A (attempted merging Alice and Anna)

        // Handle duplicates: keep the first value
        Map keepFirst = names.stream()
            .collect(Collectors.toMap(
                n -> n.charAt(0),     // key
                n -> n,              // value
                (existing, replacement) -> existing  // merge: keep first
            ));
        System.out.println("Keep first: " + keepFirst);
        // Output: Keep first: {A=Alice, B=Bob, C=Charlie}

        // Handle duplicates: keep the last value
        Map keepLast = names.stream()
            .collect(Collectors.toMap(
                n -> n.charAt(0),
                n -> n,
                (existing, replacement) -> replacement  // merge: keep last
            ));
        System.out.println("Keep last: " + keepLast);
        // Output: Keep last: {A=Anna, B=Brian, C=Charlie}

        // Handle duplicates: concatenate values
        Map concat = names.stream()
            .collect(Collectors.toMap(
                n -> n.charAt(0),
                n -> n,
                (a, b) -> a + ", " + b  // merge: join with comma
            ));
        System.out.println("Concatenated: " + concat);
        // Output: Concatenated: {A=Alice, Anna, B=Bob, Brian, C=Charlie}

        // Count occurrences using toMap
        List words = List.of("apple", "banana", "apple", "cherry", "banana", "apple");
        Map wordCount = words.stream()
            .collect(Collectors.toMap(
                w -> w,
                w -> 1,
                Integer::sum
            ));
        System.out.println("Word counts: " + wordCount);
        // Output: Word counts: {apple=3, banana=2, cherry=1}
    }
}

3.3 Controlling the Map Type

By default, toMap() returns a HashMap. The four-argument form lets you specify a different Map implementation, such as TreeMap (sorted) or LinkedHashMap (insertion order).

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

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

        // TreeMap -- keys sorted alphabetically
        TreeMap sorted = names.stream()
            .collect(Collectors.toMap(
                n -> n,
                String::length,
                (a, b) -> a,          // merge function (required for 4-arg form)
                TreeMap::new           // map factory
            ));
        System.out.println("TreeMap: " + sorted);
        // Output: TreeMap: {Alice=5, Bob=3, Charlie=7, David=5}

        // LinkedHashMap -- preserves insertion order
        LinkedHashMap ordered = names.stream()
            .collect(Collectors.toMap(
                n -> n,
                String::length,
                (a, b) -> a,
                LinkedHashMap::new
            ));
        System.out.println("LinkedHashMap: " + ordered);
        // Output: LinkedHashMap: {Charlie=7, Alice=5, Bob=3, David=5}
    }
}

4. joining()

Collectors.joining() concatenates CharSequence elements (Strings) into a single String. It has three overloaded forms.

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

public class JoiningCollector {
    public static void main(String[] args) {
        List languages = List.of("Java", "Python", "Go", "Rust", "C++");

        // joining() -- no separator
        String noSep = languages.stream().collect(Collectors.joining());
        System.out.println("No separator: " + noSep);
        // Output: No separator: JavaPythonGoRustC++

        // joining(delimiter) -- with separator
        String commaSep = languages.stream().collect(Collectors.joining(", "));
        System.out.println("Comma: " + commaSep);
        // Output: Comma: Java, Python, Go, Rust, C++

        // joining(delimiter, prefix, suffix)
        String formatted = languages.stream()
            .collect(Collectors.joining(" | ", "Languages: [", "]"));
        System.out.println(formatted);
        // Output: Languages: [Java | Python | Go | Rust | C++]

        // Practical: CSV row
        List fields = List.of("John", "Doe", "john@example.com", "555-1234");
        String csvRow = fields.stream().collect(Collectors.joining(","));
        System.out.println("CSV: " + csvRow);
        // Output: CSV: John,Doe,john@example.com,555-1234

        // Practical: SQL IN clause
        List ids = List.of(101, 205, 310, 422);
        String sql = ids.stream()
            .map(String::valueOf)
            .collect(Collectors.joining(", ", "SELECT * FROM users WHERE id IN (", ");"));
        System.out.println("SQL: " + sql);
        // Output: SQL: SELECT * FROM users WHERE id IN (101, 205, 310, 422);

        // Practical: HTML list
        List items = List.of("Home", "About", "Contact");
        String html = items.stream()
            .map(item -> "  
  • " + item + "
  • ") .collect(Collectors.joining("\n", "
      \n", "\n
    ")); System.out.println(html); // joining() on empty stream returns empty prefix+suffix String empty = List.of().stream() .collect(Collectors.joining(", ", "[", "]")); System.out.println("Empty: " + empty); // Output: Empty: [] } }

    5. groupingBy()

    Collectors.groupingBy() is the stream equivalent of SQL’s GROUP BY. It classifies elements by a key and groups them into a Map<K, List<T>>. With downstream collectors, you can perform aggregations within each group.

    5.1 Single-Level Grouping

    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    public class GroupingByBasic {
    
        record Employee(String name, String department, double salary) {}
    
        public static void main(String[] args) {
            List employees = List.of(
                new Employee("Alice", "Engineering", 95000),
                new Employee("Bob", "Engineering", 110000),
                new Employee("Charlie", "Marketing", 72000),
                new Employee("Diana", "Sales", 68000),
                new Employee("Eve", "Engineering", 125000),
                new Employee("Frank", "Marketing", 78000),
                new Employee("Grace", "Sales", 71000)
            );
    
            // Group by department
            Map> byDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::department));
    
            byDept.forEach((dept, emps) -> {
                System.out.println(dept + ":");
                emps.forEach(e -> System.out.println("  " + e.name()));
            });
            // Output:
            // Engineering:
            //   Alice
            //   Bob
            //   Eve
            // Marketing:
            //   Charlie
            //   Frank
            // Sales:
            //   Diana
            //   Grace
    
            // Group strings by length
            List words = List.of("cat", "dog", "fish", "bird", "ant", "fox", "bear");
            Map> byLength = words.stream()
                .collect(Collectors.groupingBy(String::length));
            System.out.println("By length: " + byLength);
            // Output: By length: {3=[cat, dog, ant, fox], 4=[fish, bird, bear]}
        }
    }

    5.2 groupingBy with Downstream Collectors

    The real power of groupingBy() comes from the second argument — a downstream collector that processes each group. Instead of getting List<T>, you can count, sum, average, or further transform each group.

    import java.util.*;
    import java.util.stream.Collectors;
    
    public class GroupingByDownstream {
    
        record Employee(String name, String department, double salary) {}
    
        public static void main(String[] args) {
            List employees = List.of(
                new Employee("Alice", "Engineering", 95000),
                new Employee("Bob", "Engineering", 110000),
                new Employee("Charlie", "Marketing", 72000),
                new Employee("Diana", "Sales", 68000),
                new Employee("Eve", "Engineering", 125000),
                new Employee("Frank", "Marketing", 78000),
                new Employee("Grace", "Sales", 71000)
            );
    
            // Count per group
            Map countByDept = employees.stream()
                .collect(Collectors.groupingBy(Employee::department, Collectors.counting()));
            System.out.println("Count: " + countByDept);
            // Output: Count: {Engineering=3, Marketing=2, Sales=2}
    
            // Average salary per department
            Map avgByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.averagingDouble(Employee::salary)
                ));
            System.out.println("Average salary: " + avgByDept);
    
            // Sum of salaries per department
            Map totalByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.summingDouble(Employee::salary)
                ));
            System.out.println("Total salary: " + totalByDept);
    
            // Max salary per department
            Map> topEarner = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.maxBy(Comparator.comparingDouble(Employee::salary))
                ));
            topEarner.forEach((dept, emp) ->
                emp.ifPresent(e -> System.out.println(dept + " top earner: " + e.name())));
    
            // Map to names only (mapping downstream)
            Map> namesByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.mapping(Employee::name, Collectors.toList())
                ));
            System.out.println("Names: " + namesByDept);
            // Output: Names: {Engineering=[Alice, Bob, Eve], Marketing=[Charlie, Frank], Sales=[Diana, Grace]}
    
            // Join names as a comma-separated string
            Map joinedNames = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.mapping(Employee::name, Collectors.joining(", "))
                ));
            System.out.println("Joined: " + joinedNames);
            // Output: Joined: {Engineering=Alice, Bob, Eve, Marketing=Charlie, Frank, Sales=Diana, Grace}
    
            // Collect names into a Set (no duplicates)
            Map> nameSetByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.mapping(Employee::name, Collectors.toSet())
                ));
            System.out.println("Name sets: " + nameSetByDept);
        }
    }

    5.3 Multi-Level Grouping

    You can nest groupingBy() calls to create multi-level groupings — similar to SQL’s GROUP BY col1, col2.

    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    public class MultiLevelGrouping {
    
        record Employee(String name, String department, String level, double salary) {}
    
        public static void main(String[] args) {
            List employees = List.of(
                new Employee("Alice", "Engineering", "Senior", 120000),
                new Employee("Bob", "Engineering", "Junior", 75000),
                new Employee("Charlie", "Engineering", "Senior", 115000),
                new Employee("Diana", "Marketing", "Junior", 60000),
                new Employee("Eve", "Marketing", "Senior", 85000),
                new Employee("Frank", "Sales", "Junior", 55000),
                new Employee("Grace", "Sales", "Senior", 80000),
                new Employee("Hannah", "Sales", "Junior", 58000)
            );
    
            // Two-level grouping: department -> level -> list of employees
            Map>> twoLevel = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.groupingBy(Employee::level)
                ));
    
            twoLevel.forEach((dept, levels) -> {
                System.out.println(dept + ":");
                levels.forEach((level, emps) -> {
                    System.out.println("  " + level + ":");
                    emps.forEach(e -> System.out.println("    " + e.name() + " - $" + e.salary()));
                });
            });
    
            // Two-level grouping with counting
            Map> headcount = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.groupingBy(Employee::level, Collectors.counting())
                ));
            System.out.println("\nHeadcount: " + headcount);
            // Output: Headcount: {Engineering={Junior=1, Senior=2}, Marketing={Junior=1, Senior=1}, Sales={Junior=2, Senior=1}}
    
            // Two-level grouping with average salary
            Map> avgSalary = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.groupingBy(
                        Employee::level,
                        Collectors.averagingDouble(Employee::salary)
                    )
                ));
            System.out.println("\nAvg salary: " + avgSalary);
        }
    }

    6. partitioningBy()

    Collectors.partitioningBy() is a special case of groupingBy() that splits elements into exactly two groups based on a predicate: true and false. The result is always a Map<Boolean, List<T>> with both keys present (even if one group is empty).

    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 counting
            Map evenOddCount = numbers.stream()
                .collect(Collectors.partitioningBy(
                    n -> n % 2 == 0,
                    Collectors.counting()
                ));
            System.out.println("Even count: " + evenOddCount.get(true));   // 5
            System.out.println("Odd count:  " + evenOddCount.get(false));  // 5
    
            // Partition students into pass/fail
            record Student(String name, int score) {}
    
            List students = List.of(
                new Student("Alice", 85), new Student("Bob", 42),
                new Student("Charlie", 91), new Student("Diana", 58),
                new Student("Eve", 73), new Student("Frank", 35)
            );
    
            int passingScore = 60;
            Map> passFail = students.stream()
                .collect(Collectors.partitioningBy(s -> s.score() >= passingScore));
    
            System.out.println("\nPassing:");
            passFail.get(true).forEach(s ->
                System.out.println("  " + s.name() + ": " + s.score()));
            System.out.println("Failing:");
            passFail.get(false).forEach(s ->
                System.out.println("  " + s.name() + ": " + s.score()));
    
            // Partition with mapping downstream
            Map> passFailNames = students.stream()
                .collect(Collectors.partitioningBy(
                    s -> s.score() >= passingScore,
                    Collectors.mapping(Student::name, Collectors.toList())
                ));
            System.out.println("Pass names: " + passFailNames.get(true));
            System.out.println("Fail names: " + passFailNames.get(false));
    
            // Empty partition -- both keys still present
            Map> allPass = List.of(100, 90, 80).stream()
                .collect(Collectors.partitioningBy(n -> n >= 60));
            System.out.println("True:  " + allPass.get(true));   // [100, 90, 80]
            System.out.println("False: " + allPass.get(false));   // [] (empty, but key exists)
        }
    }

    7. Counting and Statistics

    Collectors provides several methods for numeric aggregation. These are most commonly used as downstream collectors inside groupingBy().

    Collector Returns Description
    counting() Long Count of elements
    summingInt/Long/Double() Integer/Long/Double Sum of extracted values
    averagingInt/Long/Double() Double Average of extracted values
    summarizingInt/Long/Double() IntSummaryStatistics Count, sum, min, max, average in one pass
    minBy(Comparator) Optional<T> Minimum element
    maxBy(Comparator) Optional<T> Maximum element
    import java.util.*;
    import java.util.stream.Collectors;
    
    public class CountingStatsExample {
    
        record Product(String name, String category, double price, int quantity) {}
    
        public static void main(String[] args) {
            List products = List.of(
                new Product("Laptop", "Electronics", 999.99, 50),
                new Product("Phone", "Electronics", 699.99, 200),
                new Product("Tablet", "Electronics", 449.99, 100),
                new Product("Desk", "Furniture", 299.99, 30),
                new Product("Chair", "Furniture", 199.99, 80),
                new Product("Notebook", "Office", 4.99, 1000),
                new Product("Pen", "Office", 1.99, 5000)
            );
    
            // counting() -- total number of products
            long totalProducts = products.stream()
                .collect(Collectors.counting());
            System.out.println("Total products: " + totalProducts);
    
            // counting() as downstream -- products per category
            Map countPerCategory = products.stream()
                .collect(Collectors.groupingBy(Product::category, Collectors.counting()));
            System.out.println("Per category: " + countPerCategory);
            // Output: Per category: {Electronics=3, Furniture=2, Office=2}
    
            // summingDouble() -- total inventory value
            double totalValue = products.stream()
                .collect(Collectors.summingDouble(p -> p.price() * p.quantity()));
            System.out.printf("Total inventory value: $%,.2f%n", totalValue);
    
            // averagingDouble() -- average price
            double avgPrice = products.stream()
                .collect(Collectors.averagingDouble(Product::price));
            System.out.printf("Average price: $%,.2f%n", avgPrice);
    
            // summarizingDouble() -- all stats in one pass
            DoubleSummaryStatistics priceStats = products.stream()
                .collect(Collectors.summarizingDouble(Product::price));
            System.out.println("\nPrice statistics:");
            System.out.printf("  Count: %d%n", priceStats.getCount());
            System.out.printf("  Sum:   $%,.2f%n", priceStats.getSum());
            System.out.printf("  Min:   $%,.2f%n", priceStats.getMin());
            System.out.printf("  Max:   $%,.2f%n", priceStats.getMax());
            System.out.printf("  Avg:   $%,.2f%n", priceStats.getAverage());
    
            // summarizingDouble per category
            Map statsByCategory = products.stream()
                .collect(Collectors.groupingBy(
                    Product::category,
                    Collectors.summarizingDouble(Product::price)
                ));
            System.out.println("\nStats by category:");
            statsByCategory.forEach((cat, stats) ->
                System.out.printf("  %s: count=%d, avg=$%,.2f, max=$%,.2f%n",
                    cat, stats.getCount(), stats.getAverage(), stats.getMax()));
    
            // minBy and maxBy
            Optional cheapest = products.stream()
                .collect(Collectors.minBy(Comparator.comparingDouble(Product::price)));
            cheapest.ifPresent(p -> System.out.println("\nCheapest: " + p.name() + " $" + p.price()));
    
            Optional mostExpensive = products.stream()
                .collect(Collectors.maxBy(Comparator.comparingDouble(Product::price)));
            mostExpensive.ifPresent(p -> System.out.println("Most expensive: " + p.name() + " $" + p.price()));
        }
    }

    8. Reducing and Mapping

    Collectors.reducing() and Collectors.mapping() are general-purpose downstream collectors that give you fine-grained control when the built-in aggregation collectors are not enough.

    8.1 Collectors.reducing()

    reducing() is the Collector equivalent of Stream.reduce(). It is typically used as a downstream collector inside groupingBy() when you need a custom reduction per group.

    import java.util.*;
    import java.util.stream.Collectors;
    
    public class ReducingCollector {
    
        record Employee(String name, String department, double salary) {}
    
        public static void main(String[] args) {
            List employees = List.of(
                new Employee("Alice", "Engineering", 95000),
                new Employee("Bob", "Engineering", 110000),
                new Employee("Charlie", "Marketing", 72000),
                new Employee("Diana", "Marketing", 78000),
                new Employee("Eve", "Sales", 68000)
            );
    
            // reducing() with identity -- total salary per department
            Map totalByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.reducing(0.0, Employee::salary, Double::sum)
                ));
            System.out.println("Total by dept: " + totalByDept);
            // Output: Total by dept: {Engineering=205000.0, Marketing=150000.0, Sales=68000.0}
    
            // reducing() without identity -- highest earner per department
            Map> topPerDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.reducing((e1, e2) ->
                        e1.salary() > e2.salary() ? e1 : e2)
                ));
            topPerDept.forEach((dept, emp) ->
                emp.ifPresent(e -> System.out.println(dept + " top: " + e.name())));
    
            // reducing() to concatenate names per department
            Map namesByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.reducing("",
                        Employee::name,
                        (a, b) -> a.isEmpty() ? b : a + ", " + b)
                ));
            System.out.println("Names by dept: " + namesByDept);
        }
    }

    8.2 Collectors.mapping() and Collectors.flatMapping()

    mapping() transforms elements before passing them to a downstream collector. flatMapping() (Java 9+) does the same but flattens the result. Both are used as downstream collectors.

    import java.util.*;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    public class MappingCollector {
    
        record Employee(String name, String department, List skills) {}
    
        public static void main(String[] args) {
            List employees = List.of(
                new Employee("Alice", "Engineering", List.of("Java", "Python", "SQL")),
                new Employee("Bob", "Engineering", List.of("Java", "Go", "Docker")),
                new Employee("Charlie", "Marketing", List.of("SEO", "Analytics")),
                new Employee("Diana", "Marketing", List.of("Content", "SEO", "Social"))
            );
    
            // mapping() -- extract names per department
            Map> namesByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.mapping(Employee::name, Collectors.toList())
                ));
            System.out.println("Names: " + namesByDept);
            // Output: Names: {Engineering=[Alice, Bob], Marketing=[Charlie, Diana]}
    
            // mapping() with joining downstream
            Map joinedByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.mapping(Employee::name, Collectors.joining(", "))
                ));
            System.out.println("Joined: " + joinedByDept);
    
            // flatMapping() (Java 9+) -- collect all skills per department
            Map> skillsByDept = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.flatMapping(
                        e -> e.skills().stream(),
                        Collectors.toSet()
                    )
                ));
            System.out.println("Skills: " + skillsByDept);
            // Output: Skills: {Engineering=[Docker, Go, Java, Python, SQL], Marketing=[Analytics, Content, SEO, Social]}
    
            // filtering() (Java 9+) -- filter within each group
            Map> seniorDevs = employees.stream()
                .collect(Collectors.groupingBy(
                    Employee::department,
                    Collectors.filtering(
                        e -> e.skills().size() >= 3,
                        Collectors.mapping(Employee::name, Collectors.toList())
                    )
                ));
            System.out.println("3+ skills: " + seniorDevs);
            // Output: 3+ skills: {Engineering=[Alice, Bob], Marketing=[Diana]}
        }
    }

    9. Custom Collectors

    When the built-in collectors do not fit your needs, you can create a custom collector using Collector.of(). You provide four components:

    Component Type Description
    Supplier Supplier<A> Creates the mutable result container
    Accumulator BiConsumer<A, T> Adds an element to the container
    Combiner BinaryOperator<A> Merges two containers (for parallel streams)
    Finisher Function<A, R> Transforms the container into the final result
    import java.util.*;
    import java.util.stream.Collector;
    import java.util.stream.Collectors;
    
    public class CustomCollectorExample {
        public static void main(String[] args) {
            List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    
            // Custom collector: collect into a comma-separated string with brackets
            String result = numbers.stream()
                .collect(Collector.of(
                    StringBuilder::new,                           // supplier
                    (sb, num) -> {                                // accumulator
                        if (sb.length() > 0) sb.append(", ");
                        sb.append(num);
                    },
                    (sb1, sb2) -> {                               // combiner
                        if (sb1.length() > 0 && sb2.length() > 0) sb1.append(", ");
                        return sb1.append(sb2);
                    },
                    sb -> "[" + sb.toString() + "]"               // finisher
                ));
            System.out.println("Custom string: " + result);
            // Output: Custom string: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    
            // Custom collector: compute running average
            double avg = numbers.stream()
                .collect(Collector.of(
                    () -> new double[]{0, 0},                     // [sum, count]
                    (acc, num) -> { acc[0] += num; acc[1]++; },   // accumulate
                    (a1, a2) -> {                                  // combine
                        a1[0] += a2[0];
                        a1[1] += a2[1];
                        return a1;
                    },
                    acc -> acc[1] == 0 ? 0.0 : acc[0] / acc[1]   // finish
                ));
            System.out.println("Average: " + avg); // Output: Average: 5.5
    
            // Custom collector: toImmutableList using Collector.of with UNORDERED characteristic
            List names = List.of("Alice", "Bob", "Charlie");
            List immutable = names.stream()
                .filter(n -> n.length() > 3)
                .collect(Collector.of(
                    ArrayList::new,
                    ArrayList::add,
                    (list1, list2) -> { list1.addAll(list2); return list1; },
                    Collections::unmodifiableList
                ));
            System.out.println("Immutable: " + immutable);
            // immutable.add("Fail"); // throws UnsupportedOperationException
    
            // Custom collector: group into even-indexed and odd-indexed elements
            List items = List.of("A", "B", "C", "D", "E", "F");
            Map> evenOddIndex = items.stream()
                .collect(Collector.of(
                    () -> {
                        Map> map = new HashMap<>();
                        map.put("even-index", new ArrayList<>());
                        map.put("odd-index", new ArrayList<>());
                        map.put("_counter", new ArrayList<>()); // track index
                        return map;
                    },
                    (map, item) -> {
                        int index = map.get("_counter").size();
                        map.get("_counter").add("x");
                        if (index % 2 == 0) {
                            map.get("even-index").add(item);
                        } else {
                            map.get("odd-index").add(item);
                        }
                    },
                    (m1, m2) -> { m1.get("even-index").addAll(m2.get("even-index"));
                                  m1.get("odd-index").addAll(m2.get("odd-index")); return m1; },
                    map -> { map.remove("_counter");  return map; }
                ));
            System.out.println("Even-index: " + evenOddIndex.get("even-index"));
            System.out.println("Odd-index:  " + evenOddIndex.get("odd-index"));
            // Output: Even-index: [A, C, E]
            //         Odd-index:  [B, D, F]
        }
    }

    Reusable Custom Collector

    For collectors you use frequently, define them as static methods so they can be imported and reused like the built-in collectors.

    import java.util.*;
    import java.util.stream.Collector;
    
    public class ReusableCollector {
    
        // Custom collector: toLinkedList()
        public static  Collector> toLinkedList() {
            return Collector.of(
                LinkedList::new,
                LinkedList::add,
                (list1, list2) -> { list1.addAll(list2); return list1; }
            );
        }
    
        // Custom collector: toReversedList()
        public static  Collector> toReversedList() {
            return Collector.of(
                ArrayList::new,
                (list, item) -> list.add(0, item),
                (list1, list2) -> { list2.addAll(list1); return list2; }
            );
        }
    
        public static void main(String[] args) {
            List names = List.of("Alice", "Bob", "Charlie", "David");
    
            // Use custom toLinkedList()
            LinkedList linked = names.stream()
                .filter(n -> n.length() > 3)
                .collect(toLinkedList());
            System.out.println("LinkedList: " + linked);
            System.out.println("Type: " + linked.getClass().getSimpleName());
            // Output: LinkedList: [Alice, Charlie, David]
            //         Type: LinkedList
    
            // Use custom toReversedList()
            List reversed = names.stream()
                .collect(toReversedList());
            System.out.println("Reversed: " + reversed);
            // Output: Reversed: [David, Charlie, Bob, Alice]
        }
    }

    10. Complete Practical Example: Sales Analytics

    Let us combine everything into a realistic scenario. We have a list of sales transactions and need to perform various analytics using the full power of the Collectors class.

    import java.time.LocalDate;
    import java.time.Month;
    import java.util.*;
    import java.util.stream.Collectors;
    
    public class SalesAnalytics {
    
        record Sale(String product, String category, String region,
                    double amount, int quantity, LocalDate date) {}
    
        public static void main(String[] args) {
            List sales = List.of(
                new Sale("Laptop",    "Electronics", "North", 999.99, 5,  LocalDate.of(2024, 1, 15)),
                new Sale("Phone",     "Electronics", "South", 699.99, 12, LocalDate.of(2024, 1, 20)),
                new Sale("Tablet",    "Electronics", "North", 449.99, 8,  LocalDate.of(2024, 2, 5)),
                new Sale("Desk",      "Furniture",   "East",  299.99, 3,  LocalDate.of(2024, 2, 10)),
                new Sale("Chair",     "Furniture",   "West",  199.99, 15, LocalDate.of(2024, 2, 14)),
                new Sale("Laptop",    "Electronics", "East",  999.99, 7,  LocalDate.of(2024, 3, 1)),
                new Sale("Phone",     "Electronics", "North", 699.99, 20, LocalDate.of(2024, 3, 5)),
                new Sale("Notebook",  "Office",      "South", 4.99,   500,LocalDate.of(2024, 3, 10)),
                new Sale("Pen",       "Office",      "West",  1.99,   1000, LocalDate.of(2024, 3, 15)),
                new Sale("Chair",     "Furniture",   "North", 199.99, 10, LocalDate.of(2024, 4, 1)),
                new Sale("Desk",      "Furniture",   "South", 299.99, 5,  LocalDate.of(2024, 4, 10)),
                new Sale("Laptop",    "Electronics", "West",  999.99, 3,  LocalDate.of(2024, 4, 20)),
                new Sale("Phone",     "Electronics", "East",  699.99, 15, LocalDate.of(2024, 5, 1)),
                new Sale("Tablet",    "Electronics", "South", 449.99, 6,  LocalDate.of(2024, 5, 10)),
                new Sale("Chair",     "Furniture",   "East",  199.99, 20, LocalDate.of(2024, 5, 15))
            );
    
            // ===== 1. Revenue by Category =====
            System.out.println("=== Revenue by Category ===");
            Map revenueByCategory = sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::category,
                    Collectors.summingDouble(s -> s.amount() * s.quantity())
                ));
            revenueByCategory.entrySet().stream()
                .sorted(Map.Entry.comparingByValue().reversed())
                .forEach(e -> System.out.printf("  %-15s $%,12.2f%n", e.getKey(), e.getValue()));
    
            // ===== 2. Top Products by Total Revenue =====
            System.out.println("\n=== Top Products by Revenue ===");
            sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::product,
                    Collectors.summingDouble(s -> s.amount() * s.quantity())
                ))
                .entrySet().stream()
                .sorted(Map.Entry.comparingByValue().reversed())
                .limit(3)
                .forEach(e -> System.out.printf("  %-15s $%,12.2f%n", e.getKey(), e.getValue()));
    
            // ===== 3. Average Order Value by Region =====
            System.out.println("\n=== Average Order Value by Region ===");
            Map avgByRegion = sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::region,
                    Collectors.averagingDouble(s -> s.amount() * s.quantity())
                ));
            avgByRegion.forEach((region, avg) ->
                System.out.printf("  %-10s $%,10.2f%n", region, avg));
    
            // ===== 4. Monthly Revenue Trend =====
            System.out.println("\n=== Monthly Revenue ===");
            Map monthlyRevenue = sales.stream()
                .collect(Collectors.groupingBy(
                    s -> s.date().getMonth(),
                    TreeMap::new,  // sorted by month
                    Collectors.summingDouble(s -> s.amount() * s.quantity())
                ));
            monthlyRevenue.forEach((month, rev) ->
                System.out.printf("  %-12s $%,12.2f%n", month, rev));
    
            // ===== 5. Products per Category with Quantities =====
            System.out.println("\n=== Products per Category ===");
            Map> productsByCat = sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::category,
                    Collectors.groupingBy(
                        Sale::product,
                        Collectors.summingInt(Sale::quantity)
                    )
                ));
            productsByCat.forEach((cat, products) -> {
                System.out.println("  " + cat + ":");
                products.forEach((prod, qty) ->
                    System.out.printf("    %-15s %,d units%n", prod, qty));
            });
    
            // ===== 6. Partition: High-Value vs Low-Value Transactions =====
            System.out.println("\n=== High vs Low Value Transactions ===");
            double threshold = 3000;
            Map highLow = sales.stream()
                .collect(Collectors.partitioningBy(
                    s -> s.amount() * s.quantity() >= threshold,
                    Collectors.counting()
                ));
            System.out.println("  High-value (>= $" + threshold + "): " + highLow.get(true));
            System.out.println("  Low-value  (<  $" + threshold + "): " + highLow.get(false));
    
            // ===== 7. Region with Highest Total Revenue =====
            sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::region,
                    Collectors.summingDouble(s -> s.amount() * s.quantity())
                ))
                .entrySet().stream()
                .max(Map.Entry.comparingByValue())
                .ifPresent(e -> System.out.printf(
                    "%n=== Top Region ===%n  %s with $%,.2f total revenue%n",
                    e.getKey(), e.getValue()));
    
            // ===== 8. Summary Statistics per Category =====
            System.out.println("\n=== Revenue Stats per Category ===");
            sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::category,
                    Collectors.summarizingDouble(s -> s.amount() * s.quantity())
                ))
                .forEach((cat, stats) -> System.out.printf(
                    "  %s: %d transactions, avg=$%,.2f, min=$%,.2f, max=$%,.2f, total=$%,.2f%n",
                    cat, stats.getCount(), stats.getAverage(),
                    stats.getMin(), stats.getMax(), stats.getSum()));
    
            // ===== 9. Category-Region Cross Report =====
            System.out.println("\n=== Category-Region Revenue Matrix ===");
            Map> matrix = sales.stream()
                .collect(Collectors.groupingBy(
                    Sale::category,
                    Collectors.groupingBy(
                        Sale::region,
                        Collectors.summingDouble(s -> s.amount() * s.quantity())
                    )
                ));
            matrix.forEach((cat, regions) -> {
                System.out.println("  " + cat + ":");
                regions.entrySet().stream()
                    .sorted(Map.Entry.comparingByValue().reversed())
                    .forEach(e -> System.out.printf("    %-10s $%,10.2f%n", e.getKey(), e.getValue()));
            });
    
            // ===== 10. Full Report as Formatted String =====
            System.out.println("\n=== Full Sales Report ===");
            String report = sales.stream()
                .sorted(Comparator.comparing(Sale::date))
                .map(s -> String.format("  %s | %-10s | %-12s | %-6s | %3d units | $%,10.2f",
                    s.date(), s.product(), s.category(), s.region(),
                    s.quantity(), s.amount() * s.quantity()))
                .collect(Collectors.joining("\n"));
            System.out.println(String.format("  %-10s | %-10s | %-12s | %-6s | %9s | %12s",
                "Date", "Product", "Category", "Region", "Qty", "Revenue"));
            System.out.println("  " + "-".repeat(75));
            System.out.println(report);
        }
    }

    Collectors Quick Reference

    Collector Use Case Example
    toList() Collect to mutable List stream.collect(Collectors.toList())
    toSet() Collect to Set (no duplicates) stream.collect(Collectors.toSet())
    toMap() Build key-value pairs Collectors.toMap(k, v, merge)
    joining() Concatenate strings Collectors.joining(", ", "[", "]")
    groupingBy() Group by classifier Collectors.groupingBy(f, downstream)
    partitioningBy() Split into true/false Collectors.partitioningBy(pred)
    counting() Count elements in group groupingBy(f, counting())
    summingDouble() Sum numeric values groupingBy(f, summingDouble(g))
    averagingDouble() Average numeric values groupingBy(f, averagingDouble(g))
    summarizingDouble() All stats in one pass groupingBy(f, summarizingDouble(g))
    minBy() / maxBy() Min/max in group groupingBy(f, minBy(comp))
    reducing() Custom reduction per group groupingBy(f, reducing(id, mapper, op))
    mapping() Transform before downstream groupingBy(f, mapping(g, toList()))
    flatMapping() Flatten before downstream groupingBy(f, flatMapping(g, toSet()))
    filtering() Filter within each group groupingBy(f, filtering(pred, toList()))
    Collector.of() Build a custom collector Collector.of(supplier, acc, comb, fin)

    Key Takeaways

    • Collectors are the bridge between streams and concrete data structures. The collect() terminal operation relies on them entirely.
    • toList(), toSet(), toMap() cover 80% of use cases. Learn these first.
    • groupingBy() is the Swiss army knife for analytics. Combined with downstream collectors (counting, summing, averaging, mapping), it replaces complex manual grouping code.
    • partitioningBy() is a specialized groupingBy for boolean splits — always has both true and false keys.
    • Downstream collectors (counting, mapping, reducing, filtering, flatMapping) compose inside groupingBy to build powerful data transformations.
    • toMap() merge functions handle duplicate keys gracefully — decide whether to keep first, keep last, or combine.
    • Custom collectors via Collector.of() give you full control when built-in options are insufficient.
    • summarizingDouble() is efficient — it computes count, sum, min, max, and average in a single pass.



    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 *