Как применить несколько фильтров в потоке Java?

Я должен фильтровать коллекцию объектов по карте, в которой содержатся пары ключевых значений имен полей объектов и значений полей. Я пытаюсь применить все фильтры с помощью stream(). Filter().

Объекты на самом деле являются JSON, поэтому Map содержит имена своих переменных, а также значение, которое они должны содержать для того, чтобы их можно было принять, но для простоты и потому, что он не имеет отношения к вопросу, я написал простой Testclass для имитации поведение:

public class TestObject {

  private int property1;
  private int property2;
  private int property3;

  public TestObject(int property1, int property2, int property3) {
      this.property1 = property1;
      this.property2 = property2;
      this.property3 = property3;
  }

  public int getProperty(int key) {
      switch(key) {
          case 1: return property1;
          case 2: return property2;
          default: return property3;
      }
  }
}

То, что я пробовал до сих пор:

public static void main(String[] args) {
    List<TestObject> list = new ArrayList<>();
    Map<Integer, Integer> filterMap = new HashMap<>();
    list.add(new TestObject(1, 2, 3));
    list.add(new TestObject(1, 2, 4));
    list.add(new TestObject(1, 4, 3));
    filterMap.put(3, 3); //Filter property3 == 3
    filterMap.put(2, 2); //Filter property2 == 2

    //Does not apply the result
    filterMap.forEach((key, value) -> list.stream()
            .filter(testObject -> testObject.getProperty(key) == value)
            .collect(Collectors.toList())
    );
    /* Gives error: boolean can not be converted to void
    list = list.stream()
            .filter(testObject -> filterMap.forEach((key, value) -> testObject.getProperty(key) == value))
            .collect(Collectors.toList()
            );
    */
    //Printing result

    list.forEach(obj -> System.out.println(obj.getProperty(1) + " " + obj.getProperty(2) + " " + obj.getProperty(3)));
}

Сначала я попытался сначала перенести карту First и первую коллекцию, но оба решения не работали должным образом. Желательным результатом этого примера будет только печать объекта со значениями property1 = 1, property2 = 2 и property3 = 3.

Как я могу применить все фильтры правильно, как если бы вы поместили их один за другим в код с фиксированным количеством фильтров?

При известном количестве фильтров:

list.stream().filter(...).filter(...)

Редактировать:

Sweeper очень хорошо подвел мой вопрос в своем ответе, поэтому просто для разъяснения (и, вероятно, будущих читателей) здесь: я хочу сохранить все объекты, удовлетворяющие всем фильтрам.

Ответ 1

Я полагаю, вы хотите сохранить все TestObjects которые удовлетворяют всем условиям, указанным на карте?

Это сделает работу:

List<TestObject> newList = list.stream()
        .filter(x ->
                filterMap.entrySet().stream()
                        .allMatch(y ->
                                x.getProperty(y.getKey()) == y.getValue()
                        )
        )
        .collect(Collectors.toList());

Перевод на английский,

filter список list, сохранив все элементы x которые:

  • все пары значений ключа y filterMap должны удовлетворять:
    • x.getProperty(y.getKey()) == y.getValue()

(Я не думаю, что я сделал хорошую работу, чтобы сделать этот человек доступным для чтения...) Если вы хотите получить более читаемое решение, я рекомендую ответить Джерейн Стенбейке.

Ответ 2

Чтобы применить переменное число шагов фильтра к потоку (который станет известен только во время выполнения), вы можете использовать цикл для добавления шагов фильтра.

Stream<TestObject> stream = list.stream();
for (Predicate<TestObject> predicate: allPredicates) {
  stream = stream.filter(predicate);
}
list = stream.collect(Collectors.toList());

Ответ 3

Более общий подход заключается в создании мультифильтра (Predicate), который объединяется с использованием Predicate.and(...) или Predicate.or(...). Это применимо ко всему, используя Predicate - в первую очередь Stream и Optional.
Поскольку результатом является сам Predicate, можно продолжить с Predicate.and(...) или Predicate.or(...) или с построением более сложных предикатов с использованием MultiPredicate.

class MultiPredicate {

    public static <T> Predicate<T> matchingAll(Collection<Predicate<T>> predicates) {
        Predicate<T> multiPredicate = to -> true;
        for (Predicate<T> predicate : predicates) {
            multiPredicate = multiPredicate.and(predicate);
        }

        return multiPredicate;

    }

    @SafeVarargs
    public static <T> Predicate<T> matchingAll(Predicate<T> first, Predicate<T>... other) {
        if (other == null || other.length == 0) {
            return first;
        }

        Predicate<T> multiPredicate = first;
        for (Predicate<T> predicate : other) {
            multiPredicate = multiPredicate.and(predicate);
        }

        return multiPredicate;
    }

    public static <T> Predicate<T> matchingAny(Collection<Predicate<T>> predicates) {
        Predicate<T> multiPredicate = to -> false;
        for (Predicate<T> predicate : predicates) {
            multiPredicate = multiPredicate.or(predicate);
        }

        return multiPredicate;

    }

    @SafeVarargs
    public static <T> Predicate<T> matchingAny(Predicate<T> first, Predicate<T>... other) {
        if (other == null || other.length == 0) {
            return first;
        }

        Predicate<T> multiPredicate = first;
        for (Predicate<T> predicate : other) {
            multiPredicate = multiPredicate.or(predicate);
        }

        return multiPredicate;
    }

}

Обратившись к вопросу:

public static void main(String... args) {
    List<TestObject> list = new ArrayList<>();
    Map<Integer, Integer> filterMap = new HashMap<>();
    list.add(new TestObject(1, 2, 3));
    list.add(new TestObject(1, 2, 4));
    list.add(new TestObject(1, 4, 3));
    filterMap.put(3, 3); // Filter property3 == 3
    filterMap.put(2, 2); // Filter property2 == 2

    List<Predicate<TestObject>> filters = filterMap.entrySet().stream()
            .map(filterMapEntry -> mapToFilter(filterMapEntry))
            .collect(Collectors.toList());

    Predicate<TestObject> multiFilter = MultiPredicate.matchingAll(filters);
    List<TestObject> filtered = list.stream()
            .filter(multiFilter)
            .collect(Collectors.toList());
    for (TestObject to : filtered) {
        System.out.println("(" + to.getProperty(1) + "|" + to.getProperty(2) + "|" + to.getProperty(3) + ")");
    }

}

private static Predicate<TestObject> mapToFilter(Entry<Integer,Integer> filterMapEntry) {
    return to -> to.getProperty(filterMapEntry.getKey()) ==  filterMapEntry.getValue();
}

В этом случае все фильтры должны совпадать. Результат:

(1|2|3)  

Если мы используем MultiPredicate.matchingAny(...) результат:

(1|2|3)  
(1|2|4)  
(1|4|3)  

Ответ 4

Он не имеет никакого отношения к фильтру. На самом деле фильтр никогда не работает в соответствии с вашим кодом. смотреть на

//Does not apply the result
filterMap.forEach((key, value) -> list.stream()
        .filter(testObject -> testObject.getProperty(key) == value)
        .collect(Collectors.toList())
);

Список был отфильтрован, но здесь ничего не изменилось. Ни один элемент не был удален, а также не был изменен адрес объекта. Попробовать removeIf

// Does not apply the result
filterMap.forEach((key, value) -> list.removeIf(testObject -> testObject.getProperty(key) != value));

выход

1 2 3