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

Page content

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

Let’s create a User class and List of users, which we will use in 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 id is unique but name is not unique. You can see multiple users having similar names i.e. Andrew and Charlie. We have kept them intentionally to handle duplicate scenarios in the 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 keys

Let’s stream the List and collect it to a Map using Collectors.toMap(keyMapper, valueMapper). We used id as key and name as value in the collector. The generated map has all the unique keys but value may contain duplicates, which is perfectly fine in the case of Map.

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 of creating a Map using a unique 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}}

Notice the use of Function.identity() method to collect the object itself.


Example 2: Map from streams having a duplicate key

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

duplicate key results error!

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

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

It throws IllegalStateException which is expected since the 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 issues occur.

Let’s collect a Map having user name as a key, The merge function indicates 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 objects by name using Collectors.groupingBy(keyMapper) which returns a Map where key is user name and value is List of objects of the users having the same name:-

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 functions in Collectors.groupingBy(keyMapper, valueMapper). For example:-

Create a map where key is user name and value is count of the users having the same name:-

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

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

Create a map where key is user name and value is sum of age of users having the same name:-

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.