Filter on original stream if previous filter resulted in an empty stream

Viewed 717

Looking to come up with a way to apply a filter on the original stream if a previous filter returned an empty stream. For instance:

  • Given a stream of numbers
  • Get and return all even numbers
  • If none, get and return all numbers that are a multiple of 3
  • If none, get and return all numbers that are a multiple of 5
  • etc..

A trivial approach to this would be:

List<Integer> numbers = ...

List<Integer> filteredNums = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

if (filteredNums.isEmpty()) {
    filteredNums = numbers.stream()
        .filter(n -> n % 3 == 0)
        .collect(Collectors.toList());
}

if (filteredNums.isEmpty()) {
    filteredNums = numbers.stream()
        .filter(n -> n % 5 == 0)
        .collect(Collectors.toList());
}

Given a list of numbers such as 1, 3, 5, 15, 21, would result in 3,15,21.

My example with numbers is a simplified version of what I'm trying to do, but conveys the same concept. I could create a Collector to do this, but in general I feel there is something simple I'm missing. Ideally I'd like the solution to be able to be used in a stream so I can continue performing additional stream operations on it. In this scenario, maybe I'd want to reduce it to a sum or map where I multiple each number and the collect into a list.

3 Answers

You could have a stream with your predicates and filter your collection with each one of them, until the result of the filter operation returns a non empty list:

Stream<Predicate<Integer>> predicates = Stream.of(n -> n % 2 == 0,
                                                  n -> n % 3 == 0,
                                                  n -> n % 5 == 0);

List<Integer> filteredNums = predicates.map(
        predicate -> numbers.filter(predicate)
                            .collect(Collectors.toList()))
    .dropWhile(Collection::isEmpty)
    .findFirst()
    .orElse(Collections.emptyList());

This transforms each predicate to a filtered list (by applying the current predicate to the original list). Filtered empty lists are dropped, until a predicate produces a non empty list or the predicates are all consumed.

Here is a way where you can combine your conditions :

List<Integer> divs = Arrays.asList(2, 3, 5);
List<Integer> filteredNums = divs.stream()
        .filter(d -> numbers.stream().anyMatch(n -> n % d == 0))
        .findFirst()
        .map(d -> numbers.stream().filter(n -> n % d == 0).collect(Collectors.toList()))
        .orElse(Collections.EMPTY_LIST);

The idea is to create a list with the dividers that you want to use, and then find the first divider if it is present then filter all the numbers or return an Empty list

List<Predicate<Integer>> predicates = Arrays.asList(n -> n % 2 == 0,
            n -> n % 3 == 0,
            n -> n % 5 == 0);

    List<Integer> numbers = Arrays.asList(11,12,15,4,43);

    List<Integer> integers = predicates.stream()
            .map(p -> numbers.stream().filter(n -> p.test(n)).collect(Collectors.toList()))
            .filter(l -> !l.isEmpty())
            .findFirst().orElse(Collections.emptyList());
Related