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 explain the basic idea we'll be using to work with Maps and Streams. Then we present a couple of different problems related to Maps and their concrete solutions using Streams.
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. 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 with the title “Effective Java”.
3.2. Retrieving a Match
Since the book title could not exist in our Map, we want to be able to indicate 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 want to only 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
Let's change the problem now 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 picking the first – putting 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 for which their ISBN start 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 to our previous set of problems – we stream the entry set, and then filter, map, and collect.
And like before, if we wanted to return only the first match, we could after the map method call the findFirst() method instead of collecting all the results in a List.
We've shown 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, the processing using Streams becomes much easier and intuitive.
And, of course, all the examples can be found in the GitHub project.