Collectors.mapping()

Transforms each element before a downstream collector sees it, so groups hold just the field you need instead of whole objects.

Signature
public static <T, U, A, R> Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper, Collector<? super U, A, R> downstream)
  • Added in Java Java 8

Parameters

ParameterTypeDescription
$mapperFunction<? super T, ? extends U>Transforms each input element.
$downstreamCollector<? super U, A, R>Receives the transformed values, such as toList(), toSet() or joining(", ").

Return value

Collector<T, ?, R>A collector that applies the mapper to each element and passes the result to the downstream collector, producing whatever the downstream collector produces.

Errors

Building the collector throws NullPointerException for a null downstream; a null mapper only fails, with NullPointerException, once an element is collected.

Collectors.mapping applies a function to each element and hands the result to another collector. It is the collector counterpart of Stream.map, and it exists for one main reason: inside groupingBy or partitioningBy you cannot call map per group, so you transform the elements in the downstream collector instead.

// Without mapping: team -> List<User>
// With mapping:    team -> List<String> of emails
Map<String, List<String>> emailsByTeam = users.stream()
    .collect(Collectors.groupingBy(User::team,
        Collectors.mapping(User::email, Collectors.toList())));

That shape is what an API response usually needs: a list of ids or names per key, not full entities. Carrying whole objects into the map and converting later works, but it keeps more in memory and pushes the conversion into every caller.

Pick the downstream collector for the shape you want

  • mapping(f, toList()): values per group, in encounter order.
  • mapping(f, toSet()) or toCollection(TreeSet::new): distinct values per group.
  • mapping(f, joining(", ")): one string per group, ready for a report or an email.

Its siblings: filtering and flatMapping

Java 9 added Collectors.filtering(predicate, downstream) and Collectors.flatMapping(function, downstream), which work the same way. filtering differs from a filter before groupingBy in one important way: a group whose elements are all filtered out still appears with an empty result, while filtering first removes the key entirely. Use filtering when every key must be present, for example every team in a dashboard even if it has no admins. flatMapping turns each element into a stream and collects all of them, such as every role held by anyone in a team.

When not to use it

At the top level, stream.collect(mapping(f, toList())) is just stream.map(f).toList() written the long way; use map. mapping also has no ordering of its own: the order you see comes from the stream and the downstream collector. Another place it pays off is in your own reusable collectors: a method that returns mapping(Order::id, joining(",")) can be passed to any groupingBy.

Example

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

When to use it

  • Returning team names mapped to member emails, not full user entities, from an admin endpoint.
  • Listing every warehouse with the SKUs it is out of, keeping warehouses with nothing missing via filtering.
  • Building one comma-separated line of order ids per customer for a nightly reconciliation email.

Frequently asked questions

How do I get a list of one field per group with groupingBy?

Use Collectors.mapping as the downstream collector: groupingBy(User::team, Collectors.mapping(User::email, Collectors.toList())). Each key then maps to a list of emails instead of a list of users.

What is the difference between Collectors.mapping and Stream.map?

They do the same transformation. Stream.map works on the whole stream; Collectors.mapping works inside a collector, which is what you need per group in groupingBy or partitioningBy. At the top level, prefer map.

What is the difference between Collectors.filtering and filter before groupingBy?

filter before groupingBy removes elements before keys are computed, so a key whose elements are all removed disappears. Collectors.filtering (Java 9) filters inside each group, so the key stays with an empty result.

More examples

filtering keeps empty groups

Filtering before grouping removes the ops team entirely because none of its members is an admin. Collectors.filtering (Java 9) runs inside each group, so ops stays with an empty list, which a page listing every team needs.

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

record User(String name, String team, boolean admin) {}

void main() {
    List<User> users = List.of(
        new User("Ada", "api", true), new User("Bob", "ops", false),
        new User("Cy", "api", false));

    Map<String, List<String>> filterFirst = users.stream()
        .filter(User::admin)
        .collect(Collectors.groupingBy(User::team, TreeMap::new,
            Collectors.mapping(User::name, Collectors.toList())));

    Map<String, List<String>> filtering = users.stream()
        .collect(Collectors.groupingBy(User::team, TreeMap::new,
            Collectors.filtering(User::admin,
                Collectors.mapping(User::name, Collectors.toList()))));

    IO.println("filter first: " + filterFirst);
    IO.println("filtering:    " + filtering);
}
// Output:
// filter first: {api=[Ada]}
// filtering:    {api=[Ada], ops=[]}

flatMapping a list field per group

mapping(User::roles, ...) would give a set of lists. flatMapping (Java 9) streams each user's roles and collects the individual values, here into a sorted set so duplicates collapse.

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

record User(String name, String team, List<String> roles) {}

void main() {
    List<User> users = List.of(
        new User("Ada", "api", List.of("deploy", "read")),
        new User("Cy", "api", List.of("read", "billing")),
        new User("Bob", "ops", List.of("deploy")));

    Map<String, Set<String>> rolesByTeam = users.stream()
        .collect(Collectors.groupingBy(User::team, TreeMap::new,
            Collectors.flatMapping(u -> u.roles().stream(),
                Collectors.toCollection(TreeSet::new))));
    IO.println(rolesByTeam);
}
// Output:
// {api=[billing, deploy, read], ops=[deploy]}

A reusable collector built on mapping

Because mapping returns an ordinary Collector, it can be wrapped in a method and reused with any grouping. That keeps formatting rules, such as how ids are listed in a report, in one place.

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

record Order(String id, String customer, String status) {}

static <T> Collector<T, ?, String> csvOf(Function<? super T, String> field) {
    return Collectors.mapping(field, Collectors.joining(","));
}

void main() {
    List<Order> orders = List.of(
        new Order("o-1", "ada", "PAID"), new Order("o-2", "bob", "NEW"),
        new Order("o-3", "ada", "NEW"));

    Map<String, String> idsByCustomer = orders.stream()
        .collect(Collectors.groupingBy(Order::customer, TreeMap::new, csvOf(Order::id)));
    Map<String, String> idsByStatus = orders.stream()
        .collect(Collectors.groupingBy(Order::status, TreeMap::new, csvOf(Order::id)));

    IO.println(idsByCustomer);
    IO.println(idsByStatus);
}
// Output:
// {ada=o-1,o-3, bob=o-2}
// {NEW=o-2,o-3, PAID=o-1}

Discussion

  • Be the first to comment on this lesson.