Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Filtering a stream based on its values in a toMap collection

I have a situation where I have Player objects in a development project, and the task is simply measuring the distance and returning results which fall under a certain threshold. Of course, I'm wanting to use streams in the most concise manner possible.

Currently, I have a solution which maps the stream, and then filters via an iterator:

Stream<Player> str = /* source of my player stream I'm filtering */;
Map<Player, Double> dists = str.collect(Collectors.toMap(...)); //mapping function
Iterator<Map.Entry<Player, Double>> itr = map.entrySet().iterator();
while (itr.hasNext()) {
    if (itr.next().getValue() <= radiusSquared) {
        itr.remove();
    }
}

However, what I'd like to achieve is something which performs this filtering while the stream is being operated upon, something which says "if this predicate fails, do not collect", to attempt and save the second iteration. Additionally, I don't want to calculate the distances twice, so doing a filter via the mapping function, and then re-mapping isn't a plausible solution.

The only real viable solution I've thought of is mapping to a Pair<A, B>, but if there's native support for some form of binary stream, that'd be better.

Is there native support for this in java's stream API?

like image 477
Rogue Avatar asked Dec 13 '15 16:12

Rogue


People also ask

How do you filter a stream map?

Since our filter condition requires an int variable we first need to convert Stream of String to Stream of Integer. That's why we called the map() function first. Once we have the Stream of Integer, we can apply maths to find out even numbers. We passed that condition to the filter method.

What is stream filtering?

A filter stream is constructed on another stream (the underlying stream). The read method in a readable filter stream reads input from the underlying stream, filters it, and passes on the filtered data to the caller.

How do you filter values in Java?

The filter() method of the Stream class, as the name suggests, filters any Collection based on a given condition. For example, given a Collection of names, you can filter them out based on conditions such as - containing certain characters or starting with a specific character.

What kind of interface does the filter method in a stream accept?

Using the Stream filter method A stream interface's filter() method identifies elements in a stream that satisfy a criterion. It is a stream interface intermediate operation. Notice how it accepts a predicate object as a parameter. A predicate is a logical interface to a functional interface.


2 Answers

Note that your old-style iterator-loop could be rewritten in Java-8 using Collection.removeIf:

map.values().removeIf(dist -> dist <= radiusSquared);

So it does not actually that bad. Don't forget that keySet() and values() are modifiable.

If you want to solve this using single pipeline (for example, most of the entries are to be removed), then bad news for you. Seems that current Stream API does not allow you to do this without explicit use of the class with pair semantics. It's quite natural to create a Map.Entry instance, though already existing option is AbstractMap.SimpleEntry which has quite long and unpleasant name:

str.map(player -> new AbstractMap.SimpleEntry(player, getDistance(player)))
   .filter(entry -> entry.getValue() > radiusSquared)
   .toMap(Entry::getKey, Entry::getValue);

Note that it's likely that in Java-9 there will be Map.entry() static method, so you could use Map.entry(player, getDistance(player)). See JEP-269 for details.

As usual my StreamEx library has some syntactic sugar to solve this problem in cleaner way:

StreamEx.of(str).mapToEntry(player -> getDistance(player))
                .filterValues(dist -> dist > radiusSquared)
                .toMap();

And regarding the comments: yes, toMap() collector uses one-by-one insert, but don't worry: bulk inserts to map rarely improve the speed. You even cannot pre-size the hash-table (if your map is hash-based) as you don't know much about the elements being inserted. Probably you want to insert a million of objects with the same key: allocating the hash-table for million entries just to discover that you will have only one entry after insertion would be too wasteful.

like image 33
Tagir Valeev Avatar answered Sep 22 '22 01:09

Tagir Valeev


Filtering a Map afterwards is not as bad as it seems, keep in mind that iterating over a Map does not imply the same cost as performing a lookup (e.g. hashing).

But instead of

Iterator<Map.Entry<Player, Double>> itr = map.entrySet().iterator();
while (itr.hasNext()) {
    if (itr.next().getValue() <= radiusSquared) {
        itr.remove();
    }
}

you may simply use

map.values().removeIf(value -> value <= radiusSquared);

Even if you insist on having it as part of the collect operation, you can do it as postfix operation:

Map<Player, Double> dists = str.collect(
    Collectors.collectingAndThen(Collectors.toMap(p->p, p->calculate(p)),
    map -> { map.values().removeIf(value -> value <= radiusSquared); return map; }));

Avoiding to put unwanted entries in the first place is possible, but it implies manually retracing what the existing toMap collector does:

Map<Player, Double> dists = str.collect(
    HashMap::new,
    (m, p) -> { double value=calculate(p); if(value > radiusSquared) m.put(p, value); },
    Map::putAll);
like image 152
Holger Avatar answered Sep 24 '22 01:09

Holger