Collectors & Grouping
The terminal operation collect gathers a stream's elements into a result, and the Collectors class provides ready-made recipes: build maps, join strings, group by a key, count, sum and partition. Together they turn many-line reporting loops into a single expression.
Collecting into collections
import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<String> cities = List.of("Lisbon", "Berlin", "Lisbon", "Dublin"); Set<String> unique = cities.stream().collect(Collectors.toSet()); TreeSet<String> sorted = cities.stream().collect(Collectors.toCollection(TreeSet::new)); System.out.println(unique.size()); System.out.println(sorted); } }
3 [Berlin, Dublin, Lisbon]
For lists, stream.toList() (Java 16+) is the shortest option and returns an unmodifiable list.
Joining strings
import java.util.List; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<String> tags = List.of("java", "streams", "collectors"); System.out.println(tags.stream().collect(Collectors.joining(", "))); System.out.println(tags.stream().map(t -> "#" + t).collect(Collectors.joining(" ", "Tags: ", "."))); } }
java, streams, collectors Tags: #java #streams #collectors.
toMap
import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; record Product(String code, String name, double price) {} public class Main { public static void main(String[] args) { List<Product> products = List.of(new Product("P1", "Mug", 12.5), new Product("P2", "Lamp", 34.0)); Map<String, Double> priceByCode = products.stream() .collect(Collectors.toMap(Product::code, Product::price)); System.out.println(priceByCode.get("P2")); List<String> words = List.of("apple", "avocado", "banana"); Map<Character, String> firstByLetter = words.stream() .collect(Collectors.toMap(w -> w.charAt(0), w -> w, (a, b) -> a + "+" + b, TreeMap::new)); System.out.println(firstByLetter); } }
34.0
{a=apple+avocado, b=banana}toMap(keyFn, valueFn) throws an exception on duplicate keys; the third argument says how to merge them, and the fourth chooses the map type.
groupingBy
import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; record Employee(String name, String department, int salary) {} public class Main { public static void main(String[] args) { List<Employee> staff = List.of( new Employee("Rafael", "Sales", 82000), new Employee("Lucas", "Sales", 52000), new Employee("Mei", "Engineering", 98000), new Employee("Jonas", "Engineering", 85000), new Employee("Amara", "Engineering", 68000), new Employee("Ethan", "Support", 60000) ); Map<String, List<String>> names = staff.stream().collect(Collectors.groupingBy( Employee::department, TreeMap::new, Collectors.mapping(Employee::name, Collectors.toList()))); System.out.println(names); Map<String, Long> headcount = staff.stream().collect(Collectors.groupingBy( Employee::department, TreeMap::new, Collectors.counting())); System.out.println(headcount); Map<String, Double> avgSalary = staff.stream().collect(Collectors.groupingBy( Employee::department, TreeMap::new, Collectors.averagingInt(Employee::salary))); System.out.println(avgSalary); Map<String, Integer> payroll = staff.stream().collect(Collectors.groupingBy( Employee::department, TreeMap::new, Collectors.summingInt(Employee::salary))); System.out.println(payroll); } }
{Engineering=[Mei, Jonas, Amara], Sales=[Rafael, Lucas], Support=[Ethan]}
{Engineering=3, Sales=2, Support=1}
{Engineering=83666.66666666667, Sales=67000.0, Support=60000.0}
{Engineering=251000, Sales=134000, Support=60000}groupingBy(key) is SQL's GROUP BY for collections. A second collector — the downstream collector — decides what to compute for each group: a list, a count, an average, a sum. Passing TreeMap::new keeps the groups sorted.
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> scores = List.of(45, 82, 67, 30, 91, 58); Map<Boolean, List<Integer>> passed = scores.stream() .collect(Collectors.partitioningBy(s -> s >= 50)); System.out.println("Passed: " + passed.get(true)); System.out.println("Failed: " + passed.get(false)); } }
Passed: [82, 67, 91, 58] Failed: [45, 30]
Finding the top item per group
import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; import java.util.stream.Collectors; record Employee(String name, String department, int salary) {} public class Main { public static void main(String[] args) { List<Employee> staff = List.of( new Employee("Rafael", "Sales", 82000), new Employee("Lucas", "Sales", 52000), new Employee("Mei", "Engineering", 98000), new Employee("Jonas", "Engineering", 85000) ); Map<String, Optional<Employee>> top = staff.stream().collect(Collectors.groupingBy( Employee::department, TreeMap::new, Collectors.maxBy(Comparator.comparingInt(Employee::salary)))); top.forEach((dept, e) -> System.out.println(dept + ": " + e.map(Employee::name).orElse("-"))); } }
Engineering: Mei Sales: Rafael
Try it yourself
Count words by length, then list the words of each length.
3 letters: [sun, sky] 4 letters: [moon, star, nova] 5 letters: [comet, orbit]