Java 8 Collectors.toMap Tutorial with Examples

Introduction

This tutorial explains Collectors.toMap(), the Java 8 collector that turns a stream into a Map. We will start with how toMap() builds a map from a key extractor and a value extractor, and its three overloaded versions. We will then run an example covering the basic form, Function.identity(), what happens with duplicate keys, merge functions for combining values, choosing a TreeMap or LinkedHashMap, and the null value trap. Lastly, we will compare toMap() with groupingBy() and look at toConcurrentMap().

(This tutorial is part of the Java 8 Collectors series. For the concept of a collector, see the Collector basics tutorialClick to read: Collector basics tutorial.)

How toMap() builds a map

For each element of the stream, toMap() calls a key mapper to get the map key and a value mapper to get the value, both FunctionsClick to read: Functions, and puts the pair into the map. The interesting question is what happens when a second element produces a key that is already in the map.
How Java 8 Collectors.toMap works - key mapper, value mapper, duplicate keys, merge function and map supplier

The three toMap() methods

Definition of the toMap() overloads in java.util.stream.Collectors -
toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper) AND toMap(keyMapper, valueMapper, BinaryOperator<U> mergeFunction) AND toMap(keyMapper, valueMapper, mergeFunction, Supplier<M> mapSupplier)
Where,
  • keyMapper and valueMapper produce the key and the value from each element.
  • mergeFunction is a BinaryOperatorClick to read tutorial: Java 8 UnaryOperator and BinaryOperator Tutorial called with the existing value and the new value whenever a key repeats; its result is stored. Without it, a repeated key throws an IllegalStateException.
  • mapSupplier creates the map to fill, such as TreeMap::new. Without it, the result is a HashMap, whose iteration order is not defined.

Java 8 code example of Collectors.toMap()

The examples use a small Employee class with an id, a name, a department and a salary. Its toString() returns the name, to keep the output short.
Employee.java
//Employee.java
public class Employee {
  private final String id;
  private final String name;
  private final String department;
  private final double salary;

  public Employee(String id, String name, String department, double salary) {
    this.id = id;
    this.name = name;
    this.department = department;
    this.salary = salary;
  }

  public String getId() { return id; }
  public String getName() { return name; }
  public String getDepartment() { return department; }
  public double getSalary() { return salary; }

  @Override
  public String toString() { return name; }
}
Java 8 code showing Collectors.toMap with merge functions and map suppliers
//ToMapExamples.java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.TreeMap;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;

public class ToMapExamples {

  public static void main(String[] args) {
    List<Employee> staff = Arrays.asList(
        new Employee("E-1003", "Ethan Hardy", "Sales", 6300),
        new Employee("E-1001", "Tom Jones", "Finance", 5200),
        new Employee("E-1004", "Nancy Smith", "Sales", 5800),
        new Employee("E-1002", "Harry Major", "Finance", 4100),
        new Employee("E-1005", "Deborah Sprightly", "IT", 7000));

    // 1. Key and value extractors
    Map<String, String> namesById = staff.stream()
        .collect(Collectors.toMap(Employee::getId, Employee::getName));
    System.out.println("1. names by id: " + namesById);

    // 2. Function.identity() keeps the whole object as the value
    Map<String, Employee> byId = staff.stream()
        .collect(Collectors.toMap(Employee::getId, Function.identity()));
    System.out.println("2. E-1004 is " + byId.get("E-1004").getName());

    // 3. Duplicate keys throw an IllegalStateException...
    try {
      staff.stream()
          .collect(Collectors.toMap(Employee::getDepartment, Employee::getName));
    } catch (IllegalStateException e) {
      System.out.println("3. duplicate department key -> IllegalStateException");
    }

    // 4. ...unless a merge function says how to combine the values
    Map<String, Double> payrollByDept = staff.stream()
        .collect(Collectors.toMap(Employee::getDepartment, Employee::getSalary,
            Double::sum));
    System.out.println("4. payroll by department: " + payrollByDept);

    Map<String, Employee> topEarnerByDept = staff.stream()
        .collect(Collectors.toMap(Employee::getDepartment, Function.identity(),
            BinaryOperator.maxBy(Comparator.comparingDouble(Employee::getSalary))));
    System.out.println("   top earner by department: " + topEarnerByDept);

    // 5. A map supplier chooses the Map implementation
    TreeMap<String, Double> sortedById = staff.stream()
        .collect(Collectors.toMap(Employee::getId, Employee::getSalary,
            (a, b) -> a, TreeMap::new));
    System.out.println("5. TreeMap, sorted by key: " + sortedById);

    LinkedHashMap<String, Double> inStreamOrder = staff.stream()
        .collect(Collectors.toMap(Employee::getName, Employee::getSalary,
            (a, b) -> a, LinkedHashMap::new));
    System.out.println("   LinkedHashMap, stream order: " + inStreamOrder.keySet());

    // 6. A null value throws a NullPointerException
    try {
      Arrays.asList("Tom Jones", "Harry Major").stream()
          .collect(Collectors.toMap(Function.identity(), name -> (String) null));
    } catch (NullPointerException e) {
      System.out.println("6. null value -> NullPointerException");
    }
  }
}
 OUTPUT of the above code
1. names by id: {E-1005=Deborah Sprightly, E-1004=Nancy Smith, E-1003=Ethan Hardy, E-1002=Harry Major, E-1001=Tom Jones}
2. E-1004 is Nancy Smith
3. duplicate department key -> IllegalStateException
4. payroll by department: {Sales=12100.0, Finance=9300.0, IT=7000.0}
   top earner by department: {Sales=Ethan Hardy, Finance=Tom Jones, IT=Deborah Sprightly}
5. TreeMap, sorted by key: {E-1001=5200.0, E-1002=4100.0, E-1003=6300.0, E-1004=5800.0, E-1005=7000.0}
   LinkedHashMap, stream order: [Ethan Hardy, Tom Jones, Nancy Smith, Harry Major, Deborah Sprightly]
6. null value -> NullPointerException

Explanation of the code

  • Part 1: the ids are unique, so the two-argument toMap() works. The result is a HashMap, so the order of the printed entries is not the order of the stream - and may differ between Java versions.
  • Part 2: Function.identity() is a function that returns its argument, so each employee becomes the value under its own id - a quick way to build a lookup table.
  • Part 3: two employees work in Sales and two in Finance, so using the department as the key throws IllegalStateException. In Java 8 the message wrongly shows a value instead of the duplicate key; Java 9 fixed it to name the key and both values.
  • Part 4: with Double::sum as the merge function, salaries in the same department are added, giving the payroll per department. The second map keeps the better-paid employee per department, using BinaryOperator.maxBy() with a ComparatorClick to read: Comparator.
  • Part 5: the four-argument form fills a TreeMap, sorted by key, or a LinkedHashMap, which keeps the order in which keys were first seen in the stream. A merge function must be given even when keys are unique; (a, b) -> a is the usual choice.
  • Part 6: toMap() rejects null values with a NullPointerException, because it is built on Map.merge(), which does not accept null. Filter out nulls first, or wrap values in an OptionalClick to read: Optional.

toMap() or groupingBy()?

Both build a map, but they answer different questions -
  • toMap() gives one value per key. Use it when keys are unique, or when you can say how to combine two values into one.
  • groupingBy() gives a collection of elements per key, such as Map<String, List<Employee>>, and can then summarise each group with a downstream collector - see the groupingBy() tutorialClick to read: groupingBy() tutorial.
  • Part 4's payroll could also be written as groupingBy(Employee::getDepartment, summingDouble(Employee::getSalary)). For sums and counts, groupingBy() usually reads better; for "one item per key" lookups and custom merges, toMap() does.

toConcurrentMap() for parallel streams

Collectors.toConcurrentMap() has the same three overloads, but collects into a ConcurrentHashMap (or the ConcurrentMap from the supplier). On a parallel streamClick to read tutorial: Java 8 Parallel Streams Tutorial with Examples, all threads then insert into one shared concurrent map instead of building partial maps and merging them, which can be faster for large streams. It is an unordered collector, so the merge function may see values in any order.

Summary

In this tutorial we first saw how Collectors.toMap() turns each element into a key and a value, and its three overloads. Next we ran an example building a map by id, a lookup table with Function.identity(), the IllegalStateException for duplicate keys, merge functions that add salaries or keep the top earner, TreeMap and LinkedHashMap results, and the NullPointerException for null values. Lastly, we compared toMap() with groupingBy() and looked at toConcurrentMap().