1. Introduction
In this tutorial, weβll discuss some examples of how to use Java Streams to work with Maps. Itβs worth noting that some of these exercises could be solved using a bidirectional Map data structure, but weβre interested here in a functional approach.
First, weβll explain the basic idea weβll be using to work with Maps and Streams. Then weβll present a couple of different problems related to Maps and their concrete solutions using Streams.
Further reading:
Learn different techniques for merging maps in Java 8
Learn how to use the toMap() method of the Collectors class.
The article is an example-heavy introduction of the possibilities and operations offered by the Java 8 Stream API.
2. Basic Idea
The principal thing to notice is that Streams are sequences of elements which can be easily obtained from a Collection.
Maps have a different structure, with a mapping from keys to values, without sequence. However, this doesnβt mean that we canβt convert a Map structure into different sequences which then allow us to work in a natural way with the Stream API.
Letβs see ways of obtaining different Collections from a Map, which we can then pivot into a Stream:
Map<String, Integer> someMap = new HashMap<>();
We can obtain a set of key-value pairs:
Set<Map.Entry<String, Integer>> entries = someMap.entrySet();
We can also get the key set associated with the Map:
Set<String> keySet = someMap.keySet();
Or we could work directly with the set of values:
Collection<Integer> values = someMap.values();
These each give us an entry point to process those collections by obtaining streams from them:
Stream<Map.Entry<String, Integer>> entriesStream = entries.stream();
Stream<Integer> valuesStream = values.stream();
Stream<String> keysStream = keySet.stream();
3. Getting a Mapβs Keys Using Streams
3.1. Input Data
Letβs assume we have a Map:
Map<String, String> books = new HashMap<>();
books.put(
"978-0201633610", "Design patterns : elements of reusable object-oriented software");
books.put(
"978-1617291999", "Java 8 in Action: Lambdas, Streams, and functional-style programming");
books.put("978-0134685991", "Effective Java");
We are interested in finding the ISBN for the book titled βEffective Java.β
3.2. Retrieving a Match
Since the book title could not exist in our Map, we want to be able to indicate that there is no associated ISBN for it. We can use an Optional to express that:
Letβs assume for this example that we are interested in any key for a book matching that title:
Optional<String> optionalIsbn = books.entrySet().stream()
.filter(e -> "Effective Java".equals(e.getValue()))
.map(Map.Entry::getKey)
.findFirst();
assertEquals("978-0134685991", optionalIsbn.get());
Letβs analyze the code. First, we obtain the entrySet from the Map, as we saw previously.
We only want to consider the entries with βEffective Javaβ as the title, so the first intermediate operation will be a filter.
Weβre not interested in the whole Map entry, but in the key of each entry. So the next chained intermediate operation does just that: it is a map operation that will generate a new stream as output, which will contain only the keys for the entries that matched the title we were looking for.
As we only want one result, we can apply the findFirst() terminal operation, which will provide the initial value in the Stream as an Optional object.
Letβs see a case in which a title does not exist:
Optional<String> optionalIsbn = books.entrySet().stream()
.filter(e -> "Non Existent Title".equals(e.getValue()))
.map(Map.Entry::getKey).findFirst();
assertEquals(false, optionalIsbn.isPresent());
3.3. Retrieving Multiple Results
Now letβs change the problem to see how we could deal with returning multiple results instead of one.
To have multiple results returned, letβs add the following book to our Map:
books.put("978-0321356680", "Effective Java: Second Edition");
So now if we look for all books that start with βEffective Java,β weβll get more than one result back:
List<String> isbnCodes = books.entrySet().stream()
.filter(e -> e.getValue().startsWith("Effective Java"))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
assertTrue(isbnCodes.contains("978-0321356680"));
assertTrue(isbnCodes.contains("978-0134685991"));
What we have done in this case is to replace the filter condition to verify if the value in the Map starts with βEffective Javaβ instead of comparing for String equality.
This time we collect the results, instead of just picking the first, and put the matches into a List.
4. Getting a Mapβs Values Using Streams
Now letβs focus on a different problem with maps. Instead of obtaining ISBNs based on the titles, weβll try and get titles based on the ISBNs.
Letβs use the original Map. We want to find titles with an ISBN starting with β978-0β.
List<String> titles = books.entrySet().stream()
.filter(e -> e.getKey().startsWith("978-0"))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
assertEquals(2, titles.size());
assertTrue(titles.contains(
"Design patterns : elements of reusable object-oriented software"));
assertTrue(titles.contains("Effective Java"));
This solution is similar to the solutions of our previous set of problems; we stream the entry set, and then filter, map, and collect.
Also like before, if we wanted to return only the first match, then after the map method we could call the findFirst() method instead of collecting all the results in a List.
5. Conclusion
In this article, weβve demonstrated how to process a Map in a functional way.
In particular, we have seen that once we switch to using the associated collections to Maps, processing using Streams becomes much easier and intuitive.
The code backing this article is available on GitHub. Once you're
logged in as a Baeldung Pro Member, start learning and coding on the project.