Study guide
8.3 Advanced Collectors, Grouping, Partitioning, and Teeing
Oracle Java SE 21 Developer 1Z0-830
Track your progress as you read
Finished sections get a check mark.
1 Chapter 1: Java Basics, Primitive Types, and Operators
1.1 Program Structure, Primitives, Literals, Scopes, and var 1.2 Operators, Precedence, and Type Casting 1.3 Strings, Text Blocks, and StringBuilder 1.4 Date and Time APIs (java.time)
2 Chapter 2: Controlling Program Flow & Pattern Matching
2.1 Conditional Logic: if-else and Ternary Operators 2.2 Switch Statements and Switch Expressions 2.3 Pattern Matching for switch and instanceof 2.4 Loops and Branching Statements
3 Chapter 3: Object-Oriented Design & Class Architecture
3.1 Class Declarations, Constructors, and Object Lifecycle 3.2 Encapsulation and Access Modifiers 3.3 Inheritance, Method Overriding, and Polymorphism 3.4 Abstract Classes and Interfaces 3.5 Nested, Inner, Local, and Anonymous Classes 3.6 Method Overloading, Overload Resolution, and Varargs
4 Chapter 4: Modern Java Data Modeling: Records, Enums, and Sealed Types
4.1 Java Records, Canonical/Compact Constructors, and Component Accessors 4.2 Java Enums: Fields, Methods, and Abstract Constant-Specific Behavior 4.3 Sealed Classes and Interfaces: permits, final, non-sealed, and Hierarchy Architecture 4.4 Advanced Record Patterns (JEP 440), Nested Deconstruction, and Sealed Hierarchy Exhaustiveness
5 Chapter 5: Exception Handling and Annotations
5.1 Exception Hierarchy: Checked vs. Unchecked Exceptions 5.2 Exception Handling: try, catch, finally, and Multi-Catch 5.3 Resource Management: try-with-resources and AutoCloseable 5.4 Custom Exceptions and Best Practices 5.5 Java Annotations: Built-in, Meta-Annotations, and Custom Types
6 Chapter 6: Generics and the Collections Framework
6.1 Generics Fundamentals and Generic Methods 6.2 Wildcards, Type Bounds, and Type Erasure 6.3 Core Collections: Lists, Sets, Queues, and Maps 6.4 Sequenced Collections in Java 21 6.5 Sorting, Comparable, and Comparator 6.6 Arrays: Creation, Multidimensional Forms, and the Arrays Utility Class
7 Chapter 7: Functional Programming & Lambda Expressions
7.1 Built-in Functional Interfaces in java.util.function 7.2 Lambda Expressions, Variable Scopes, and Effectively Final 7.3 Method References: Four Varieties and Edge Cases 7.4 Working with java.util.Optional
8 Chapter 8: The Stream API and Data Pipelines
8.1 Stream Creation and Intermediate Operations 8.2 Terminal Operations and Reductions 8.3 Advanced Collectors, Grouping, Partitioning, and Teeing 8.4 Primitive Streams and Parallel Stream Processing
9 Chapter 9: Concurrency, Multithreading, and Virtual Threads
9.1 Thread Fundamentals, Creation, and Lifecycle 9.2 Thread Safety, Synchronization, and the Java Memory Model 9.3 Concurrency Utilities: ExecutorService, Callable, and Future 9.4 Concurrent Collections, Atomics, and Explicit Locks 9.5 Virtual Threads in Java 21 (Carrier Threads, Pinning, and Best Practices)
10 Chapter 10: The Java Platform Module System (JPMS)
10.1 Module Architecture and module-info.java Declarations 10.2 Module Directives: requires, exports, opens, provides, and uses 10.3 Modular Compilation, Packaging, and CLI Execution 10.4 Module Types, Migration Strategies, ServiceLoader SPI, and JLink
11 Chapter 11: Java I/O, NIO.2, and Object Serialization
11.1 Classic Java I/O Streams and Readers/Writers 11.2 Java NIO.2 File API: Path, Paths, and File System Operations 11.3 Advanced NIO.2 Stream Operations and Directory Traversal 11.4 Java Object Serialization and Record Serialization
12 Chapter 12: Implementing Localization and Internationalization
12.1 Locales and Resource Bundles 12.2 Formatting and Parsing Numbers, Currencies, Dates, Times, and Messages
8.3 Advanced Collectors, Grouping, Partitioning, and Teeing
Key Takeaways Explain This Topic AI
The Collectors utility class provides mutable reduction implementations including toList, toSet, toCollection, toMap, and unmodifiable collection factories.
toMap requires an explicit merge function to resolve key collisions; omitting the merge function when duplicate keys occur causes an IllegalStateException at runtime.
groupingBy organizes elements into multi-valued maps and supports downstream collectors for nested reductions, while partitioningBy always divides elements by a boolean predicate into a Map containing both true and false keys.
The Collectors.teeing collector (Java 12+) evaluates two independent downstream collectors simultaneously over a single stream pass and merges their results using a BiFunction.
Last updated: September 2026
Advanced Collectors, Grouping, Partitioning, and Teeing
The collect() terminal operation represents the most powerful and versatile reduction mechanism in the Stream API. Rather than combining elements into a single scalar value like reduce() , collect() performs a mutable reduction , accumulating input elements into a mutable result container (such as a List , Set , Map , StringBuilder , or custom aggregate).
The java.util.stream.Collectors class provides a rich library of static factory methods for collection aggregation, grouping, partitioning, string formatting, and multi-stage downstream transformations. For the Java SE 21 Developer (1Z0-830) exam, advanced mastery of downstream collectors, key collision strategies, and the teeing collector is essential.
1. The Collector Interface Architecture
A Collector<T, A, R> is parameterized by three types:
T : The type of input elements to be collected.
A : The mutable accumulation type (often an internal container like List<T> or StringBuilder ).
R : The final result type returned by the collection process.
The Five Core Methods of a Collector
supplier() : Creates and returns a new, empty mutable result container ( Supplier<A> ).
accumulator() : Folds an input element into the mutable container ( BiConsumer<A, T> ).
combiner() : Merges two partial result containers together during parallel processing ( BinaryOperator<A> ).
finisher() : Transforms the intermediate container A into the final result R ( Function<A, R> ).
characteristics() : Returns a set of Characteristics ( CONCURRENT , UNORDERED , IDENTITY_FINISH ) informing the stream execution engine of optimization capabilities. // Three-argument collect() overload (imperative style equivalent to Collector):
- List<String> list = Stream.of("a", "b", "c").collect(
- ArrayList::new, // Supplier: creates container
- ArrayList::add, // Accumulator: adds item
- ArrayList::addAll // Combiner: merges containers in parallel
- );
- 2. Standard and Unmodifiable Collection Collectors
Java provides multiple collectors to accumulate elements into collections: Stream<String> techStream = Stream.of("Java", "Kotlin", "Java", "Rust");
- // 1. toList() vs Stream.toList()
- List<String> list1 = techStream.collect(Collectors.toList()); // Standard modifiable List (ArrayList)
- List<String> list2 = Stream.of("A", "B").toList(); // Java 16+ concise, unmodifiable List (allows nulls)
// 2. toUnmodifiableList() (Java 10+)
List<String> unmodList = Stream.of("A", "B").collect(Collectors.toUnmodifiableList()); // Immutable, rejects nulls
- // 3. toSet() and toUnmodifiableSet()
- Set<String> set = Stream.of("A", "B", "A").collect(Collectors.toSet()); // Set of ["A", "B"]
- Set<String> unmodSet = Stream.of("A", "B").collect(Collectors.toUnmodifiableSet());
- // 4. toCollection() - Specify concrete collection implementation
- TreeSet<String> treeSet = Stream.of("Z", "A", "M")
- .collect(Collectors.toCollection(TreeSet::new)); // Sorted: ["A", "M", "Z"]
- LinkedList<String> linkedList = Stream.of("1", "2")
- .collect(Collectors.toCollection(LinkedList::new));
- Stream.toList() vs Collectors.toList() vs Collectors.toUnmodifiableList()
- Feature Stream.toList() (Java 16+) Collectors.toList() Collectors.toUnmodifiableList() (Java 10+)
- Syntax Direct terminal method on Stream Passed to .collect() Passed to .collect()
Modifiable? No (Throws UnsupportedOperationException ) Yes (Typically ArrayList ) No (Throws UnsupportedOperationException )
- Allows Nulls? Yes Yes No (Throws NullPointerException )
- Memory / Allocation Optimized internal array Standard ArrayList Unmodifiable wrapper / array
- 3. Map Collectors and Key Collision Resolution
The Collectors.toMap() collector converts stream elements into key-value map entries. There are three primary overloads:
Overload 1: Two-Argument toMap(keyMapper, valueMapper)
Throws IllegalStateException at runtime if duplicate keys are encountered! List<String> fruits = List.of("Apple", "Banana", "Cherry");
- Map<String, Integer> map = fruits.stream()
- .collect(Collectors.toMap(
- Function.identity(), // Key: the string itself
- String::length // Value: length of string
- ));
- // Result: {"Apple": 5, "Banana": 6, "Cherry": 6}
// RUNTIME CRASH (Duplicate Key: "Apple"):
- // List.of("Apple", "Apricot", "Apple").stream()
- // .collect(Collectors.toMap(s -> s.charAt(0), s -> s));
- // Throws: java.lang.IllegalStateException: Duplicate key A
- Overload 2: Three-Argument toMap(keyMapper, valueMapper, mergeFunction)
Provides a BinaryOperator merge function to resolve duplicate key collisions without throwing an exception. List<String> items = List.of("apple", "apricot", "banana", "avocado");
// Keep the longer string when keys (first letter) collide:
- Map<Character, String> longestByChar = items.stream()
- .collect(Collectors.toMap(
- s -> s.charAt(0), // Key: first char
- Function.identity(), // Value: string
- (existing, replacement) -> existing.length() >= replacement.length() ? existing : replacement // Merger
- ));
- // Result: {'a': "apricot", 'b': "banana"}
- Overload 3: Four-Argument toMap(keyMapper, valueMapper, mergeFunction, mapSupplier)
Allows specifying a custom Map implementation (e.g., TreeMap , LinkedHashMap ). TreeMap<Character, String> sortedMap = items.stream()
- .collect(Collectors.toMap(
- s -> s.charAt(0),
- s -> s,
- (v1, v2) -> v1 + ", " + v2,
- TreeMap::new // mapSupplier: returns sorted TreeMap
- ));
- 4. String Joining and Numeric Summaries List<String> frameworks = List.of("Spring", "Micronaut", "Quarkus");
- // joining()
- String joined1 = frameworks.stream().collect(Collectors.joining()); // "SpringMicronautQuarkus"
- String joined2 = frameworks.stream().collect(Collectors.joining(", ")); // "Spring, Micronaut, Quarkus"
String joined3 = frameworks.stream().collect(Collectors.joining(", ", "[", "]")); // "[Spring, Micronaut, Quarkus]"
- // Summarizing Numbers
- List<String> words = List.of("Java", "SE", "21");
- IntSummaryStatistics stats = words.stream()
- .collect(Collectors.summarizingInt(String::length));
- // stats.getAverage(), stats.getCount(), stats.getMax(), stats.getMin(), stats.getSum()
- 5. Multi-Level Grouping with groupingBy
Collectors.groupingBy partitions stream elements into a Map<K, List<T>> based on a classifier function. Like toMap , it offers three overloads: record Employee(String name, String department, double salary) {}
- List<Employee> employees = List.of(
- new Employee("Alice", "Engineering", 120_000),
- new Employee("Bob", "Engineering", 110_000),
- new Employee("Charlie", "HR", 85_000),
- new Employee("David", "Finance", 95_000),
- new Employee("Eve", "HR", 90_000)
- );
- // 1-Arg: groupingBy(classifier) -> Map<String, List<Employee>>
- Map<String, List<Employee>> byDept = employees.stream()
- .collect(Collectors.groupingBy(Employee::department));
- // 2-Arg: groupingBy(classifier, downstreamCollector)
- // Count employees per department -> Map<String, Long>
- Map<String, Long> countByDept = employees.stream()
- .collect(Collectors.groupingBy(Employee::department, Collectors.counting()));
- // Extract employee names into a Set per department -> Map<String, Set<String>>
- Map<String, Set<String>> namesByDept = employees.stream()
- .collect(Collectors.groupingBy(
- Employee::department,
- Collectors.mapping(Employee::name, Collectors.toSet())
- ));
- // 3-Arg: groupingBy(classifier, mapFactory, downstreamCollector)
- // Returns sorted TreeMap<String, Double> of average salary per department
- TreeMap<String, Double> avgSalaryByDept = employees.stream()
- .collect(Collectors.groupingBy(
- Employee::department,
- TreeMap::new,
- Collectors.averagingDouble(Employee::salary)
- ));
- 6. Partitioning with partitioningBy
Collectors.partitioningBy is a specialized form of grouping that takes a Predicate<? super T> and divides elements into two groups: true and false .
Important
The Partitioning Invariant : The resulting map from partitioningBy ALWAYS contains exactly two keys: Boolean.TRUE and Boolean.FALSE , even if one (or both) of the partitions contains zero matching elements! List<Integer> numbers = List.of(10, 15, 20, 25, 30);
- // 1-Arg: partitioningBy(predicate) -> Map<Boolean, List<Integer>>
- Map<Boolean, List<Integer>> evenOdd = numbers.stream()
- .collect(Collectors.partitioningBy(n -> n % 2 == 0));
- // evenOdd.get(true) -> [10, 20, 30]
- // evenOdd.get(false) -> [15, 25]
- // 2-Arg: partitioningBy(predicate, downstreamCollector)
- Map<Boolean, Long> countEvenOdd = numbers.stream()
- .collect(Collectors.partitioningBy(n -> n % 2 == 0, Collectors.counting()));
- // {true=3, false=2}
// Empty Partition Demonstration:
- Map<Boolean, List<Integer>> emptyTrue = List.of(1, 3, 5).stream()
- .collect(Collectors.partitioningBy(n -> n % 2 == 0));
// Result: {true=[], false=[1, 3, 5]} -> true key is PRESENT with an empty list!
7. Downstream Transformations (Java 9+)
Java 9 added downstream adaptation collectors that can be nested inside groupingBy and partitioningBy :
Collectors.filtering(Predicate, downstream) : Filters elements before downstream accumulation, ensuring categories with zero matching elements are still present in the map (unlike a top-level stream .filter() which removes empty categories entirely).
Collectors.flatMapping(Function<T, Stream<R>>, downstream) : Flattens nested streams into the downstream container.
Collectors.collectingAndThen(downstream, finisher) : Applies a post-processing finisher function to the result of a downstream collector. // filtering(): Keeps departments with 0 matching high-earners
- Map<String, Set<String>> highEarnersByDept = employees.stream()
- .collect(Collectors.groupingBy(
- Employee::department,
- Collectors.filtering(
- e -> e.salary() > 100_000,
- Collectors.mapping(Employee::name, Collectors.toSet())
- )
- ));
- // Finance dept is present: {"Engineering": ["Alice", "Bob"], "HR": [], "Finance": []}
- 8. Compound Aggregation with Collectors.teeing (Java 12+)
Introduced in Java 12, Collectors.teeing() takes two independent downstream collectors and a merging BiFunction . It passes every stream element to both collectors simultaneously in a single pass, then combines their results. Stream Elements ⟶ { Collector 1 ⟶ R 1 Collector 2 ⟶ R 2 ⟶ merger ( R 1 , R 2 ) ⟶ R f i n a l \text{Stream Elements} \longrightarrow \begin{cases} \text{Collector 1} \longrightarrow R_1 \\ \text{Collector 2} \longrightarrow R_2 \end{cases} \longrightarrow \text{merger}(R_1, R_2) \longrightarrow R_{final} Stream Elements ⟶ { Collector 1 ⟶ R 1 Collector 2 ⟶ R 2 ⟶ merger ( R 1 , R 2 ) ⟶ R f ina l record MinMax(int min, int max) {}
List<Integer> nums = List.of(42, 12, 89, 5, 67, 99, 23);
- MinMax minMax = nums.stream().collect(
- Collectors.teeing(
- Collectors.minBy(Integer::compareTo), // Collector 1: Optional<Integer>
- Collectors.maxBy(Integer::compareTo), // Collector 2: Optional<Integer>
- (minOpt, maxOpt) -> new MinMax(minOpt.orElse(0), maxOpt.orElse(0)) // Merger
- )
- );
- // minMax = MinMax[min=5, max=99]
// Calculate Mean and Count in a single pass:
record AverageStats(double average, long count) {}
- AverageStats stats = nums.stream().collect(
- Collectors.teeing(
- Collectors.averagingInt(n -> n), // Collector 1
- Collectors.counting(), // Collector 2
- AverageStats::new // Merger: (avg, cnt) -> new AverageStats(avg, cnt)
- )
- );