2

I am looking for some object in map:

mapObjects.entrySet().stream().map(map -> map.getValue()).filter(predicateA)

When I find more then one item, I want to specify a second predicate to filter on some additional attribute. Is there some way I can do this in just one iteration of stream, or do I need to iterate once and when count > 1 then I need to iterate a second time with another predicate ?

For example, say I have list of persons. First I am looking for name=John. When there is more than one John, I look for surname=Smith. Now I don't care if there is more than one and I just take the first.

Didier L
  • 18,905
  • 10
  • 61
  • 103
hudi
  • 15,555
  • 47
  • 142
  • 246
  • You can use a stateful `Predicate` - but note that that won't work with a parallel `Stream`. – Boris the Spider Sep 30 '16 at 13:40
  • I don't think this is a good idea. You should index your data by your predicate, and then detect duplicates. This will be more efficient in the long run. – Jazzwave06 Sep 30 '16 at 13:45
  • 3
    You can replace `mapObjects.entrySet().stream().map(map -> map.getValue())` with `mapObjects.values().stream()`. – VGR Sep 30 '16 at 14:48
  • @BoristheSpider A stateful predicate would work, the state just needs to be atomic. – 4castle Oct 01 '16 at 03:09

3 Answers3

1

It could be done by first filtering the Person instances by name then grouping by surname. The result will be put into a LinkedHashMap in order to get the first match if there is no full match (name and surname), finally we rely on Map#getOrDefault(key, defaultValue) to get the full match if it exists otherwise it will get the first entry as default value.

Map<String, Person> map = mapObjects.values().stream()
    .filter(p -> Objects.equals(p.getName(), name))
    .collect(
        Collectors.groupingBy(
            Person::getSurname, 
            LinkedHashMap::new, 
            Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))
        )
    );

Optional<Person> result =
    map.isEmpty() ?
        Optional.absent() :
        Optional.of(
            map.getOrDefault(surname, map.entrySet().iterator().next().getValue())
        );

This way you iterate only once to get your result and you don't use a stateful Predicate.

Nicolas Filotto
  • 43,537
  • 11
  • 94
  • 122
0

You can use a reduction operation which prioritizes the second predicate when possible:

mapObjects.values().stream()
    .filter(predicateA)
    .reduce((acc, obj) -> predicateB.test(obj) ? obj : acc)
    .ifPresent(doThing);

Unfortunately, the reduction can't be short-circuited. If this is important, keep reading.


You could give predicateB a wrapping class which only tries to return true if it never has before and the argument meets the criteria. Here is an atomic implementation so that it still works in parallel streams.

public class ShortCircuitPredicate<T> implements Predicate<T> {
    private final AtomicBoolean hasBeenTrue;
    private final Predicate<T> predicate;
    private ShortCircuitPredicate(Predicate<T> pred) {
        hasBeenTrue = new AtomicBoolean(false);
        predicate = pred;
    }

    public static <T> ShortCircuitPredicate<T> of(Predicate<T> pred) {
        return new ShortCircuitPredicate<>(pred);
    }

    @Override
    public boolean test(T t) {
        return hasBeenTrue.get()
            ? false
            : predicate.test(t) && hasBeenTrue.compareAndSet(false, true);
    }
}

You can wrap predicateB using ShortCircuitPredicate.of(predicateB).

4castle
  • 32,613
  • 11
  • 69
  • 106
0

I am not convinced this problem is a good fit for using the stream API, but an option would be to rely on Stream#peek() to keep a reference to one of the elements that matches the first filter, if none match the second one:

    List<Person> people = ...
    Person[] holder = new Person[1];

    Person result = people.stream()
            .filter(p -> p.getName().equals("John"))
            .peek(p -> holder[0] = p)
            .filter(p -> p.getSurname().equals("Smith"))
            .findAny()
            .orElse(holder[0]);

This is short-circuiting in case there is any match for both filters. On the other hand, peek() and the second filter will have to be executed on all matches of the first predicate before findAny() returns an empty optional. Consequently, the holder will always be filled, except when there is no match to the first predicate.

I suggested carefully reading In Java streams is peek really only for debugging? though, and make your own opinion on whether this is an appropriate option for your specific case.

Community
  • 1
  • 1
Didier L
  • 18,905
  • 10
  • 61
  • 103