Let’s Stream a Map in Java 8 with jOOλ
I wanted to find an easy way to stream a Map in Java 8. Guess what? There isn’t!
What I would’ve expected for convenience is the following method:
public interface Map<K, V> { default Stream<Entry<K, V>> stream() { return entrySet().stream(); } }
But there’s no such method. There are probably a variety of reasons why such a method shouldn’t exist, e.g.:
- There’s no “clear” preference for
entrySet()
being chosen overkeySet()
orvalues()
, as a stream source Map
isn’t really a collection. It’s not even anIterable
- That wasn’t the design goal
- The EG didn’t have enough time
Well, there is a very compelling reason for Map
to have been retrofitted to provide both an entrySet().stream()
and to finally implement Iterable<Entry<K, V>>
. And that reason is the fact that we now have Map.forEach()
:
default void forEach( BiConsumer<? super K, ? super V> action) { Objects.requireNonNull(action); for (Map.Entry<K, V> entry : entrySet()) { K k; V v; try { k = entry.getKey(); v = entry.getValue(); } catch(IllegalStateException ise) { // this usually means the entry is no longer in the map. throw new ConcurrentModificationException(ise); } action.accept(k, v); } }
forEach()
in this case accepts a BiConsumer
that really consumes entries in the map. If you search through JDK source code, there are really very few references to the BiConsumer
type outside of Map.forEach()
and perhaps a couple of CompletableFuture
methods and a couple of streams collection methods.
So, one could almost assume that BiConsumer
was strongly driven by the needs of this forEach()
method, which would be a strong case for making Map.Entry
a more important type throughout the collections API (we would have preferred the type Tuple2, of course).
Let’s continue this line of thought. There is also Iterable.forEach()
:
public interface Iterable<T> { default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } }
Both Map.forEach()
and Iterable.forEach()
intuitively iterate the “entries” of their respective collection model, although there is a subtle difference:
Iterable.forEach()
expects aConsumer
taking a single valueMap.forEach()
expects aBiConsumer
taking two values: the key and the value (NOT aMap.Entry
!)
Think about it this way:
This makes the two methods incompatible in a “duck typing sense”, which makes the two types even more different
Bummer!
Improving Map with jOOλ
We find that quirky and counter-intuitive. forEach()
is really not the only use-case of Map
traversal and transformation. We’d love to have a Stream<Entry<K, V>>
, or even better, a Stream<Tuple2<T1, T2>>
. So we implemented that in jOOλ, a library which we’ve developed for our integration tests at jOOQ. With jOOλ, you can now wrap a Map
in a Seq
type (“Seq” for sequential stream, a stream with many more functional features):
Map<Integer, String> map = new LinkedHashMap<>(); map.put(1, "a"); map.put(2, "b"); map.put(3, "c"); assertEquals( Arrays.asList( tuple(1, "a"), tuple(2, "b"), tuple(3, "c") ), Seq.seq(map).toList() );
What you can do with it? How about creating a new Map
, swapping keys and values in one go:
System.out.println( Seq.seq(map) .map(Tuple2::swap) .toMap(Tuple2::v1, Tuple2::v2) ); System.out.println( Seq.seq(map) .toMap(Tuple2::v2, Tuple2::v1) );
Both of the above will yield:
{a=1, b=2, c=3}
Just for the record, here’s how to swap keys and values with standard JDK API:
System.out.println( map.entrySet() .stream() .collect(Collectors.toMap( Map.Entry::getValue, Map.Entry::getKey )) );
It can be done, but the every day verbosity of standard Java API makes things a bit hard to read / write.
Reference: | Let’s Stream a Map in Java 8 with jOOλ from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog. |