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 Mapisn’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 aConsumertaking a single valueMap.forEach()expects aBiConsumertaking 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. |

