Collectors to Map in Java

752 views

In this article, we’ll go through Collectors to Map method in Java, which is used to convert a stream to a map.

1. Collectors to Map overview

The Collectors to Map method is used to transform a Stream of items into a Map. Note that Collectors.toMap() is a terminal operation which means you cannot perform any stream methods after you call this method. Collectors.toMap() has 3 versions which we will explain with detailed examples in the following sections.

In the following section, we’ll go through some prerequisite knowledge regarding BinaryOperator<T> and Function<T, R> since we will use both of them when calling Collectors.toMap().

2. BinaryOperator and Function Overview

2.1 What is a BinaryOperator<T>

BinaryOperator is basically a function that accepts 2 parameters of the same type and returns a value that is the same type as the parameters.

2.2 What is a Function<T, R>

Function is a function that accepts a parameter of type T and returns a value of type R.

3. BinaryOperator and Function syntax

3.1 One-line Lambda

//For BinaryOperator
(a, b) -> c
// For Function
a -> c

Both a,b, and c must be of the same type when it comes to BinaryOperator while Function does not have any restriction unless it is stated explicitly.

Of course, c should be produced by combining a and b using some operations.

3.2 Method Reference

Object_class_::a_method

Assuming that we have a class A, and a method a_method inside this class, then method a_method should have one of the following signatures:

// For BinaryOperator
A a_method (A var){
  // combine this object with var to produce a new result
  return result;
}

// For Function
R a_method(){
 return result;
}

OR

// For BinaryOperator
A a_method (A var1, A var2){
  // combine var1 and var2 to produce a new result
  return result;
}

Note that Method Reference can always be replaced by one-line lambda while the opposite is not true.

3.3 Multiline Lambda

// For BinaryOperator
(variable1, variable2)  -> {
lines_of_code;
...
return result;
}

// For Function
var1 -> {
lines_of_code
....
return result;
}

The above will perform some operations before it returns the result. Note that this cannot be replaced by method reference without calling an external method.

4. Collectors toMap methods

Before we go through each method, consider that we have created this record:

private record Car(long id, String model, String brand, int horses, double price){};

and we have created a list of cars:

private static final List<Car> cars = new ArrayList<>(
        Arrays.asList(
                new Car(1L, "Astra", "Opel", 100, 18000d),
                new Car(2L, "Insignia", "Opel", 120, 22000d),
                new Car(3L, "Golf", "VW", 90, 17000d),
                new Car(4L, "Passat", "VW", 120, 25000d),
                new Car(5L, "Gallardo", "Lamborghini", 350, 120000d)
        ));

4.1 Collector toMap(Function keyMapper, Function valueMapper)

The first method that we’ll explain has the following signature:

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper)

It accepts:

  • A keyMapper function, that will be used to specify how the keys of the new Map will be created
  • A valueMapper function, that will be used to specify how the values of the new Map will be created

It returns a new HashMap with keys of type K and values of type U.

4.1.1 Converting a list of cars to a Map[Id, Car]

// Transform cars list to map (id, car_object)
Map<Long, Car> carsMap = cars.stream()
        .collect(Collectors.toMap(Car::id, Function.identity()));

carsMap.entrySet().forEach(System.out::println);

Collectors.toMap(Car::id, Function.identity()) will create a new Map<Long, Car> which will be filled with the respective values.

Car::id will return the id of the car and Function.identity() is just a function that returns whatever input is given(in our case, it will have a car object as an input)

The snippet above will print:

1=Car[id=1, model=Astra, brand=Opel, horses=100, price=18000.0]
2=Car[id=2, model=Insignia, brand=Opel, horses=120, price=22000.0]
3=Car[id=3, model=Golf, brand=VW, horses=90, price=17000.0]
4=Car[id=4, model=Passat, brand=VW, horses=120, price=25000.0]
5=Car[id=5, model=Gallardo, brand=Lamborghini, horses=350, price=120000.0]

4.1.2 Converting a list of cars to a Map[Id, Model]

Map<Long, String> idModelMap = cars.stream()
        .collect(Collectors.toMap(Car::id, Car::model));

idModelMap.entrySet().forEach(System.out::println);

Same as before, the id will be the key of every entry of the map but now the value will be the model of the car.

The snippet will just print the following:

1=Astra
2=Insignia
3=Golf
4=Passat
5=Gallardo

4.2 Collector toMap(Function keyMapper, Function valueMapper, BinaryOperator mergeFunction)

This method also accepts a BinaryOperator function, but how would that be useful?

Let’s say we would like to have a Map with the brand as the key, and the value as the model of the car but we would like to group the models of the car based on the key.

That’s where mergeFunction comes into play. Consider the example below:

Map<String, String> brandmodel = cars.stream()
        .collect(Collectors.toMap(
                Car::brand,
                Car::model,
                (model1, model2) -> model1 + ", " + model2 )
        );

brandmodel.entrySet().forEach(System.out::println);

Let’s see step by step how mergeFunction works:

  1. Car[Opel, Astra] -> Create new entry [Opel, Astra]
  2. Car[Opel, Insignia] -> Since key=Opel already exists, use the mergeFunction and set the value as "Astra, Insignia"
  3. Car[VW, Golf] -> Create a new entry [VW, Golf]
  4. Car[VW, Passat] -> Since key=VW already exists, use the mergeFunction and set the value as "Golf, Passat"
  5. Car[Lamborghini, Gallardo] -> Create new entry [Lamborghini, Gallardo]

So the result that will be printed is the following:

Lamborghini=Gallardo
VW=Golf, Passat
Opel=Astra, Insignia

4.3 Collector toMap(Function keyMapper, Function valueMapper, BinaryOperator mergeFunction, Supplier mapFactory)

The only difference with the previous method is that this one allows you to choose the implementation of the Map that will be created.

So let’s say that for some reason you would like the map that will be created, to have all of its keys sorted by ascending order. Then you would like the TreeMap implementation of the Map interface and not the default which is HashMap.

In other cases, where you might need your keys to maintain insertion order, you should choose the LinkedHashMap implementation of the Map interface.

Below you can find an example where TreeMap implementation is preferred:

TreeMap<String, String> brandmodel = cars.stream()
        .collect(Collectors.toMap(
                Car::brand,
                Car::model,
                (model1, model2) -> model1 + ", " + model2,
                TreeMap::new)
        );

brandmodel.entrySet().forEach(System.out::println);

Of course, the following will be printed:

Lamborghini=Gallardo
Opel=Astra, Insignia
VW=Golf, Passat

5. Conclusion

By now you should be able to convert a stream to a map by using the most suitable method for the needs of your program. You can find our source code on our GitHub page.

6. Sources

[1]: Collectors (Java SE 12 & JDK 12 ) – Oracle Help Center

Related Posts