Reci
Reci

Reputation: 4274

How to create map with mapped value?

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

Answers (3)

Holger
Holger

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

sprinter
sprinter

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

Louis Wasserman
Louis Wasserman

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

Related Questions