Collectors.groupingBy()

Groups stream elements into a Map by a key, with a downstream collector per group and a choice of map type.

Signature
public static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier, Supplier<M> mapFactory, Collector<? super T, A, D> downstream)
  • Added in Java Java 8

Parameters

ParameterTypeDescription
$classifierFunction<? super T, ? extends K>Computes the group key for each element. It must not return null.
$mapFactoryoptionalSupplier<M>Creates the empty result map, such as TreeMap::new or () -> new EnumMap<>(Status.class). Without it the map type is unspecified (a HashMap in current JDKs).
$downstreamoptionalCollector<? super T, A, D>= Collectors.toList()Reduces the elements of each group, such as counting() or mapping(...).

Return value

Collector<T, ?, M>A collector that produces a Map from each key to the result of the downstream collector for the elements with that key. With one argument the result is Map<K, List<T>>.

Errors

The collect operation throws NullPointerException with the message 'element cannot be mapped to a null key' when the classifier returns null.

Collectors.groupingBy is the stream form of SQL's GROUP BY. A classifier function computes a key for each element, and the result is a Map from each key to what the elements with that key were reduced to. With only the classifier, each key maps to a List of its elements.

Map<String, List<Order>> byCustomer = orders.stream()
    .collect(Collectors.groupingBy(Order::customer));

Three overloads

  • groupingBy(classifier): key to List<T>.
  • groupingBy(classifier, downstream): key to whatever the downstream collector produces, such as counting(), summingInt(...) or mapping(...).
  • groupingBy(classifier, mapFactory, downstream): the same, into a map you choose.

Note the argument order of the last one: the map factory comes before the downstream collector, so there is no two-argument form that takes only a factory. Pass Collectors.toList() explicitly when you want lists in a TreeMap.

Choose the map type when someone will see it

The default map is a HashMap, so key order in a JSON response, a report or a test assertion is not something to rely on. Use TreeMap::new for sorted keys, or () -> new EnumMap<>(Status.class) when the keys are an enum and you want declaration order.

Mistakes people make

Groups only exist for keys that occurred. If no order was cancelled, there is no CANCELLED key, and get returns null; use getOrDefault, or pre-fill the keys if a dashboard must show zeros. A classifier that returns null, for example from an optional column, throws NullPointerException with the message element cannot be mapped to a null key; map missing values to a placeholder such as "unknown".

Grouping happens in memory. For a large table, a GROUP BY in the database sends back a few rows of totals instead of every row, so group in Java when the data is already loaded or comes from somewhere other than SQL.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Counting API requests per endpoint and status code from an access log for a health dashboard.
  • Grouping order lines by order id after a single join query, to build nested order DTOs for a JSON response.
  • Bucketing tickets by assignee in a support tool so each person's queue can be rendered in one pass.

Frequently asked questions

How do I group a list of objects by a field in Java?

Stream the list and collect with Collectors.groupingBy(Item::field). The result is a Map from each field value to a List of the items with that value. Add a downstream collector such as Collectors.counting() to get a count per key instead of the list.

Why are the keys from groupingBy in a random order?

The default result is a HashMap, which does not keep any order. Pass a map factory as the middle argument: groupingBy(f, TreeMap::new, Collectors.toList()) for sorted keys, or LinkedHashMap::new for the order in which keys were first met.

Can Collectors.groupingBy handle null keys?

No. If the classifier returns null, collect throws NullPointerException with the message 'element cannot be mapped to a null key'. Map null to a placeholder value in the classifier, or filter those elements out first.

More examples

Grouping by two fields

A groupingBy used as the downstream collector of another produces a nested map, here service then status code then count. For more than two levels, a record key such as groupingBy(r -> new Key(r.service(), r.status())) is easier to read and to serialise.

Example · java
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

record Request(String service, int status) {}

void main() {
    List<Request> log = List.of(
        new Request("orders", 200), new Request("orders", 500),
        new Request("users", 200), new Request("orders", 200),
        new Request("users", 404));

    Map<String, Map<Integer, Long>> counts = log.stream()
        .collect(Collectors.groupingBy(Request::service, TreeMap::new,
            Collectors.groupingBy(Request::status, TreeMap::new, Collectors.counting())));
    IO.println(counts);
    IO.println(counts.get("orders").get(500));
}
// Output:
// {orders={200=2, 500=1}, users={200=1, 404=1}}
// 1

Null keys and how to avoid them

groupingBy refuses null keys with a clear message, so optional columns need a placeholder key. Choosing the placeholder explicitly also makes the report honest about how many rows had no value.

Example · java
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

record Signup(String email, String country) {}

void main() {
    List<Signup> signups = List.of(
        new Signup("[email protected]", "NZ"),
        new Signup("[email protected]", null),
        new Signup("[email protected]", "NZ"));

    try {
        signups.stream().collect(Collectors.groupingBy(Signup::country));
    } catch (NullPointerException e) {
        IO.println(e.getMessage());
    }

    Map<String, Long> byCountry = signups.stream()
        .collect(Collectors.groupingBy(
            s -> s.country() == null ? "unknown" : s.country(),
            TreeMap::new, Collectors.counting()));
    IO.println(byCountry);
}
// Output:
// element cannot be mapped to a null key
// {NZ=2, unknown=1}

The largest order per customer

maxBy yields an Optional per group because a collector cannot know a group is never empty. collectingAndThen unwraps it; orElseThrow is safe here because groupingBy only creates a group when it has at least one element.

Example · java
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 Order(String id, String customer, int cents) {}

void main() {
    List<Order> orders = List.of(
        new Order("o-1", "ada", 2500), new Order("o-2", "bob", 1200),
        new Order("o-3", "ada", 4000), new Order("o-4", "bob", 300));

    Map<String, String> biggest = orders.stream()
        .collect(Collectors.groupingBy(Order::customer, TreeMap::new,
            Collectors.collectingAndThen(
                Collectors.maxBy(Comparator.comparingInt(Order::cents)),
                max -> max.map(Order::id).orElseThrow())));
    IO.println(biggest);
}
// Output:
// {ada=o-3, bob=o-2}

Discussion

  • Be the first to comment on this lesson.