Lesson 7: Collectors & Method References

The Collectors API (java.util.stream.Collectors) provides powerful tools for processing and collecting stream results, such as converting them into lists, sets, maps, or even performing grouping, partitioning, and joining.

Method References (::) provide a shorthand way to refer to methods or constructors for cleaner code.


1. Collecting Stream Results (Collectors)

The Collectors utility class provides several useful terminal operations for collecting results from a stream.

Common Collectors Methods

MethodDescriptionExample
toList()Collect elements into a Listlist.stream().collect(Collectors.toList())
toSet()Collect elements into a Setlist.stream().collect(Collectors.toSet())
toMap()Collect elements into a Maplist.stream().collect(Collectors.toMap(k, v))
joining()Join elements into a stringlist.stream().collect(Collectors.joining(", "))
counting()Count elementslist.stream().collect(Collectors.counting())
groupingBy()Group elements by a criterionlist.stream().collect(Collectors.groupingBy(e -> e.getCategory()))
partitioningBy()Partition elements into two groups (true/false)list.stream().collect(Collectors.partitioningBy(n -> n > 10))

📌 Example 1: Collecting Elements into a List

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

public class Main {
    public static void main(String[] args) {
        List<String> names = Stream.of("Alice", "Bob", "Charlie")
                                   .collect(Collectors.toList());
        System.out.println(names);
    }
}

Output:

[Alice, Bob, Charlie]

✔ The collect(Collectors.toList()) converts a stream into a List.


📌 Example 2: Collecting to a Map

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

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

        Map<String, Integer> nameLengths = names.stream()
                                               .collect(Collectors.toMap(name -> name, name -> name.length()));

        System.out.println(nameLengths);
    }
}

Output:

{Alice=5, Bob=3, Charlie=7}

✔ The toMap() collector transforms a stream into a key-value mapping.


📌 Example 3: Joining Elements into a String

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

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("Java", "is", "awesome");
        String sentence = words.stream()
                               .collect(Collectors.joining(" "));

        System.out.println(sentence);
    }
}

Output:

Java is awesome

joining(" ") concatenates elements with a space.


2. Grouping and Partitioning Data

📌 Example 4: Grouping Elements (groupingBy())

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

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

        Map<Character, List<String>> groupedByFirstLetter = names.stream()
                                                                 .collect(Collectors.groupingBy(name -> name.charAt(0)));

        System.out.println(groupedByFirstLetter);
    }
}

Output:

{A=[Alice, Amanda], B=[Bob, Brian], C=[Charlie]}

groupingBy() groups elements based on the first letter.


📌 Example 5: Partitioning Elements (partitioningBy())

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

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(5, 12, 3, 8, 19, 14);

        Map<Boolean, List<Integer>> partitioned = numbers.stream()
                                                         .collect(Collectors.partitioningBy(n -> n > 10));

        System.out.println(partitioned);
    }
}

Output:

{false=[5, 3, 8], true=[12, 19, 14]}

partitioningBy(n -> n > 10) divides numbers into two lists (≤10 and >10).


3. Method References (::)

What Are Method References?

Method references (::) provide a shorthand way to refer to existing methods.

Types of Method References

TypeSyntaxExample
Static method referenceClassName::methodNameMath::abs
Instance method reference (specific object)instance::methodNameSystem.out::println
Instance method reference (any instance of class)ClassName::methodNameString::toUpperCase
Constructor referenceClassName::newArrayList::new

📌 Example 6: Using a Static Method Reference

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(-5, -10, 15, 20);

        numbers.stream()
               .map(Math::abs)  // Using static method reference
               .forEach(System.out::println);
    }
}

Output:

5
10
15
20

Math::abs replaces n -> Math.abs(n) for readability.


📌 Example 7: Using an Instance Method Reference

import java.util.List;

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

        names.stream()
             .map(String::toUpperCase)  // Instead of name -> name.toUpperCase()
             .forEach(System.out::println);
    }
}

Output:

ALICE
BOB
CHARLIE

String::toUpperCase replaces name -> name.toUpperCase().


📌 Example 8: Using a Constructor Reference

import java.util.List;
import java.util.function.Supplier;

public class Main {
    public static void main(String[] args) {
        Supplier<StringBuilder> supplier = StringBuilder::new;
        StringBuilder sb = supplier.get(); // Creates new instance
        System.out.println("StringBuilder created: " + sb);
    }
}

StringBuilder::new replaces () -> new StringBuilder() for readability.


Lesson Reflection

  1. How do Collectors improve working with Streams?
  2. When should we use groupingBy() vs. partitioningBy()?
  3. Why are method references useful compared to lambdas?

1. How do Collectors improve working with Streams?

Collectors help aggregate and transform data efficiently

  • Without Collectors, we would have to manually accumulate results using loops.
  • They allow collecting stream data into List, Set, Map, or even performing complex operations like grouping and partitioning.

Make code more readable and concise

  • Instead of manually iterating over a stream, Collectors allow a declarative approach.

📌 Example: Without Collectors (Traditional Approach)

import java.util.ArrayList;
import java.util.List;

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

        for (String name : names) {
            upperCaseNames.add(name.toUpperCase());
        }

        System.out.println(upperCaseNames);
    }
}

Manual iteration, mutable collection, more code.

📌 With Collectors (Cleaner Approach)

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

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

        List<String> upperCaseNames = names.stream()
                                           .map(String::toUpperCase)
                                           .collect(Collectors.toList());

        System.out.println(upperCaseNames);
    }
}

Simpler, more readable, and immutable result.


2. When should we use groupingBy() vs. partitioningBy()?

Both are used to categorize data, but they serve different purposes:

FeaturegroupingBy()partitioningBy()
PurposeGroups elements into multiple categoriesSplits elements into two categories (true/false)
Return TypeMap<K, List<V>> (Multiple groups)Map<Boolean, List<V>> (Only two groups)
Use CaseCategorization based on key (e.g., by first letter, type)Binary classification (e.g., odd/even, adults/minors)

📌 Example: groupingBy() – Group Names by First Letter

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

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

        Map<Character, List<String>> groupedByLetter = names.stream()
                                                            .collect(Collectors.groupingBy(name -> name.charAt(0)));

        System.out.println(groupedByLetter);
    }
}

Output:

{A=[Alice, Amanda], B=[Bob, Brian], C=[Charlie]}

Best when grouping elements into multiple categories (A, B, C, etc.).


📌 Example: partitioningBy() – Separate Even & Odd Numbers

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

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(5, 12, 3, 8, 19, 14);

        Map<Boolean, List<Integer>> partitioned = numbers.stream()
                                                         .collect(Collectors.partitioningBy(n -> n % 2 == 0));

        System.out.println(partitioned);
    }
}

Output:

{false=[5, 3, 19], true=[12, 8, 14]}

Best when we need only two groups (e.g., pass/fail, true/false, yes/no).


3. Why are Method References Useful Compared to Lambdas?

Improve Readability

  • Method References (::) remove unnecessary lambda syntax.

More Concise & Direct

  • Instead of writing n -> Math.abs(n), we simply use Math::abs.

Reuses Existing Methods

  • No need to define new anonymous functions when an existing method already does the job.

📌 Example: Using Lambda Instead of Method Reference

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(-5, -10, 15, 20);

        numbers.stream()
               .map(n -> Math.abs(n))  // Using lambda
               .forEach(n -> System.out.println(n));
    }
}

✔ Works fine, but lambda adds unnecessary boilerplate.


📌 Example: Using Method Reference (Cleaner)

import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(-5, -10, 15, 20);

        numbers.stream()
               .map(Math::abs)  // Using method reference
               .forEach(System.out::println);
    }
}

Output:

5
10
15
20

Less code, more readable, and directly reuses Math.abs().


📌 When to Use Method References Instead of Lambdas?

ScenarioUse LambdaUse Method Reference
When extra logic is neededlist.forEach(n -> System.out.println("Number: " + n))❌ Not possible
When directly calling an existing methodlist.forEach(n -> System.out.println(n))list.forEach(System.out::println)
When calling a static methodlist.map(n -> Math.abs(n))list.map(Math::abs)
When using a constructor reference() -> new ArrayList<>()ArrayList::new

🔄 Conclusion: Why Use Collectors & Method References?

Collectors help aggregate, transform, and organize stream data efficiently.
Use groupingBy() for multi-category grouping and partitioningBy() for binary classification.
Method references make code cleaner and remove redundant lambda expressions.

The next Java 8 feature: Stream API Enhancements in Java 9+ 😊

Java Sleep