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 over keySet()
or values()
, as a stream source
Map
isn’t really a collection. It’s not even an Iterable
- 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 a Consumer
taking a single value
Map.forEach()
expects a BiConsumer
taking two values: the key and the value (NOT a Map.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
Like this:
Like Loading...
I’ve been playing with JOOλ over the last few days and the Seq class adds some great features on top of the streams api. What I couldn’t try is your Seq.seq(map) example as it’s not in a release yet. I can reproduce using the code from github, but do you know when the next version will be released?
We’re quite busy with the upcoming jOOQ release right now – but it’s probably a good idea to ship another jOOλ release soon. Will do before the end of November 2014
I hate that Map doesn’t have stream(). I hate that Collections.toMap doesn’t have an overload that takes Entry. I love Brian Goetz — but everytime I hear some pedantic justification for extra verbosity over reasonable features (that MANY other SDKs offer out of the box)– I want to scream. I just want to filter a map! Groovy this would be a one liner with the filter closure being the bulk of the code. And in java8 I have to write a novel:
I know I know. I agree with everything. Including the pedantic justification part :)
Oh well. I then wrote jOOλ, trying to be constructive and harvesting additional brand value for jOOQ. But yes. I wish some JDK version would render all of jOOλ obsolete…