34

I want to collect the stream to a LinkedHashMap<String, Object>.

I have a JSON resource that is stored in LinkedHashMap<String, Object> resources. Then I filter out JSON elements by streaming the EntrySet of this map. Currently I am collecting the elements of stream to a regular HashMap. But after this I am adding other elements to the map. I want these elements to be in the inserted order.

final List<String> keys = Arrays.asList("status", "createdDate");

Map<String, Object> result = resources.entrySet()
        .stream()
        .filter(e -> keys.contains(e.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

result.put("date", "someDate");
return result;

That is why I want to collect the stream to a LinkedHashMap<String, Object>. How can I achieve this?

xtra
  • 1,957
  • 4
  • 22
  • 40
  • 1
    *I have a JSON resource that is stored in Map resources* -- How do you know that the entries in this map are in ANY order? Unless that's stored in a LinkedHashMap how do you expect order going forward? – Nicholas K Oct 24 '18 at 18:07
  • JSON doesn't care about the orders of the keys of a JSON Object. – njzk2 Oct 25 '18 at 02:26
  • 1
    Yes but I do. I print them out in a specific order and I unittest the method by comparing it to another JSON that is in my resources. – xtra Oct 25 '18 at 04:13
  • @NicholasK Indeed, I gave the wrong type. Should have been `LinkedHashMap resources`. – xtra Oct 25 '18 at 04:32

4 Answers4

81

You can do this with Stream:

Map<String, Object> result = resources.entrySet()
            .stream()
            .filter(e -> keys.contains(e.getKey()))
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (x, y) -> y, LinkedHashMap::new));

The part (x, y) -> y is because of mergeFunction when find duplicate keys, it returns value of second key which found. the forth part is mapFactory which a supplier providing a new empty Map into which the results will be inserted.

Amin
  • 1,643
  • 16
  • 25
  • 1
    How can you do this in scala? :) – Rimer Oct 05 '20 at 15:22
  • how to do it when we want just to change hashmap impl, but keep duplicates behavior? JDK conveninetly kept `throwingMerger` (previous name, not necesserily present in current jdk) as private method, and also 4 param version does not allow to specify accumulator, only map merger... What is correct way? – Martin Mucha Sep 20 '21 at 09:06
  • `LinkedHashMap.merge()` throws a `NullPointerException` when any entry value is `null` using this approach, FYI. – Sam Barnum Aug 17 '22 at 18:40
  • This is great. It seems like `Collectors.toMap` is a bit more complicated than the other `Collectors.to*` options. Shouldn't there be a default way to filter a map and reconstruct it? i.e. taking a `List>` should be overloaded -- i'm guessing in this case, there also wouldn't be a way to generate duplicates either... and maybe we can choose the original map class as the class to use? – Ehtesh Choudhury Mar 08 '23 at 08:01
  • @MartinMucha Just throw an `IllegalStateException` in the merge function. Check my answer below for an example – Dzeri96 May 11 '23 at 15:30
4

An alternate way of doing this using Map.forEach is:

Map<String, Object> result = new LinkedHashMap<>();
resources.forEach((key, value) -> {
    if (keys.contains(key)) {
        result.put(key, value);
    }
});
result.put("date", "someDate");

and if you could consider iterating on the keySet as an option:

Map<String, Object> result = new LinkedHashMap<>();
resources.keySet().stream()
        .filter(keys::contains)
        .forEach(key -> result.put(key,resources.get(key)));
result.put("date", "someDate");
Naman
  • 27,789
  • 26
  • 218
  • 353
1

The accepted answer contains different merger than the default one (throwing). It is pity that there exists no three-arg toMap(keyFunction, valueFunction, mapFactory) overload in Java Collectors class. Since I needed just LinkedHashMap I just made it as a custom helper method from copypasted (where necessary) internals of that class. The behavior is same as the two-arg toMap except the resulting map is LinkedHashMap.

class Helper {

    private static <K,V,M extends Map<K,V>> BinaryOperator<M> uniqKeysMapMerger() {
        return (m1, m2) -> {
            for (Map.Entry<K,V> e : m2.entrySet()) {
                K k = e.getKey();
                V v = Objects.requireNonNull(e.getValue());
                V u = m1.putIfAbsent(k, v);
                if (u != null) throw new IllegalStateException(String.format("Duplicate key %s (attempted merging values %s and %s)", k, u, v));
            }
            return m1;
        };
    }

    private static <T,K,V> BiConsumer<Map<K,V>,T> uniqKeysMapAccumulator(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends V> valueMapper) {
        return (map, element) -> {
            K k = keyMapper.apply(element);
            V v = Objects.requireNonNull(valueMapper.apply(element));
            V u = map.putIfAbsent(k, v);
            if (u != null) throw new IllegalStateException(String.format("Duplicate key %s (attempted merging values %s and %s)", k, u, v));
        };
    }

    public static <T,K,U> Collector<T,?,Map<K,U>> toLinkedHashMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) {
        return Collector.of(LinkedHashMap::new, uniqKeysMapAccumulator(keyMapper, valueMapper), uniqKeysMapMerger());
    }

}
Tomáš Záluský
  • 10,735
  • 2
  • 36
  • 64
1

As others have pointed out, if you want to supply your own mapFactory, you also have to supply a merge function. The accepted answer actually changes the default behavior of the three-argument function, which is to throw when duplicate keys are encountered. This implementation retains the original behavior:

LinkedHashMap lmp = collection.
                .stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (v1, v2) -> {throw new IllegalStateException("YOUR MESSAGE HERE");},
                        LinkedHashMap::new
                ));
Dzeri96
  • 196
  • 2
  • 13