Collect to map skipping null key/values

10,379

Solution 1

If you want to avoid evaluating the functions func1 and func2 twice, you have to store the results. E.g.

stream.map(t -> new AbstractMap.SimpleImmutableEntry<>(func1(t), func2(t))
      .filter(e -> e.getKey()!=null && e.getValue()!=null)
      .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

This doesn’t make the code shorter and even the efficiency depends on the circumstances. This change pays off, if the costs of evaluating the func1 and func2 are high enough to compensate the creation of temporary objects. In principle, the temporary object could get optimized away, but this isn’t guaranteed.

Starting with Java 9, you can replace new AbstractMap.SimpleImmutableEntry<>(…) with Map.entry(…). Since this entry type disallows null right from the start, it would need filtering before constructing the entry:

stream.flatMap(t -> {
          Type1 value1 = func1(t);
          Type2 value2 = func2(t);
          return value1!=null && value2!=null? Stream.of(Map.entry(value1, value2)): null;
      })
      .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

Alternatively, you may use a pair type of one of the libraries you’re already using (the Java API itself doesn’t offer such a type).

Solution 2

Another way to avoid evaluating the functions twice. Use a pair class of your choice. Not as concise as Holger's but it's a little less dense which can be easier to read.

stream.map(A::doFuncs)
    .flatMap(Optional::stream)
    .collect(Collectors.toMap(Pair::getKey, Pair::getValue));

private static Optional<Pair<Bar, Baz>> doFuncs(Foo foo)
{
    final Bar bar = func1(foo);
    final Baz baz = func2(foo);
    if (bar == null || baz == null) return Optional.empty();
    return Optional.of(new Pair<>(bar, baz));
}

(Choose proper names - I didn't know what types you were using)

Solution 3

One option is to do as in the other answers, i.e. use a Pair type, or an implementation of Map.Entry. Another approach used in functional programming would be to memoize the functions. According to Wikipedia:

memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

So you could do it by caching the results of the functions in maps:

public static <K, V> Function<K, V> memoize(Function<K, V> f) {
    Map<K, V> map = new HashMap<>();
    return k -> map.computeIfAbsent(k, f);
}

Then, use the memoized functions in the stream:

Function<E, K> memoizedFunc1 = memoize(this::func1);
Function<E, V> memoizedFunc2 = memoize(this::func2);

stream.filter(t -> memoizedFunc1.apply(t) != null)
    .filter(t -> memoizedFunc2.apply(t) != null)
    .collect(Collectors.toMap(memoizedFunc1, memoizedFunc2));

Here E stands for the type of the elements of the stream, K stands for the type returned by func1 (which is the type of the keys of the map) and V stands for the type returned by func2 (which is the type of the values of the map).

Solution 4

This is a naive solution, but does not call functions twice and does not create extra objects:

List<Integer> ints = Arrays.asList(1, null, 2, null, 3);
Map<Integer, Integer> res = ints.stream().collect(LinkedHashMap::new, (lhm, i) -> {
    final Integer integer1 = func1(i);
    final Integer integer2 = func2(i);
    if(integer1 !=  null && integer2 != null) {
        lhm.put(integer1, integer2);
    }
}, (lhm1, lhm2) -> {});
Share:
10,379

Related videos on Youtube

Joel
Author by

Joel

Updated on September 15, 2022

Comments

  • Joel
    Joel over 1 year

    Let's say I have some stream and want to collect to map like this

    stream.collect(Collectors.toMap(this::func1, this::func2));
    

    But I want to skip null keys/values. Of course, I can do like this

    stream.filter(t -> func1(t) != null)
        .filter(t -> func2(t) != null)
        .collect(Collectors.toMap(this::func1, this::func2));
    

    But is there more beautiful/effective solution?

  • FrederikVH
    FrederikVH about 6 years
    @Michael I'd steer away from any javafx imports tbh
  • FrederikVH
    FrederikVH about 6 years
    Won't this cause func1 and func2 to be evaluated twice for each foo?
  • Michael
    Michael about 6 years
    @FrederikVH It's fine for the time being, though it'll be decoupled from the JDK in JDK 11.
  • FrederikVH
    FrederikVH about 6 years
    @Michael Yeah that's what I was thinking, we're on the same page :)
  • Holger
    Holger about 6 years
    @Michael starting with Java 9, you can use the much simpler Map.entry(…). I’ve added a note to the answer. It’s not only shorter in source code, the result being value-based makes it even more suitable for temporary objects.
  • davidxxx
    davidxxx about 6 years
    @FrederikVH Evaluating something twice may be a issue but I would sacrifice the readability only as required.
  • Michael
    Michael about 6 years
    Good shout. Map.entry is the best of both worlds.
  • Holger
    Holger about 6 years
    You could also implement doFuncs as return Optional.ofNullable(func1(foo)) .flatMap(v1 -> Optional.ofNullable(func2(foo)).map(v2 -> new Pair<>(v1, v2)));; this would also harmonize with Map.entry, as both values are already guaranteed to be non-null when the pair/entry is constructed.
  • davidxxx
    davidxxx about 6 years
    @Holger in your second example, should you not filter null values before collecting to prevent NullPointerException: .filter(Objects::nonNull).collect(Collectors.toMap(Map.Entry‌​::getKey, Map.Entry::getValue)); ?
  • Holger
    Holger about 6 years
    @davidxxx not necessary in the case of flatMap which maps to a stream, not an element: “If a mapped stream is null an empty stream is used, instead.
  • davidxxx
    davidxxx about 6 years
    @Holger I would have looked in the documentation before :) Thanks for the reference.
  • Terran
    Terran about 3 years
    Better name of memoize is memleak.
  • fps
    fps about 3 years
    @Terran Agreed, you shouldn't forget those things there