83

I'm using Java 8 lambdas and want to use Collectors toMap to return a SortedMap. The best I can come up with is to call the following Collectors toMap method with a dummy mergeFunction and mapSupplier equal to TreeMap::new.

public static <T, K, U, M extends Map<K, U>>
        Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                Function<? super T, ? extends U> valueMapper,
                BinaryOperator<U> mergeFunction,
                Supplier<M> mapSupplier) {
    BiConsumer<M, T> accumulator = (map, element) -> map.merge(keyMapper.apply(element),
            valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

I don't want to pass in a merge function though, as I just want throwingMerger(), in the same way as the basic toMapimplementation as follows:

public static <T, K, U>
        Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper,
                Function<? super T, ? extends U> valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

What would be the best practise method of using Collectors to return a SortedMap?

Robert Bain
  • 9,113
  • 8
  • 44
  • 63

5 Answers5

106

I don't think you can get much better than this:

.collect(Collectors.toMap(keyMapper, valueMapper,
                        (v1,v2) ->{ throw new RuntimeException(String.format("Duplicate key for values %s and %s", v1, v2));},
                        TreeMap::new));

where the throw lambda is the same as throwingMerger() but I can't directly call that since it's package private (you can of course always make your own static method for that like throwingMerger() is. )

Christoffer Hammarström
  • 27,242
  • 4
  • 49
  • 58
dkatzel
  • 31,188
  • 3
  • 63
  • 67
  • I have *exactly* this. – Robert Bain Jun 23 '15 at 14:13
  • I'm going to go down the custom Collectors route as I can see similar things happening in future. – Robert Bain Jun 23 '15 at 15:03
  • 4
    The parameter you've designated `k` is not the *key*, as the letter would imply, but rather the first value of the binary operation for merging. – antak Sep 25 '15 at 05:17
  • 1
    @antak the javadoc is confusing. But I actually took the Exception message from [Collectors#throwingMerger](http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/java/util/stream/Collectors.java#Collectors.throwingMerger%28%29) which says "duplicate key" for the first parameter – dkatzel Sep 25 '15 at 14:53
  • 2
    Yes, the exception message is bad. http://mail.openjdk.java.net/pipermail/lambda-dev/2014-April/012005.html – antak Sep 26 '15 at 08:07
  • 2
    This SO-answer: http://stackoverflow.com/questions/25712591/java8-convert-one-map-to-an-another-using-stream suggest a shorter way to put this, by simply assuming no duplicates (overwrite if there is). – mortensi May 03 '16 at 11:12
  • The JDK uses `IllegalStateException`, not `RuntimeException`. – shmosel Aug 29 '16 at 21:40
  • 6
    I changed from `(k,v)` to `(v1,v2)`, as the lambda parameters are actually the two conflicting values. The `throwingMerger()` in the JDK is wrong. I hope you don't mind. :) – Christoffer Hammarström Dec 08 '16 at 19:14
8

Based on dkatzel's confirmation that there's not a nice API method, I've opted for maintaining my own custom Collectors class:

public final class StackOverflowExampleCollectors {

    private StackOverflowExampleCollectors() {
        throw new UnsupportedOperationException();
    }

    private static <T> BinaryOperator<T> throwingMerger() {
        return (u, v) -> {
            throw new IllegalStateException(String.format("Duplicate key %s", u));
        };
    }

    public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
            Function<? super T, ? extends U> valueMapper, Supplier<M> mapSupplier) {
        return Collectors.toMap(keyMapper, valueMapper, throwingMerger(), mapSupplier);
    }

}
Robert Bain
  • 9,113
  • 8
  • 44
  • 63
  • 2
    You should change the exception message. You use one of the values (as the JDK did: https://bugs.openjdk.java.net/browse/JDK-8040892), but the message suggests it’s the key. It is possible to show the key (http://hg.openjdk.java.net/jdk9/dev/jdk/rev/8b80651ce43f), but that’s more complex, so maybe just use `throw new IllegalStateException(String.format("Duplicate key for values %s and %s", u, v));`. – Martin Nov 07 '17 at 16:14
  • Hey @Martin, thanks for the links! In the example, I was trying to post the code verbatim from the private `throwingMerger` method of the `Collectors` class to show that I was working around the fact it's private. I totally see where you're coming from and the error message you present is better but maybe detracts from trying to say "do the exact same thing, the visibility modifier is getting in way"? How about I add your suggestion as an edit at the bottom and cite your sources? – Robert Bain Nov 07 '17 at 22:44
7

Seems that there's no standard way to do this without defining your own throwingMerger() method or using explicit lambda. In my StreamEx library I defined the toSortedMap method which also uses my own throwingMerger().

Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334
5

Another way you can do this is to allow Collectors.toMap() to return whatever map it is going to return, and then pass that to a new TreeMap<>().

The caveat there is that this only works if your "hashCode()+equals()" and "compareTo" are consistent. If they aren't consistent, then you'll end up with the HashMap removing different set of keys than your TreeMap.

Daniel
  • 4,481
  • 14
  • 34
4

If you use the guava library then you can use:

.collect(ImmutableSortedMap.toImmutableSortedMap(comparator, keyMapper, valueMapper));

The resulting map will be a SortedMap and also immutable.

uwe
  • 3,931
  • 1
  • 27
  • 19