groupingBy, partitioningBy, toMap and Joining
Four collectors do most of the real work in stream code. groupingBy is used to group elements by a key, like students by city. partitioningBy is used to split elements into exactly two groups, true and false, like passed and failed. toMap is used to build a map from each element. joining is used to build a string from elements. In this lesson, you will learn each collector in detail, including downstream collectors that compute something for every group.
Declaration
groupingBy(Function classifier)
groupingBy(Function classifier, Collector downstream)
groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
partitioningBy(Predicate predicate)
partitioningBy(Predicate predicate, Collector downstream)
toMap(Function keyMapper, Function valueMapper)
toMap(Function keyMapper, Function valueMapper, BinaryOperator merge)
joining(CharSequence delimiter)The examples below use this data:
record Student(String name, String city, int marks) { }
List<Student> students = List.of(
new Student("Navika", "Pune", 91),
new Student("Srihitha", "Delhi", 78),
new Student("Advitha", "Pune", 85),
new Student("Anamaneni", "Delhi", 38));groupingBy: Group by a Key
Let's see an example that groups students by city in several ways.
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static java.util.stream.Collectors.*;
public class Grouping {
record Student(String name, String city, int marks) { }
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Navika", "Pune", 91),
new Student("Srihitha", "Delhi", 78),
new Student("Advitha", "Pune", 85),
new Student("Anamaneni", "Delhi", 38));
Map<String, List<String>> names = students.stream()
.collect(groupingBy(Student::city, TreeMap::new,
mapping(Student::name, toList())));
System.out.println("Names by city: " + names);
Map<String, Long> count = students.stream()
.collect(groupingBy(Student::city, TreeMap::new, counting()));
System.out.println("Count by city: " + count);
Map<String, Double> avg = students.stream()
.collect(groupingBy(Student::city, TreeMap::new,
averagingInt(Student::marks)));
System.out.println("Average by city: " + avg);
}
}Names by city: {Delhi=[Srihitha, Anamaneni], Pune=[Navika, Advitha]}
Count by city: {Delhi=2, Pune=2}
Average by city: {Delhi=58.0, Pune=88.0}Explanation: groupingBy(Student::city, ...) puts each student in a group keyed by their city. TreeMap::new keeps the cities sorted. The downstream collector decides what each group becomes: mapping(Student::name, toList()) makes a list of names, counting() counts, and averagingInt averages the marks.
partitioningBy: Split into Two
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.*;
public class Partition {
record Student(String name, String city, int marks) { }
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Navika", "Pune", 91),
new Student("Srihitha", "Delhi", 78),
new Student("Advitha", "Pune", 85),
new Student("Anamaneni", "Delhi", 38));
Map<Boolean, List<String>> result = students.stream()
.collect(partitioningBy(s -> s.marks() >= 50,
mapping(Student::name, toList())));
System.out.println("Passed: " + result.get(true));
System.out.println("Failed: " + result.get(false));
}
}Passed: [Navika, Srihitha, Advitha]
Failed: [Anamaneni]Explanation: partitioningBy always produces a map with exactly two keys, true and false, even if one group is empty. It is a special case of grouping for yes-or-no questions.
toMap: Build a Map
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import static java.util.stream.Collectors.*;
public class ToMap {
record Student(String name, String city, int marks) { }
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Navika", "Pune", 91),
new Student("Srihitha", "Delhi", 78),
new Student("Advitha", "Pune", 85));
Map<String, Integer> marks = students.stream()
.collect(toMap(Student::name, Student::marks, (a, b) -> a,
TreeMap::new));
System.out.println(marks);
Map<String, Integer> bestByCity = students.stream()
.collect(toMap(Student::city, Student::marks, Math::max,
TreeMap::new));
System.out.println(bestByCity);
try {
students.stream().collect(toMap(Student::city, Student::name));
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
}
}
}{Advitha=85, Navika=91, Srihitha=78}
{Delhi=78, Pune=91}
Duplicate key Pune (attempted merging values Navika and Advitha)Explanation: toMap(keyFn, valueFn) builds one entry per element. When two elements produce the same key, you must give a merge function: Math::max keeps the higher mark for each city. Without one, a duplicate key throws IllegalStateException.
joining: Build a String
import java.util.List;
import static java.util.stream.Collectors.*;
public class Joining {
public static void main(String[] args) {
List<String> names = List.of("Navika", "Srihitha", "Advitha");
System.out.println(names.stream().collect(joining()));
System.out.println(names.stream().collect(joining(", ")));
System.out.println(names.stream()
.collect(joining(" | ", "Team: <", ">")));
}
}NavikaSrihithaAdvitha
Navika, Srihitha, Advitha
Team: <Navika | Srihitha | Advitha>Explanation: joining() glues strings together; joining(", ") adds a separator between them; the three-argument version also adds a prefix and a suffix.
Common Mistakes
Using toMap when keys can repeat, without a merge function. It throws IllegalStateException: Duplicate key.
Expecting groupingBy to give sorted keys. Pass TreeMap::new.
Using groupingBy with a boolean key. partitioningBy is clearer and always has both keys.
Interview Corner
What is the difference between groupingBy and partitioningBy?
groupingBy takes a classifier function and groups elements into a map with one key per distinct classifier result, which can be any type and any number of keys. partitioningBy takes a predicate and always produces a Map<Boolean, ...> with exactly two keys, true and false, even if one group is empty. Both accept a downstream collector to summarize each group.
What happens if toMap finds duplicate keys?
Without a merge function, Collectors.toMap throws IllegalStateException with a "Duplicate key" message. The three-argument version, toMap(keyMapper, valueMapper, mergeFunction), resolves collisions, for example (a, b) -> a to keep the first value or Integer::sum to add them.
Practice
- Easy: Group a list of words by their length, and print the map sorted by length.
- Harder: Group students by city, and for each city find the name of the student with the highest marks, using
groupingBy,collectingAndThenandmaxBy.
Key Takeaways
Was this tutorial helpful?