22

I find myself wanting a variant of Collectors.toMap which returns an ImmutableMap, such that I can do:

ImmutableMap result = list.stream().collect(MyCollectors.toImmutableMap(
    tuple -> tuple._1(), tuple -> tuple._2());

(where tuple in this particular example is a Scala Tuple2)

I've just learned that such a method will be coming with Java-8 support in Guava 21 (yay!) but that sounds a good 6 months away. Does anyone know of any existing libraries (etc) which might implement this today?

ImmutableMap is not strictly required but seems the best choice as I require: lookup by key, and retaining original iteration order. Immutability is always preferred too.

Note that FluentIterable.toMap(Function) is not sufficient because I need both a key-mapping function as well as a value-mapping function.

Luke Usherwood
  • 3,082
  • 1
  • 28
  • 35
  • 1
    I'm surprised you didn't see anything on the internet. Searching "guava java 8 collector" in a popular search engine returns a lot of directly usable code, including results directly in the Guava projects (as issues). – Olivier Grégoire Dec 29 '15 at 08:38
  • 1
    We provided `collectingAndThen()` for this purpose. – Brian Goetz Dec 29 '15 at 18:45
  • The answers in the question on `ImmutableMultimap` indeed answer my immediate questions. However none of the solutions there or that I've now found around the web seem to contain a perfect set of functions to handle `ImmutableMap` - they lack the ability to merge-duplicate-keys and/or don't throw `IllegalStateException` (following the convention set by `Collectors`) – Luke Usherwood Jan 11 '16 at 20:31
  • 10
    Since Guava 21 there is built-in [ImmutableMap.toImmutableMap](https://google.github.io/guava/releases/21.0/api/docs/com/google/common/collect/ImmutableMap.html#toImmutableMap-java.util.function.Function-java.util.function.Function-) collector. – Vadzim Apr 03 '17 at 17:47
  • 1
    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:28

2 Answers2

26

You don't need to write an anonymous class for this collector. You can use Collector.of instead:

public static <T, K, V> Collector<T, ?, ImmutableMap<K,V>> toImmutableMap(
            Function<? super T, ? extends K> keyMapper,
            Function<? super T, ? extends V> valueMapper) {
    return Collector.of(
               ImmutableMap.Builder<K, V>::new,
               (b, e) -> b.put(keyMapper.apply(e), valueMapper.apply(e)),
               (b1, b2) -> b1.putAll(b2.build()),
               ImmutableMap.Builder::build);
}

Or if you don't mind collecting the results into a mutable map first and then copy the data into an immutable map, you can use the built-in toMap collector combined with collectingAndThen:

ImmutableMap<String, String> result = 
     list.stream()
         .collect(collectingAndThen(
             toMap(
                 tuple -> tuple._1(), 
                 tuple -> tuple._2()),
             ImmutableMap::copyOf));
amcc
  • 2,608
  • 1
  • 20
  • 28
Alexis C.
  • 91,686
  • 21
  • 171
  • 177
  • 1
    Perfect :-) I didn't think to look into the interface itself. I definitely prefer the `Collector.of` form as this will become a utility function with unknown future use (and tracking down sources of memory churn can be difficult later.) – Luke Usherwood Dec 29 '15 at 23:22
  • 7
    Guava now provides Collectors for their collection types, e.g. [`ImmutableList.toImmutableMap()`](https://github.com/google/guava/blob/2909a996047e575cdac02ea1c06a93a9419271cf/guava/src/com/google/common/collect/ImmutableMap.java#L65-L82). – Jonathan Oct 27 '17 at 13:55
  • If you don't want to use Guava, you can do the same with Map result = list.stream() .collect(collectingAndThen( toMap( tuple -> tuple._1(), tuple -> tuple._2()), [Collections::unmodifiableMap](https://docs.oracle.com/javase/8/docs/api/java/util/Collections.html#unmodifiableMap-java.util.Map-))); – fan Apr 02 '19 at 08:05
  • You should indicate that the second approach does not maintain the original iteration order as requested in the question. – Jose Da Silva Gomes Aug 20 '19 at 19:07
  • I preferred `ImmutableMap::builder` instead of `ImmutableMap.Builder::new` – jurl Jul 23 '20 at 14:20
  • If you are going to use a terminal operator than why not just wrap with `Collections.unmodifiableMap(...);`. There's really no need to have a specific Collector. – BWC semaJ Oct 02 '20 at 18:17
  • Re comments suggesting unmodifiableMap: there are many compelling benefits to using [ImmutableCollections](https://github.com/google/guava/wiki/ImmutableCollectionsExplained) as provided by Guava, including *treating these like interfaces*. Our team strongly frowns upon any use of unmodifiable collections these days; if you can't soundly reason about your code locally (e.g. "might the wrapped collection be changed by code elsewhere?") it really impedes your work. We even only permit the JDK's List.of (etc) in unit tests: Guava's are just better. – Luke Usherwood Dec 02 '21 at 07:45
0

Since I didn't find such a library of collectors yet, I'm sharing my first crack at the specific one I've needed. No bells or whistles here! (Such as handling or merging duplicate keys.)

Please feel free to suggest improvements.

/**
 * A variant of {@link Collectors#toMap(Function, Function)} for immutable maps.
 * <p>
 * Note this variant throws {@link IllegalArgumentException} upon duplicate keys, rather than
 * {@link IllegalStateException}
 * 
 * @param <T> type of the input elements
 * @param <K> output type of the key mapping function
 * @param <V> output type of the value mapping function
 * @param keyMapper  a mapping function to produce keys
 * @param valueMapper a mapping function to produce values
 * 
 * @return a {@code Collector} which collects elements into a {@code Map} whose keys and values
 *         are the result of applying mapping functions to the input elements
 *         
 * @throws IllegalArgumentException upon duplicate keys
 */
public static <T, K, V> Collector<T, ?, ImmutableMap<K,V>> toImmutableMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends V> valueMapper) {
    return new Collector<T, ImmutableMap.Builder<K,V>, ImmutableMap<K,V>>() {

        public Supplier<Builder<K, V>> supplier() {
            return ImmutableMap.Builder::new;
        }

        public BiConsumer<Builder<K, V>, T> accumulator() {
            return (builder, element) -> {
                K key = keyMapper.apply(element);
                V value = valueMapper.apply(element);
                builder.put(key, value);
            };
        }

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

        public Function<Builder<K, V>, ImmutableMap<K, V>> finisher() {
            return builder -> builder.build();
        }

        public Set<Collector.Characteristics> characteristics() {
            return ImmutableSet.of();
        }
    };
}
Luke Usherwood
  • 3,082
  • 1
  • 28
  • 35