Search code examples
javajava-8java-stream

Collect to map skipping null key/values


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?


Solution

  • 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).


    Another (probably the most efficient) alternative is a custom collector:

    stream.collect(HashMap::new,
        (m, o) -> {
          Type1 key   = func1(o);
          Type2 value = func2(o);
          if(key != null && value != null) m.put(key, value);
        },
        Map::putAll);
    

    Note that this collector, unlike the original toMap collector, doesn’t check for duplicates. But such a check could be added without problems.