87

I have a List<Foo> and want a Multimap<String, Foo> where we've grouped the Foo's by their getId() function.

I am using Java 8 and its almost awesome in that you can do:

List<Foo> foos = ...
Map<String, List<Foo>> foosById = foos.stream().collect(groupingBy(Foo::getId));

However, I have a good amount of code that wants a MultiMap<String, Foo> so this doesnt save me anything and I'm back to using a for-loop to create my Multimap. Is there a nice "functional" way that I am missing?

M. Justin
  • 14,487
  • 7
  • 91
  • 130
Scott B
  • 1,665
  • 1
  • 15
  • 14
  • 6
    This is not a duplicate as the other question is indexing the Foo's by a property of theirs that is an arrays (Tags) instead of a property with a single value (id) – Scott B Mar 23 '15 at 16:31
  • 9
    As of Guava 21.0, there is a builtin [Multimaps#toMultimap](https://google.github.io/guava/releases/21.0/api/docs/com/google/common/collect/Multimaps.html#toMultimap-java.util.function.Function-java.util.function.Function-java.util.function.Supplier-) and [flatteningToMultimap](https://google.github.io/guava/releases/21.0/api/docs/com/google/common/collect/Multimaps.html#flatteningToMultimap-java.util.function.Function-java.util.function.Function-java.util.function.Supplier-) methods that return collectors. – Daniel Bickler Jul 07 '17 at 15:56
  • Voting to reopen, as this is not a duplicate of the linked question. This one is a straightforward use of the API (as mentioned in the accepted answer), while the other is more about transforming and flat mapping the values and not really about the actual collection itself. – M. Justin Sep 18 '20 at 17:52
  • @DanielBickler Now that answers can be posted again since this question has been re-opened, I've expanded your comment into a full answer: https://stackoverflow.com/a/68443128/1108305 – M. Justin Jul 19 '21 at 17:39

2 Answers2

119

You can just use the Guava Multimaps factory:

ImmutableMultimap<String, Foo> foosById = Multimaps.index(foos, Foo::getId);

or wrap a call to Multimaps.index with a Collector<T, A, R> interface (shown below, in an unoptimized naive implementation).

Multimap<String, Foo> collect = foos.stream()
        .collect(MultimapCollector.toMultimap(Foo::getId));

and the Collector:

public class MultimapCollector<T, K, V> implements Collector<T, Multimap<K, V>, Multimap<K, V>> {

    private final Function<T, K> keyGetter;
    private final Function<T, V> valueGetter;

    public MultimapCollector(Function<T, K> keyGetter, Function<T, V> valueGetter) {
        this.keyGetter = keyGetter;
        this.valueGetter = valueGetter;
    }

    public static <T, K, V> MultimapCollector<T, K, V> toMultimap(Function<T, K> keyGetter, Function<T, V> valueGetter) {
        return new MultimapCollector<>(keyGetter, valueGetter);
    }

    public static <T, K, V> MultimapCollector<T, K, T> toMultimap(Function<T, K> keyGetter) {
        return new MultimapCollector<>(keyGetter, v -> v);
    }

    @Override
    public Supplier<Multimap<K, V>> supplier() {
        return ArrayListMultimap::create;
    }

    @Override
    public BiConsumer<Multimap<K, V>, T> accumulator() {
        return (map, element) -> map.put(keyGetter.apply(element), valueGetter.apply(element));
    }

    @Override
    public BinaryOperator<Multimap<K, V>> combiner() {
        return (map1, map2) -> {
            map1.putAll(map2);
            return map1;
        };
    }

    @Override
    public Function<Multimap<K, V>, Multimap<K, V>> finisher() {
        return map -> map;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return ImmutableSet.of(Characteristics.IDENTITY_FINISH);
    }
}
aioobe
  • 413,195
  • 112
  • 811
  • 826
Burg
  • 1,988
  • 1
  • 18
  • 22
  • 6
    `Multimaps.index` returns an *immutable* `Multimap`, which may not be what you want. – dkarp Dec 24 '15 at 04:26
  • Agree, multimap itself is immutable, but it does NOT mean the objects contained in them automatically are. If we can get a reference to an object in an immutable collection, then there's nothing to stop us changing any mutable state on that object. So for some use-case, Multimaps.index is still good. My 2 cents. – LeOn - Han Li Mar 25 '16 at 17:41
  • 9
    An immutable multimap is exactly what you should want in most cases. – Svante May 10 '16 at 18:51
  • 2
    I wrote an ImmutableMultimapCollector: https://medium.com/@robertmassaioli/an-immutablemultimapcollector-for-guava-3f141f9040f#.m18fnmgwo – Robert Massaioli Nov 03 '16 at 23:55
  • In addition to being immutable, it's worth noting that `Multimaps.index` returns a `ListMultimap`. If you want a `SetMultimap`, you'll need another step to convert that value if you use `Multimaps.index`, e.g.: `ImmutableSetMultimap.copyOf(Multimaps.index(foos, Foo::getId))`. – M. Justin Jul 19 '21 at 15:29
  • All these are based on an existing map, which would require two iterations. One to create the original list and then iterate on it to create the grouping. Is there not a way to add to a map that will group on the go? – mjs Mar 01 '23 at 09:26
19

Guava 21.0 introduced several methods that return Collector instances which will convert a Stream into a Multimap grouped by the result of applying a function to its elements. These methods are:

ImmutableListMultimap<String, Foo> foosById = foos.stream().collect(
        ImmutableListMultimap.toImmutableListMultimap(
                Foo::getId, Function.identity()));
ImmutableSetMultimap<String, Foo> foosById = foos.stream().collect(
        ImmutableSetMultimap.toImmutableSetMultimap(
                Foo::getId, Function.identity()));
HashMultimap<String, Foo> foosById = foos.stream().collect(
        Multimaps.toMultimap(
                Foo::getId, Function.identity(), HashMultimap::create)
);
M. Justin
  • 14,487
  • 7
  • 91
  • 130
  • All these are based on an existing map, which would require two iterations. One to create the original list and then iterate on it to create the grouping. Is there not a way to add to a map that will group on the go? – mjs Mar 01 '23 at 09:26
  • @mjs The question was about creating a `Multimap` from a `Stream`, which these do directly. Yes, you can definitely create a `HashMultimap` of the desired type and then add to it (for mutable maps), or create an immutable map containing the desired entries using a builder or a creator method. See Guava's guide on this, [NewCollectionTypesExplained](https://github.com/google/guava/wiki/NewCollectionTypesExplained#multimap). – M. Justin Mar 01 '23 at 16:15