Using Streams with Map in Java 8 Using Streams with Map in Java 8

Page content

Java 8 stream is widely used feature to write code in functional programming way. In this tutorial, we’ll discuss how to use Streams for Map creation, iteration and sorting.

Let’s create a User class and List of users, which we will use in all the examples of this tutorial:-

 class User {
    Long id;
    String name;
    Integer age;

    // constructor, getters, setters, toString
}

List<User> users = List.of(new User(1L, "Andrew", 23),
            new User(2L, "Billy", 42),
            new User(3L, "David", 29),
            new User(4L, "Charlie", 30),
            new User(5L, "Andrew", 18),
            new User(6L, "Charlie", 19));

Please note that user id is unique and user name is not unique. You can see multiple users with name i.e. Andrew and Charlie. We have kept them intentionally to be used in our examples.

Create a Map

A Map is created, when you collect a stream of elements using either Collectors.toMap() or Collectors.groupingBy().

using Collectors.toMap()

Example 1: Map from streams having unique key

Let’s stream the List and collect it to a Map using Collectors.toMap(keyMapper, valueMapper) where key is unique id of user and value is name of the user which may duplicate:-

Map<Long, String> map = users.stream()
            .collect(Collectors.toMap(User::getId, User::getName));

// {1=Andrew, 2=Billy, 3=David, 4=Charlie, 5=Andrew, 6=Charlie}

Another example to create a Map using unique user id as key and user object as value:-

Map<Long, User> map = users.stream()
            .collect(Collectors.toMap(User::getId, Function.identity()));

//{1=User{id=1, name='Andrew', age=23},
// 2=User{id=2, name='Billy', age=42},
// 3=User{id=3, name='David', age=29}, 
// 4=User{id=4, name='Charlie', age=30}, 
// 5=User{id=5, name='Andrew', age=18}, 
// 6=User{id=6, name='Charlie', age=19}}

Example 2: Map from streams having duplicate key

In previous examples, we used user id as key which perfectly works because key of a Map should be unique.

duplicate key results error!

Let’s see what happens when we use user name as a key which is not unique and user age as value:-

Map<String, Integer> map = users.stream()
      .collect(Collectors.toMap(User::getName, User::getAge));

It throws IllegalStateException which is expected since key of a Map should be unique

java.lang.IllegalStateException: Duplicate key Andrew (attempted merging values 23 and 18)
mergeFunction to the rescue!

Java 8 Streams provide Collectors.toMap(keyMapper, valueMapper, mergeFunction) overloaded method where you can specify which value to consider when duplicate key issue occur.

Let’s collect a Map having user name as a key, merge function indicate that keep the old value for the same key:-

Map<String, Integer> idValueMap = users.stream()
    .collect(Collectors.toMap(User::getName, User::getAge, (oldValue, newValue) -> oldValue));

// {Billy=42, Andrew=23, Charlie=30, David=29}

We don’t see any error this time and a Map is created with unique user names. Duplicate user names are merged having age value whichever comes first in the list.


Example 3: ConcurrentHashMap, LinkedHashMap, and TreeMap from streams

Java 8 Streams provide Collectors.toMap(keyMapper, valueMapper, mergeFunction, mapFactory) overloaded method where you can specify the type using mapFactory to return ConcurrentHashMap, LinkedHashMap or TreeMap.

 Map<String, Integer> concurrentHashMap = users.stream()
            .collect(Collectors.toMap(User::getName, User::getAge, (o1, o2) -> o1, ConcurrentHashMap::new));

 Map<String, Integer> linkedHashMap = users.stream()
            .collect(Collectors.toMap(User::getName, User::getAge, (o1, o2) -> o1, LinkedHashMap::new));

 Map<String, Integer> treeMap = users.stream()
            .collect(Collectors.toMap(User::getName, User::getAge, (o1, o2) -> o1, TreeMap::new));          

using Collectors.groupingBy()

A Map is returned, when you group a stream of objects using Collectors.groupingBy(keyMapper, valueMapper). You can specify the key and value mapping function. Specifying the value mapping function is optional and it returns a List by default.

Example 1: Group the stream by Key

Let’s group the stream of user object by name using Collectors.groupingBy(keyMapper) which returns a Map having key as user name and value as List of user object matching that key:-

Map<String, List<User>> groupByName = users.stream()
            .collect(Collectors.groupingBy(User::getName));

// {Billy=[User{id=2, name='Billy', age=42}], 
//  Andrew=[User{id=1, name='Andrew', age=23}, User{id=5, name='Andrew', age=18}],
//  Charlie=[User{id=4, name='Charlie', age=30}, User{id=6, name='Charlie', age=19}], 
//  David=[User{id=3, name='David', age=29}]}

Example2: Group the stream by key and value

This time we will specify both key and value mapping function in Collectors.groupingBy(keyMapper, valueMapper). For example:-

Count the users having the same name, where key is user name and value is count:-

 Map<String, Long> countByName = users.stream()
            .collect(Collectors.groupingBy(User::getName, Collectors.counting()));

// {Billy=1, Andrew=2, Charlie=2, David=1}

Sum of the age of users having the same name, where key is user name and value is sum of age:-

Map<String, Integer> sumAgeByName = users.stream()
        .collect(Collectors.groupingBy(User::getName, Collectors.summingInt(User::getAge)));

// {Billy=42, Andrew=41, Charlie=49, David=29}

Iterate through Map

There are three ways to iterate through a Map:-

Using keySet()

The method keySet() is applied on Map<K,V> which returns Set<K> and can be streamed to iterate through keys:-

users.stream()
        .collect(Collectors.toMap(User::getId, User::getName))
        .keySet()
        .stream()
        .forEach(System.out::print);
      
// Prints "1 2 3 4 5"

Using values()

The method values() is applied on Map<K,V> which returns Collection<V> and can be streamed to iterate through values:-

users.stream()
        .collect(Collectors.toMap(User::getId, User::getName))
        .values()
        .stream()
        .forEach(System.out::print);

// Prints "Andrew Billy David Charlie Andrew Charlie"

Using entrySet()

The method entrySet() is applied on Map<K,V> which returns Set<Map.Entry<K, V>> and can be streamed to iterate through entries (keys & values):-

users.stream()
        .collect(Collectors.toMap(User::getId, User::getName))
        .entrySet()
        .stream()
        .forEach(System.out::print);

// Prints "1=Andrew 2=Billy 3=David 4=Charlie 5=Andrew 6=Charlie"

Sort the Map

By Key

We can sort the Map by key using streams with built-in comparator Map.Entry.comparingByKey()

Sort the Map by key in alphabetical order and print it:-

users.stream()
        .collect(Collectors.toMap(User::getName, User::getAge, (o1,o2) -> o1))
        .entrySet()
        .stream()
        .sorted(Map.Entry.comparingByKey())
        .forEach(System.out::println);

// Andrew=23
// Billy=42
// Charlie=30
// David=29

Sort the Map by key in reverse alphabetical order and collect to LinkedHashMap:-

Map<String, Integer> sortByKeyReverse = users.stream()
        .collect(Collectors.toMap(User::getName, User::getAge, (o1,o2) -> o1))
        .entrySet()
        .stream()
        .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (o1,o2) -> o1, LinkedHashMap::new));

// {David=29, Charlie=30, Billy=42, Andrew=23}

By Value

We can sort the Map by value using streams with built-in comparator Map.Entry.comparingByValue()

Sort the Map by value in ascending order and print it:-

users.stream()
        .collect(Collectors.toMap(User::getName, User::getAge, (o1,o2) -> o1))
        .entrySet()
        .stream()
        .sorted(Map.Entry.comparingByValue()).forEach(System.out::println);

// Andrew=23
// David=29
// Charlie=30
// Billy=42

Sort the Map by value in descending order and collect to LinkedHashMap:-

Map<String, Integer> sortByValueReverse = users.stream()
        .collect(Collectors.toMap(User::getName, User::getAge, (o1,o2) -> o1))
        .entrySet()
        .stream()
        .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (o1,o2) -> o1, LinkedHashMap::new));

// {Billy=42, Charlie=30, David=29, Andrew=23}

By Both Key and Value

We can sort the Map by using both key and value one after another using thenComparing()

Sort the Map by value in alphabetical order and then sort by key in descending order and collect to LinkedHashMap:-

Comparator<Map.Entry<Long, String>> valueComparator = Map.Entry.comparingByValue();
Comparator<Map.Entry<Long, String>> keyComparator = Map.Entry.comparingByKey(Comparator.reverseOrder());

Map<Long, String> sortByValueThenKey = users.stream()
    .collect(Collectors.toMap(User::getId, User::getName))
    .entrySet()
    .stream()
    .sorted(valueComparator.thenComparing(keyComparator))
    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (o1, o2) -> o1, LinkedHashMap::new));

// {5=Andrew, 1=Andrew, 2=Billy, 6=Charlie, 4=Charlie, 3=David}

We got a sorted Map having user names sorted in alphabetical order first and then keys are sorted in descending order of user id.