James Dunnam James Dunnam - 7 months ago 42
Java Question

Functionally Inverting a map with a nested data structure in Java 8

I've got a problem that's currently driving me crazy. I'm trying to avoid creation of an intermediate object for this map inversion. (Clairification on objective: I have a Map with a nested data structure that I'd like to invert and explode. So,

Map<Foo,Set<String>> fooStringMap


becomes

Map<String,Foo> expandedStringFooMap

//Inverting a map is simple
private <X,Y> Map<Y,X> invertMap(Map<X,Y> source){
return source.entrySet().stream()
.collect(Collectors.toMap(Entry::getValue,Entry::getKey)

private <A,B> Map<A,B> explodeMapWithCollection(Map<? extends Collection<A>, B> collectionMap){
collectionMap.entrySet().stream()
.flatMap(x -> x.getKey().stream().collect(Collectors.toMap(Function.identity(),x.getValue())))
.collect(Collectors.toMap(Entry::getKey,Entry::getValue));
}


Currently, this is not working. I don't even think the above will compile, so just consider it pseudo code.

I've solved this using a pair like this:

someMap.keySet().stream().flatMap(key->someMap.get(key).stream().map(val -> new
Pair<>(val,key))).collect(Collectors.toMap(Pair::getLeft,Pair::getRight)));


That works like a charm, but I'd (for my own edification) like to avoid creating the intermediate pair. I know there's got to be a way to do it, but I seem to be lost in the synatax.

Answer

Below is one approach with a custom Stream#collect on the entry set. One could argue that this is not "fully functional" due to the forEach that is hidden in the accumulator, but at some point, the map entries have to be created, and I'm not sure whether there is an "elegant" way to use the stream from the Sets (the entry values) and still have the possibility of access the entry key (which will become the value of the new entries).

A side note (although I'm risking downvotes by taking up the cudgels for procedural programming): You don't have to do it the functional way just because you can. When you say that you are "lost in the syntax", then

  1. What will you think when reading this code again in a few weeks?
  2. What will your coworkers think when reading this code for the first time? (I'm concerned about the one with the chainsaw and the goalie mask here...)

I'd recommend to keep it simple. (Even though the most generic procedural form still may look confusing at the first glance)

import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class MapInvert
{
    public static void main(String[] args)
    {
        Map<Integer, Set<String>> map = 
            new LinkedHashMap<Integer, Set<String>>();

        map.put(1, new LinkedHashSet<String>(Arrays.asList("A","B","C")));
        map.put(2, new LinkedHashSet<String>(Arrays.asList("D","E","F")));
        map.put(3, new LinkedHashSet<String>(Arrays.asList("G","H","I")));

        Map<String, Integer> resultA = inverseEx(map);
        System.out.println("Procedural: "+resultA);

        Map<String, Integer> resultB = map.entrySet().stream().collect(
            LinkedHashMap::new, 
            (m, e) -> e.getValue().forEach(v -> m.put(v, e.getKey())), 
            (m0, m1) -> m0.putAll(m1));
        System.out.println("Functional: "+resultB);
    }

    /**
     * Invert the given map, by mapping each element of the values to
     * the respective key
     *  
     * @param map The input map
     * @return The inverted map
     */
    private static <K, V> Map<V, K> inverseEx(
        Map<K, ? extends Collection<? extends V>> map)
    {
        Map<V, K> result = new LinkedHashMap<V, K>();
        for (Entry<K, ? extends Collection<? extends V>> e : map.entrySet())
        {
            for (V v : e.getValue())
            {
                result.put(v, e.getKey());
            }
        }
        return result;
    }
}