Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I transform a collection into a Guava Multimap grouped by the elements of a nested collection property?

I have a List<Foo> and want a Guava Multimap<String, Foo> where we've grouped the Foos by each tag of their Collection<String> getTags() function.

I am using Java 8, so lambdas and method references are fine/encouraged.

For example if I have:

foo1, tags=a,b,c
foo2, tags=c,d
foo3, tags=a,c,e

I would get a Multimap<String, Foo> with:

a -> foo1, foo3
b -> foo1
c -> foo1, foo2, foo3
d -> foo2
e -> foo3
like image 747
Scott B Avatar asked Apr 11 '14 04:04

Scott B


People also ask

What is guava Multimap?

A Multimap is a new collection type that is found in Google's Guava library for Java. A Multimap can store more than one value against a key. Both the keys and the values are stored in a collection, and considered to be alternates for Map<K, List<V>> or Map<K, Set<V>> (standard JDK Collections Framework).

How do you create a Multimap in Java?

Following is a simple custom implementation of the Multimap class in Java using a Map and a Collection . * Add the specified value with the specified key in this multimap. * Returns the Collection of values to which the specified key is mapped, * or null if this multimap contains no mapping for the key.

How do you define Multimap?

In computer science, a multimap (sometimes also multihash, multidict or multidictionary) is a generalization of a map or associative array abstract data type in which more than one value may be associated with and returned for a given key.


2 Answers

You can use custom collector for this:

Multimap<String, Foo> map = list.stream().collect(
    ImmutableMultimap::builder,
    (builder, value) -> value.getTags().forEach(tag -> builder.put(tag, value)),
    (builder1, builder2) -> builder1.putAll(builder2.build())
).build();

This does not cause extra side effects (see here on this), is concurrent and more idiomatic.

You can also extract these ad-hoc lambdas into a full-fledged collector, something like this:

public static <T, K> Collector<T, ?, Multimap<K, T>> toMultimapByKey(Function<? super T, ? extends Iterable<? extends K>> keysMapper) {
    return new MultimapCollector<>(keysMapper);
}

private static class MultimapCollector<T, K> implements Collector<T, ImmutableMultimap.Builder<K, T>, Multimap<K, T>> {
    private final Function<? super T, ? extends Iterable<? extends K>> keysMapper;

    private MultimapCollector(Function<? super T, ? extends Iterable<? extends K>> keysMapper) {
        this.keysMapper = keysMapper;
    }

    @Override
    public Supplier<ImmutableMultimap.Builder<K, T>> supplier() {
        return ImmutableMultimap::builder;
    }

    @Override
    public BiConsumer<ImmutableMultimap.Builder<K, T>, T> accumulator() {
        return (builder, value) -> keysMapper.apply(value).forEach(k -> builder.put(k, value));
    }

    @Override
    public BinaryOperator<ImmutableMultimap.Builder<K, T>> combiner() {
        return (b1, b2) -> b1.putAll(b2.build());
    }

    @Override
    public Function<ImmutableMultimap.Builder<K, T>, Multimap<K, T>> finisher() {
        return ImmutableMultimap.Builder<K, T>::build;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

Then the collection would look like this:

Multimap<String, Foo> map = list.stream().collect(toMultimapByKey(Foo::getTags));

You can also return EnumSet.of(Characteristics.UNORDERED) from characteristics() method if the order is not important for you. This can make internal collection machinery act more efficiently, especially in case of parallel reduction.

like image 184
Vladimir Matveev Avatar answered Oct 14 '22 05:10

Vladimir Matveev


ImmutableMultimap.Builder<String, Foo> builder = ImmutableMultimap.builder();
list.forEach(foo -> foo.getTags().forEach(tag -> builder.put(tag, foo));
return builder.build();
like image 43
Louis Wasserman Avatar answered Oct 14 '22 04:10

Louis Wasserman