61

I have a Java Map that I'd like to transform and filter. As a trivial example, suppose I want to convert all values to Integers then remove the odd entries.

Map<String, String> input = new HashMap<>();
input.put("a", "1234");
input.put("b", "2345");
input.put("c", "3456");
input.put("d", "4567");

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue())
        ))
        .entrySet().stream()
        .filter(e -> e.getValue() % 2 == 0)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));


System.out.println(output.toString());

This is correct and yields: {a=1234, c=3456}

However, I can't help but wonder if there's a way to avoid calling .entrySet().stream() twice.

Is there a way I can perform both transform and filter operations and call .collect() only once at the end?

Tunaki
  • 132,869
  • 46
  • 340
  • 423
Paul I
  • 860
  • 2
  • 9
  • 16
  • I don't think it is possible. Based on javadoc "A stream should be operated on (invoking an intermediate or terminal stream operation) only once. This rules out, for example, "forked" streams, where the same source feeds two or more pipelines, or multiple traversals of the same stream. A stream implementation may throw IllegalStateException if it detects that the stream is being reused." – kosa Feb 18 '16 at 16:25
  • @Namban That's not what the question is about. – user253751 Feb 18 '16 at 20:00

6 Answers6

76

Yes, you can map each entry to another temporary entry that will hold the key and the parsed integer value. Then you can filter each entry based on their value.

Map<String, Integer> output =
    input.entrySet()
         .stream()
         .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(), Integer.valueOf(e.getValue())))
         .filter(e -> e.getValue() % 2 == 0)
         .collect(Collectors.toMap(
             Map.Entry::getKey,
             Map.Entry::getValue
         ));

Note that I used Integer.valueOf instead of parseInt since we actually want a boxed int.


If you have the luxury to use the StreamEx library, you can do it quite simply:

Map<String, Integer> output =
    EntryStream.of(input).mapValues(Integer::valueOf).filterValues(v -> v % 2 == 0).toMap();
Tunaki
  • 132,869
  • 46
  • 340
  • 423
  • 1
    Though this is one possible hacky way to do, I feel like we are sacrificing performance + readability for few lines of code, isn't it? – kosa Feb 18 '16 at 16:30
  • 6
    @Nambari I don't see why it is that "hacky". It's just a map filter. If it's the explicit use of `AbstractMap.SimpleEntry`, you can create another `Pair` but I feel it is appropriate here since we're dealing with maps already. – Tunaki Feb 18 '16 at 16:32
  • 1
    I used "hacky" just because of "temporary entry" we are tracking, may not be correct term. I liked StreamEx solution. – kosa Feb 18 '16 at 16:34
  • 3
    @Nambari, note that StreamEx does the same thing internally, it's just a syntactic sugar. – Tagir Valeev Feb 19 '16 at 06:20
  • @TagirValeev: Yes, I know.When API doesn't support, all these libraries just does the sytactic sugar. – kosa Feb 19 '16 at 14:22
  • In the map method that you used, what if we have multiple lines of logic in there and then we return a new AbstractMap.SimpleEntry ? This is throwing me a Null Pointer exception during Collect. Not sure why? Please help – Dhiraj Gandhi Oct 23 '18 at 11:38
  • See also https://stackoverflow.com/a/52426752/744133 for different use of a `Map.Entry` – YoYo Apr 27 '22 at 19:47
19

One way to solve the problem with much lesser overhead is to move the mapping and filtering down to the collector.

Map<String, Integer> output = input.entrySet().stream().collect(
    HashMap::new,
    (map,e)->{ int i=Integer.parseInt(e.getValue()); if(i%2==0) map.put(e.getKey(), i); },
    Map::putAll);

This does not require the creation of intermediate Map.Entry instances and even better, will postpone the boxing of int values to the point when the values are actually added to the Map, which implies that values rejected by the filter are not boxed at all.

Compared to what Collectors.toMap(…) does, the operation is also simplified by using Map.put rather than Map.merge as we know beforehand that we don’t have to handle key collisions here.

However, as long as you don’t want to utilize parallel execution you may also consider the ordinary loop

HashMap<String,Integer> output=new HashMap<>();
for(Map.Entry<String, String> e: input.entrySet()) {
    int i = Integer.parseInt(e.getValue());
    if(i%2==0) output.put(e.getKey(), i);
}

or the internal iteration variant:

HashMap<String,Integer> output=new HashMap<>();
input.forEach((k,v)->{ int i = Integer.parseInt(v); if(i%2==0) output.put(k, i); });

the latter being quite compact and at least on par with all other variants regarding single threaded performance.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • I upvoted for your recommendation of a plain loop. Just because you can use streams doesn't mean you should. – Jeffrey Bosboom Feb 18 '16 at 20:04
  • 3
    @Jeffrey Bosboom: yeah, the good old `for` loop is still alive. Though in the case of maps, I like to use the `Map.forEach` method for smaller loops just because `(k,v)->` is much nicer than declaring a `Map.Entry` variable and possibly another two variables for the actual key and value… – Holger Feb 18 '16 at 20:09
6

Guava's your friend:

Map<String, Integer> output = Maps.filterValues(Maps.transformValues(input, Integer::valueOf), i -> i % 2 == 0);

Keep in mind that output is a transformed, filtered view of input. You'll need to make a copy if you want to operate on them independently.

shmosel
  • 49,289
  • 6
  • 73
  • 138
4

You could use the Stream.collect(supplier, accumulator, combiner) method to transform the entries and conditionally accumulate them:

Map<String, Integer> even = input.entrySet().stream().collect(
    HashMap::new,
    (m, e) -> Optional.ofNullable(e)
            .map(Map.Entry::getValue)
            .map(Integer::valueOf)
            .filter(i -> i % 2 == 0)
            .ifPresent(i -> m.put(e.getKey(), i)),
    Map::putAll);

System.out.println(even); // {a=1234, c=3456}

Here, inside the accumulator, I'm using Optional methods to apply both the transformation and the predicate, and, if the optional value is still present, I'm adding it to the map being collected.

fps
  • 33,623
  • 8
  • 55
  • 110
  • 1
    Very close to [my first variant](http://stackoverflow.com/a/35490546/2711488) but I don’t think that the use of `Optional` is a win here… – Holger Feb 18 '16 at 19:22
  • @Holger it's just to keep everything in one line. Anyways, I hadn't seen your answer. The win (if we become very subtle), might be that `Optional.ofNullable()` allows for null keys. – fps Feb 18 '16 at 19:38
  • 1
    We were writing at the same time. The `Optional` chain actually isn’t one line (or a very large one) but a single expression. But starting with a certain expression size, the two curly braces needed for a statement lambda do not appear that costly anymore… – Holger Feb 18 '16 at 19:41
3

Another way to do this is to remove the values you don't want from the transformed Map:

Map<String, Integer> output = input.entrySet().stream()
        .collect(Collectors.toMap(
                Map.Entry::getKey,
                e -> Integer.parseInt(e.getValue()),
                (a, b) -> { throw new AssertionError(); },
                HashMap::new
         ));
output.values().removeIf(v -> v % 2 != 0);

This assumes you want a mutable Map as the result, if not you can probably create an immutable one from output.


If you are transforming the values into the same type and want to modify the Map in place this could be alot shorter with replaceAll:

input.replaceAll((k, v) -> v + " example");
input.values().removeIf(v -> v.length() > 10);

This also assumes input is mutable.


I don't recommend doing this because It will not work for all valid Map implementations and may stop working for HashMap in the future, but you can currently use replaceAll and cast a HashMap to change the type of the values:

((Map)input).replaceAll((k, v) -> Integer.parseInt((String)v));
Map<String, Integer> output = (Map)input;
output.values().removeIf(v -> v % 2 != 0);

This will also give you type safety warnings and if you try to retrieve a value from the Map through a reference of the old type like this:

String ex = input.get("a");

It will throw a ClassCastException.


You could move the first transform part into a method to avoid the boilerplate if you expect to use it alot:

public static <K, VO, VN, M extends Map<K, VN>> M transformValues(
        Map<? extends K, ? extends VO> old, 
        Function<? super VO, ? extends VN> f, 
        Supplier<? extends M> mapFactory){
    return old.entrySet().stream().collect(Collectors.toMap(
            Entry::getKey, 
            e -> f.apply(e.getValue()), 
            (a, b) -> { throw new IllegalStateException("Duplicate keys for values " + a + " " + b); },
            mapFactory));
}

And use it like this:

    Map<String, Integer> output = transformValues(input, Integer::parseInt, HashMap::new);
    output.values().removeIf(v -> v % 2 != 0);

Note that the duplicate key exception can be thrown if, for example, the old Map is an IdentityHashMap and the mapFactory creates a HashMap.

Alex - GlassEditor.com
  • 14,957
  • 5
  • 49
  • 49
  • 1
    Your `"Duplicate keys " + a + " " + b` message is misleading: `a` and `b` are actually values, not keys. – Tagir Valeev Feb 19 '16 at 06:22
  • @TagirValeev yes, I noticed that, but it's the same way it's done in `Collectors` for the two argument version of `toMap` and changing it to what I had considered would put a scrollbar on the code box so I decided to leave it. I'll change it now since you also think it is misleading. – Alex - GlassEditor.com Feb 19 '16 at 06:28
  • 1
    Yes, this is known problem which is already fixed in [Java-9](https://bugs.openjdk.java.net/browse/JDK-8040892) (but not backported to Java-8). Java-9 displays colliding key as well as both values in the exception message. – Tagir Valeev Feb 19 '16 at 06:43
  • 1
    The funny thing about your unsafe hack is that the `groupingBy` collector does something similar behind the scenes which can come at a big surprise when you use a supplier creating map implementations enforcing type safety, e.g. `()->Collections.checkedMap(new HashMap<>(), …)` – Holger Feb 19 '16 at 09:40
0

Here is code by abacus-common

Map<String, String> input = N.asMap("a", "1234", "b", "2345", "c", "3456", "d", "4567");

Map<String, Integer> output = Stream.of(input)
                          .groupBy(e -> e.getKey(), e -> N.asInt(e.getValue()))
                          .filter(e -> e.getValue() % 2 == 0)
                          .toMap(Map.Entry::getKey, Map.Entry::getValue);

N.println(output.toString());

Declaration: I'm the developer of abacus-common.

user_3380739
  • 1
  • 14
  • 14