Advanced Stream Operations in Java

Master advanced stream operations including decomposition, concatenation, reduction operations, grouping, partitioning, collectors, and parallel streams for the OCP 21 exam.

Table of Contents

1. Collectors

The Collectors class provides various reduction operations that can be used with the collect() terminal operation.

1.1 Common Collectors

Example:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Alice");

// toList() - collect to List
List<String> list = names.stream()
    .collect(Collectors.toList());

// toSet() - collect to Set (removes duplicates)
Set<String> set = names.stream()
    .collect(Collectors.toSet());  // [Alice, Bob, Charlie]

// toCollection() - collect to specific collection
LinkedList<String> linkedList = names.stream()
    .collect(Collectors.toCollection(LinkedList::new));

// joining() - join strings
String joined = names.stream()
    .collect(Collectors.joining(", "));  // "Alice, Bob, Charlie, Alice"

String joinedWithPrefixSuffix = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));  // "[Alice, Bob, Charlie, Alice]"

1.2 Aggregation Collectors

Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// counting() - count elements
Long count = numbers.stream()
    .collect(Collectors.counting());  // 5

// summingInt(), summingLong(), summingDouble()
Integer sum = numbers.stream()
    .collect(Collectors.summingInt(Integer::intValue));  // 15

// averagingInt(), averagingLong(), averagingDouble()
Double avg = numbers.stream()
    .collect(Collectors.averagingInt(Integer::intValue));  // 3.0

// summarizingInt(), summarizingLong(), summarizingDouble()
IntSummaryStatistics stats = numbers.stream()
    .collect(Collectors.summarizingInt(Integer::intValue));
// IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}

// maxBy(), minBy() - with Comparator
Optional<Integer> max = numbers.stream()
    .collect(Collectors.maxBy(Integer::compareTo));  // Optional[5]

Optional<Integer> min = numbers.stream()
    .collect(Collectors.minBy(Integer::compareTo));  // Optional[1]

2. Grouping and Partitioning

2.1 Grouping

Example:
List<String> words = Arrays.asList("apple", "banana", "apricot", "blueberry", "avocado");

// groupingBy() - group by classification function
Map<Character, List<String>> byFirstLetter = words.stream()
    .collect(Collectors.groupingBy(s -> s.charAt(0)));
// {a=[apple, apricot, avocado], b=[banana, blueberry]}

// groupingBy() with downstream collector
Map<Character, Long> countByFirstLetter = words.stream()
    .collect(Collectors.groupingBy(s -> s.charAt(0), Collectors.counting()));
// {a=3, b=2}

Map<Character, Set<String>> setByFirstLetter = words.stream()
    .collect(Collectors.groupingBy(s -> s.charAt(0), Collectors.toSet()));

// groupingBy() with map factory
TreeMap<Character, List<String>> sorted = words.stream()
    .collect(Collectors.groupingBy(s -> s.charAt(0), TreeMap::new, Collectors.toList()));

2.2 Partitioning

Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// partitioningBy() - partition by predicate (always creates two groups)
Map<Boolean, List<Integer>> partitioned = numbers.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));
// {false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}

// partitioningBy() with downstream collector
Map<Boolean, Long> countPartitioned = numbers.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0, Collectors.counting()));
// {false=5, true=5}

Map<Boolean, Integer> sumPartitioned = numbers.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0, 
        Collectors.summingInt(Integer::intValue)));
// {false=25, true=30}

3. Advanced Reduction Operations

3.1 Custom Collectors

Example:
// Collecting to custom result
String result = Stream.of("a", "b", "c")
    .collect(Collectors.reducing("", String::concat));  // "abc"

// Reducing with binary operator
Optional<Integer> sum = Stream.of(1, 2, 3, 4, 5)
    .collect(Collectors.reducing(Integer::sum));  // Optional[15]

// Reducing with identity
Integer sum2 = Stream.of(1, 2, 3, 4, 5)
    .collect(Collectors.reducing(0, Integer::sum));  // 15

// Mapping and reducing
Optional<String> longest = Stream.of("apple", "banana", "cherry")
    .collect(Collectors.reducing((s1, s2) -> s1.length() > s2.length() ? s1 : s2));

3.2 Multi-level Grouping

Example:
List<Person> people = Arrays.asList(
    new Person("Alice", 25, "Engineering"),
    new Person("Bob", 30, "Engineering"),
    new Person("Charlie", 25, "Sales"),
    new Person("David", 30, "Sales")
);

// Group by department, then by age
Map<String, Map<Integer, List<Person>>> nested = people.stream()
    .collect(Collectors.groupingBy(Person::getDepartment,
        Collectors.groupingBy(Person::getAge)));

// Group by department, count by age
Map<String, Map<Integer, Long>> nestedCount = people.stream()
    .collect(Collectors.groupingBy(Person::getDepartment,
        Collectors.groupingBy(Person::getAge, Collectors.counting())));

4. Stream Concatenation

4.1 Concatenating Streams

Example:
Stream<String> stream1 = Stream.of("a", "b", "c");
Stream<String> stream2 = Stream.of("d", "e", "f");

// Stream.concat() - concatenate two streams
Stream<String> concatenated = Stream.concat(stream1, stream2);
concatenated.forEach(System.out::println);  // a, b, c, d, e, f

// Concatenating multiple streams
Stream<String> stream3 = Stream.of("g", "h");
Stream<String> all = Stream.concat(
    Stream.concat(stream1, stream2), stream3);

// Using flatMap for concatenation
Stream<String> combined = Stream.of(stream1, stream2, stream3)
    .flatMap(Function.identity());

5. Parallel Streams

Parallel streams enable parallel processing of stream operations using multiple threads.

5.1 Creating Parallel Streams

Example:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// parallelStream() - create parallel stream from collection
Stream<Integer> parallel1 = numbers.parallelStream();

// parallel() - convert sequential stream to parallel
Stream<Integer> parallel2 = numbers.stream().parallel();

// isParallel() - check if stream is parallel
boolean isParallel = numbers.parallelStream().isParallel();  // true

// sequential() - convert parallel stream to sequential
Stream<Integer> sequential = numbers.parallelStream().sequential();

5.2 Parallel Stream Considerations

Important: Parallel streams require:
  • Stateless operations
  • Non-interfering operations
  • Associative reduction operations
  • Sufficient data size to justify overhead
Example:
// Good for parallel - stateless operations
List<Integer> numbers = IntStream.range(1, 1000000).boxed().collect(Collectors.toList());

long sum = numbers.parallelStream()
    .mapToInt(Integer::intValue)
    .sum();  // Can benefit from parallel processing

// forEachOrdered() - maintain order in parallel streams
numbers.parallelStream()
    .forEachOrdered(System.out::println);  // Maintains order

// Unordered operations can be faster
numbers.parallelStream()
    .unordered()
    .forEach(System.out::println);  // May be faster, order not guaranteed

6. Exam Key Points

Critical Concepts for OCP 21 Exam:

  • Collectors: toList(), toSet(), toCollection(), joining()
  • Grouping: groupingBy() groups elements by classification function
  • Partitioning: partitioningBy() always creates two groups (true/false)
  • Downstream Collectors: Can chain collectors for complex operations
  • Multi-level Grouping: Can nest groupingBy() operations
  • Aggregation Collectors: counting(), summingInt(), averagingInt(), summarizingInt()
  • Stream Concatenation: Stream.concat() combines two streams
  • Parallel Streams: parallelStream() or parallel() for parallel processing
  • Parallel Requirements: Stateless, non-interfering, associative operations
  • forEachOrdered(): Maintains order in parallel streams
  • unordered(): Can improve performance for parallel streams
  • Reduction: reducing() for custom reduction operations

Post a Comment

0 Comments