Java Streams, introduced in Java 8, provide a powerful way to perform functional-style operations on sequences of elements. Among the many stream operations, grouping and partitioning are essential techniques that allow developers to organize and manipulate data efficiently. In this guide, we’ll walk through the process of grouping and partitioning elements using Java’s Stream API and explain how to leverage the Collectors
class for these operations.
1. What is Grouping and Partitioning?
Grouping and partitioning are operations that allow you to classify and categorize elements in a collection. The difference between them lies in how the elements are categorized:
- Grouping involves collecting elements into a Map based on a classifier function. It can group elements by a key such as a property or value.
- Partitioning divides elements into two groups based on a predicate (true/false) and is often implemented using a Map
.
2. Grouping Elements Using Java Streams
The Collectors.groupingBy()
method is used to group elements based on a classifier function. Let’s look at an example where we group a list of people based on their age.
import java.util.*; import java.util.stream.*; class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return name + " (" + age + ")"; } } public class GroupingExample { public static void main(String[] args) { Listpeople = Arrays.asList( new Person("Alice", 30), new Person("Bob", 25), new Person("Charlie", 30), new Person("David", 35), new Person("Eve", 25) ); // Group people by age Map > groupedByAge = people.stream() .collect(Collectors.groupingBy(Person::getAge)); groupedByAge.forEach((age, peopleList) -> { System.out.println("Age " + age + ": " + peopleList); }); } }
In this example, the people are grouped by their age. The result is a map where the keys are ages, and the values are lists of people who share the same age.
Advanced Grouping: Grouping by Multiple Criteria
Java Streams also allow you to group elements by multiple criteria. This can be done by chaining multiple groupingBy()
collectors. For example, grouping people by both age and first letter of their name:
Map>> groupedByAgeAndName = people.stream() .collect(Collectors.groupingBy(Person::getAge, Collectors.groupingBy(p -> p.getName().charAt(0)))); groupedByAgeAndName.forEach((age, letterMap) -> { System.out.println("Age " + age + ":"); letterMap.forEach((letter, list) -> { System.out.println(" " + letter + ": " + list); }); });
This code groups people first by their age and then by the first letter of their name. This produces a nested map structure, where the outer map is grouped by age, and the inner map is grouped by the first letter of the name.
3. Partitioning Elements Using Java Streams
Partitioning is a specialized form of grouping where the grouping function returns two distinct groups, typically based on a condition (true or false). In Java Streams, you can achieve partitioning using the Collectors.partitioningBy()
method. Let’s take a look at an example where we partition a list of numbers into even and odd numbers:
import java.util.*; import java.util.stream.*; public class PartitioningExample { public static void main(String[] args) { Listnumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // Partition numbers into even and odd Map > partitioned = numbers.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0)); System.out.println("Even Numbers: " + partitioned.get(true)); System.out.println("Odd Numbers: " + partitioned.get(false)); } }
The partitioningBy()
method partitions the list into two groups: even and odd numbers. The resulting map has two entries with Boolean keys.
Advanced Partitioning: Partitioning with Additional Operations
You can also apply additional operations when partitioning elements. For example, you can partition the list of numbers and count how many even and odd numbers there are:
MappartitionedCounts = numbers.stream() .collect(Collectors.partitioningBy(n -> n % 2 == 0, Collectors.counting())); System.out.println("Even count: " + partitionedCounts.get(true)); System.out.println("Odd count: " + partitionedCounts.get(false));
In this case, instead of getting a list of even and odd numbers, we are counting how many numbers fall into each partition.
4. Combining Grouping and Partitioning
In some cases, you may need to combine both grouping and partitioning in your data processing logic. This can be achieved by combining the groupingBy()
and partitioningBy()
collectors, as shown in the following example:
Map>> groupedAndPartitioned = people.stream() .collect(Collectors.groupingBy(Person::getAge, Collectors.partitioningBy(p -> p.getAge() > 30))); groupedAndPartitioned.forEach((age, partitionMap) -> { System.out.println("Age " + age + ":"); System.out.println(" Older than 30: " + partitionMap.get(true)); System.out.println(" 30 or younger: " + partitionMap.get(false)); });
This example combines grouping by age and partitioning by whether the person’s age is greater than 30, resulting in a nested map structure.
5. Conclusion
Grouping and partitioning are powerful operations in Java Streams that allow you to organize and categorize data efficiently. By using the Collectors.groupingBy()
and Collectors.partitioningBy()
methods, you can easily categorize elements based on certain conditions, whether it’s by a property or a predicate. These operations help you leverage the full power of the Stream API, enabling more functional and expressive code.