3

Using java 8 streams I want to convert a list into a map like described in the solution to Java 8 List<V> into Map<K, V>. However, I want to filter to remove entries with certain keys (for instance if the key is null) without doing the conversion of the value to a key twice.

For example I could do the filtering prior to collect such as

Map<String, Choice> result = choices.stream().filter((choice) -> choice.getName() != null).collect(Collectors.toMap(Choice::getName, Function.<Choice>identity()); 

In my case the logic to get the key is more complex than simply getting a field property, and I would like to avoid doing the logic first in the filter and again in the keyMapper function of Collectors.toMap

How can I convert the list to a map using a custom keyMapper function and filter certain values based on the new key?

3
  • Is choice.getName() actually an expensive operation? If not, then it seems likely that anything fancier would actually just end up being slower than this straightforward solution. Commented May 9, 2014 at 19:41
  • In the current case it's not especially expensive, but I would like to avoid having to do it twice if there is an alternative such as writing a custom collector or using a different stream method than collect such as map to first turn each item into a Map.Entry. I would like to avoid having to make a custom 'intermediate' pair object if possible though. Commented May 9, 2014 at 19:58
  • It looks to me as though there's not really any easy option that doesn't involve an intermediate pair, and that creating an intermediate pair would be more expensive than just calling a simple getter. Commented May 9, 2014 at 21:07

3 Answers 3

9

If you want to calculate the key only once, you can use the stream method map to convert the stream to a stream of tuples, filter the tuples based on the key, and finally create the map from the tuples:

Map<String, Choice> result = choices.stream() .map(c -> new AbstractMap.SimpleEntry<String, Choice>(c.getName(), c)) .filter(e -> e.getKey() != null) .collect(toMap(e -> e.getKey(), e -> e.getValue())); 
Sign up to request clarification or add additional context in comments.

3 Comments

isn't Map.Entry abstract?
@CarrKnight: You are correct, and I replaced Map.Entry with AbstractMap.SimpleEntry.
I understand the final Collectors.toMap() it necessary in this constellation, but I can't help but think that it must be possible to rewrite and simply this expression so that it's factored out. It's probably because my intuition tells me that there's an overhead here. But perhaps there isn't?
2

Here's a custom collector for what you want:

public class FilteredKeyCollector<T, K, V> implements Collector<T, Map<K, V>, Map<K, V>> { private final Function<? super T,? extends K> keyMapper; private final Function<? super T,? extends V> valueMapper; private final Predicate<K> keyFilter; private final EnumSet<Collector.Characteristics> characteristics; private FilteredKeyCollector(Function<? super T,? extends K> keyMapper, Function<? super T,? extends V> valueMapper, Predicate<K> keyFilter) { this.keyMapper = keyMapper; this.valueMapper = valueMapper; this.keyFilter = keyFilter; this.characteristics = EnumSet.of(Collector.Characteristics.IDENTITY_FINISH); } @Override public Supplier<Map<K, V>> supplier() { return HashMap<K, V>::new; } @Override public BiConsumer<Map<K, V>, T> accumulator() { return (map, t) -> { K key = keyMapper.apply(t); if (keyFilter.test(key)) { map.put(key, valueMapper.apply(t)); } }; } @Override public BinaryOperator<Map<K, V>> combiner() { return (map1, map2) -> { map1.putAll(map2); return map1; }; } @Override public Function<Map<K, V>, Map<K, V>> finisher() { return m -> m; } @Override public Set<Collector.Characteristics> characteristics() { return characteristics; } } 

And using it:

Map<String, Choice> result = choices.stream() .collect(new FilteredKeyCollector<>( Choice::getName, // key mapper c -> c, // value mapper k -> k != null)); // key filter 

Comments

1

If you accept doing it in 2 steps you could first collect the map and then remove unwanted keys:

Map<String, Choice> result = choices.stream() .collect(Collectors.toMap(c -> c.getName(), c -> c); result.keySet().removeIf(k -> k == null); 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.