Reputation: 4274
Suppose I have a set of strings and a hash function (or any unilateral function) and a test function. I would like to create a map from the input string to its hash value, which passes the test function, with Java 8 stream. My question is how to write the keyMapper
in Collectors.toMap()
?
Pseudo code:
Map<String, String> result = inputStrings.stream()
.map(str -> hashFunc(str))
.filter(hash -> hash.startsWith("00"))
.collect(Collectors.toMap(hash -> ???, // the original input string is lost
Function::identity));
In other functional programming language, I could zip the input stream with the filtered hash stream, but Java 8 does not have zip
. Also, in map()
, I could return the pair of the input string and the hash value, so that the input will be passed down to the collector. But Java 8 does not have pair or tuple either.
It seems the old for loop is the most concise solution.
Upvotes: 2
Views: 185
Reputation: 298599
Well, even Java allows a purely functional solution, however, it’s readability heavily suffers from the fact that there are no true function types:
Map<String, String> result = inputStrings.stream()
.map(str -> { String hash=hashFunc(str);
return (Function<BinaryOperator<String>,String>)f->f.apply(str, hash); })
.filter(f -> f.apply((s,hash)->hash).startsWith("00"))
.collect(Collectors.toMap(f->f.apply((s,hash)->s), f->f.apply((s,hash)->hash)));
If the number of rejected entries is expected to be rather low compared the the number of accepted entries, you could simply create a complete map eagerly and remove the wrong ones afterwards:
Map<String, String> result = inputStrings.stream()
.collect(Collectors.collectingAndThen(
Collectors.toMap(Function.identity(), str -> hashFunc(str)),
map -> { map.values().removeIf(s->!s.startsWith("00")); return map; }));
This might be even more efficient than wrapping the elements and hash results in whatever pair type before eventually adding them to the Map
(creating another map-specific kind of pair, aka Map.Entry
). But it likely will have a higher peek memory usage, of course.
Upvotes: 2
Reputation: 27996
If the hashing function is cheap you could filter before mapping.
Map<String, String> result = inputStrings.stream()
.filter(val -> hashFunc(val).startsWith("00"))
.distinct()
.collect(Collectors.toMap(Function.identy(), this::hashFunc));
The distinct
operation is to ensure each value only occurs once as a key - they will map to the same hash value.
Upvotes: 0
Reputation: 198591
You are correct that no lambda will work there. There are a few alternative options, but the one I'd use would be:
Map<String, String> result = inputStrings.stream()
.map(str -> new AbstractMap.SimpleImmutableEntry<>(str, hashFunc(str)))
.filter(entry -> entry.getValue().startsWith("00"))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
(If I weren't collecting to a Map
, I would create my own custom tuple type appropriate for the use case rather than using Map.Entry
, but here Map.Entry
is a sufficient type.)
Upvotes: 4