25

I really like Java 8 streams and Guava's immutable collections, but I can't figure out how to use the two together.

For example, how do I implement a Java 8 Collector that gathers stream results into an ImmutableMultimap?

Bonus points: I'd like to be able to provide key/value mappers, similar to how Collectors.toMap() works.

sebkur
  • 658
  • 2
  • 9
  • 18
Gili
  • 86,244
  • 97
  • 390
  • 689
  • 3
    This link can help- http://www.jayway.com/2013/11/12/immutable-list-collector-in-java-8/ – Arjit Jun 04 '15 at 03:55
  • 1
    yes this has got http://www.jayway.com/2014/09/29/java-8-collector-for-gauvas-linkedhashmultimap/ thanks for Arijit – bobs_007 Jun 04 '15 at 04:11
  • @Arjit this is a great step in the right direction, but I'm still trying to get the key/value mappers to work. – Gili Jun 04 '15 at 04:41
  • 2
    Use this link - http://blog.comsysto.com/2014/11/12/java-8-collectors-for-guava-collections/ – Arjit Jun 04 '15 at 05:26
  • @Arjit Thank you. I independently came up with a similar implementation through trial and error (posted below). – Gili Jun 04 '15 at 05:33
  • 2
    Strongly related, but not in the related sidebar yet: [How can I collect a Java 8 stream into a Guava ImmutableCollection?](https://stackoverflow.com/questions/29013250/how-can-i-collect-a-java-8-stream-into-a-guava-immutablecollection) – Jeffrey Bosboom Dec 15 '15 at 23:24
  • See this [answer](https://stackoverflow.com/a/50071018/852336) for an example of how to use Guava's toImmutableMap() collector. – Danish Khan Apr 27 '18 at 22:30

5 Answers5

32

Since version 21, you can

.collect(ImmutableSet.toImmutableSet())
.collect(ImmutableMap.toImmutableMap())
.collect(Maps.toImmutableEnumMap())
.collect(Sets.toImmutableEnumSet())
.collect(Tables.toTable())
.collect(ImmutableList.toImmutableList())
.collect(Multimaps.toMultimap(...))
Matthieu Gabin
  • 830
  • 9
  • 26
numéro6
  • 3,921
  • 1
  • 19
  • 18
8

Update: I found an implementation that seems to cover all Guava collections at https://github.com/yanaga/guava-stream and attempted to improve upon it in my own library at https://bitbucket.org/cowwoc/guava-jdk8/

I'm leaving the previous answer below, for historical reasons.


Holy #@!( I got it!

This implementation works for any Multimap (mutable or immutable) whereas shmosel's solution focuses on immutable implementations. That said, the latter might be more efficient for the immutable case (I don't use a builder).

import com.google.common.collect.Multimap;
import java.util.EnumSet;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collector.Characteristics;
import org.bitbucket.cowwoc.preconditions.Preconditions;

/**
 * A Stream collector that returns a Multimap.
 * <p>
 * @author Gili Tzabari
 * @param <T> the type of the input elements
 * @param <K> the type of keys stored in the map
 * @param <V> the type of values stored in the map
 * @param <R> the output type of the collector
 */
public final class MultimapCollector<T, K, V, R extends Multimap<K, V>>
    implements Collector<T, Multimap<K, V>, R>
{
    private final Supplier<Multimap<K, V>> mapSupplier;
    private final Function<? super T, ? extends K> keyMapper;
    private final Function<? super T, ? extends V> valueMapper;
    private final Function<Multimap<K, V>, R> resultMapper;

    /**
     * Creates a new MultimapCollector.
     * <p>
     * @param mapSupplier  a function which returns a new, empty {@code Multimap} into which intermediate results will be
     *                     inserted
     * @param keyMapper    a function that transforms the map keys
     * @param valueMapper  a function that transforms the map values
     * @param resultMapper a function that transforms the intermediate {@code Multimap} into the final result
     * @throws NullPointerException if any of the arguments are null
     */
    public MultimapCollector(Supplier<Multimap<K, V>> mapSupplier,
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends V> valueMapper,
        Function<Multimap<K, V>, R> resultMapper)
    {
        Preconditions.requireThat(mapSupplier, "mapSupplier").isNotNull();
        Preconditions.requireThat(keyMapper, "keyMapper").isNotNull();
        Preconditions.requireThat(valueMapper, "valueMapper").isNotNull();
        Preconditions.requireThat(resultMapper, "resultMapper").isNotNull();

        this.mapSupplier = mapSupplier;
        this.keyMapper = keyMapper;
        this.valueMapper = valueMapper;
        this.resultMapper = resultMapper;
    }

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

    @Override
    public BiConsumer<Multimap<K, V>, T> accumulator()
    {
        return (map, entry) ->
        {
            K key = keyMapper.apply(entry);
            if (key == null)
                throw new IllegalArgumentException("keyMapper(" + entry + ") returned null");
            V value = valueMapper.apply(entry);
            if (value == null)
                throw new IllegalArgumentException("keyMapper(" + entry + ") returned null");
            map.put(key, value);
        };
    }

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

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

    @Override
    public Set<Characteristics> characteristics()
    {
        return EnumSet.noneOf(Characteristics.class);
    }
}

[...]

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

/**
 * Stream collectors for Guava collections.
 * <p>
 * @author Gili Tzabari
 */
public final class GuavaCollectors
{
    /**
     * Returns a {@code Collector} that accumulates elements into a {@code Multimap}.
     * <p>
     * @param <T>          the type of the input elements
     * @param <K>          the type of the map keys
     * @param <V>          the type of the map values
     * @param <R>          the output type of the collector
     * @param mapSupplier  a function which returns a new, empty {@code Multimap} into which intermediate results will be
     *                     inserted
     * @param keyMapper    a function that transforms the map keys
     * @param valueMapper  a function that transforms the map values
     * @param resultMapper a function that transforms the intermediate {@code Multimap} into the final result
     * @return a {@code Collector} which collects elements into a {@code Multimap} whose keys and values are the result of
     *         applying mapping functions to the input elements
     */
    public static <T, K, V, R extends Multimap<K, V>> Collector<T, ?, R> toMultimap(
        Supplier<Multimap<K, V>> mapSupplier,
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends V> valueMapper,
        Function<Multimap<K, V>, R> resultMapper)
    {
        return new MultimapCollector<>(mapSupplier, keyMapper, valueMapper, resultMapper);
    }

    public static void main(String[] args)
    {
        Multimap<Integer, Double> input = HashMultimap.create();
        input.put(10, 20.0);
        input.put(10, 25.0);
        input.put(50, 60.0);
        System.out.println("input: " + input);
        ImmutableMultimap<Integer, Double> output = input.entries().stream().collect(
            GuavaCollectors.toMultimap(HashMultimap::create,
                entry -> entry.getKey() + 1, entry -> entry.getValue() - 1,
                ImmutableMultimap::copyOf));
        System.out.println("output: " + output);
    }
}

main() outputs:

input: {10=[20.0, 25.0], 50=[60.0]}
output: {51=[59.0], 11=[24.0, 19.0]}

Resources

Community
  • 1
  • 1
Gili
  • 86,244
  • 97
  • 390
  • 689
  • +1 for posting your solution as a reusable library we can add as a simple Maven dependency! :-) This looks like just the stop-gap solution I was looking for until [Guava 21](https://groups.google.com/forum/#!topic/guava-discuss/ZRmDJnAq9T0) comes out with this built in (mid 2016) – Luke Usherwood Dec 30 '15 at 01:29
5

Here's a version that will support several ImmutableMultimap implementations. Note that the first method is private since it requires an unsafe cast.

@SuppressWarnings("unchecked")
private static <T, K, V, M extends ImmutableMultimap<K, V>> Collector<T, ?, M> toImmutableMultimap(
        Function<? super T, ? extends K> keyFunction,
        Function<? super T, ? extends V> valueFunction,
        Supplier<? extends ImmutableMultimap.Builder<K, V>> builderSupplier) {

    return Collector.of(
            builderSupplier,
            (builder, element) -> builder.put(keyFunction.apply(element), valueFunction.apply(element)),
            (left, right) -> {
                left.putAll(right.build());
                return left;
            },
            builder -> (M)builder.build());
}

public static <T, K, V> Collector<T, ?, ImmutableMultimap<K, V>> toImmutableMultimap(
        Function<? super T, ? extends K> keyFunction,
        Function<? super T, ? extends V> valueFunction) {
    return toImmutableMultimap(keyFunction, valueFunction, ImmutableMultimap::builder);
}

public static <T, K, V> Collector<T, ?, ImmutableListMultimap<K, V>> toImmutableListMultimap(
        Function<? super T, ? extends K> keyFunction,
        Function<? super T, ? extends V> valueFunction) {
    return toImmutableMultimap(keyFunction, valueFunction, ImmutableListMultimap::builder);
}

public static <T, K, V> Collector<T, ?, ImmutableSetMultimap<K, V>> toImmutableSetMultimap(
        Function<? super T, ? extends K> keyFunction,
        Function<? super T, ? extends V> valueFunction) {
    return toImmutableMultimap(keyFunction, valueFunction, ImmutableSetMultimap::builder);
}
shmosel
  • 49,289
  • 6
  • 73
  • 138
  • Good solution, but it's limited to immutable implementations due to the use of `ImmutableMultimap.Builder`. It doesn't seem to be possible to provide a single Collector that will do the right thing for both mutable and immutable types. My answer supports both types, but it does not use a `Builder` so it might be less efficient for immutable collections. – Gili Jun 04 '15 at 05:47
  • 2
    @Gili, the question made it clear that you were looking for immutable collectors. Your answer doesn't support immutable types per se; it relies on the assumption that a mutable result can be subsequently copied to an immutable collection. As far as performance is concerned, my approach should be more efficient for sequential streams, whereas using a mutable accumulator would probably be preferable for parallel streams. – shmosel Jun 04 '15 at 05:54
3

For your example, you can use Guava's toImmutableMap() collector. E.g.

import static com.google.common.collect.ImmutableMap.toImmutableMap;

ImmutableMap<String, ZipCode> zipCodeForName =
    people.stream()
        .collect(
            toImmutableMap(Person::getName, p -> p.getAddress().getZipCode()));

ImmutableMap<String, Person> personForName =
    people.stream()
        .collect(
            toImmutableMap(Person::getName, p -> p));

The top answer has the rest of the Guava immutable collectors APIs

Danish Khan
  • 1,514
  • 3
  • 15
  • 22
2

Although it doesn't answer specifically the question, it's noteworthy to mention that this simple pattern can help everyone building Guava's immutable collections from streams without having the need for Collectors (as their implementations are rather hard to make).

Stream<T> stream = ...

ImmutableXxx<T> collection = ImmutableXxx.copyOf(stream.iterator());
Olivier Grégoire
  • 33,839
  • 23
  • 96
  • 137
  • That won't support any custom mapping or grouping functions that are standard in other collectors. – shmosel Jun 04 '15 at 18:49