Как проверить, что Java 8 Stream имеет в нем два конкретных элемента?

Скажем, у меня есть List<Car>, и я хочу выполнить поиск по этому списку, чтобы убедиться, что у меня есть Civic AND a Focus. Если это очень просто, я могу просто применить OR на .filter(). Имейте в виду, что я не могу сделать filter().filter() для этого типа И.

Рабочее решение:

boolean hasCivic = reportElements.stream()
        .filter(car -> "Civic".equals(car.getModel()))
        .findFirst()
        .isPresent();

boolean hasFocus = reportElements.stream()
        .filter(car -> "Focus".equals(car.getModel()))
        .findFirst()
        .isPresent();

return hasCivic && hasFocus;

Но тогда я в основном обрабатываю список дважды. Я не могу применить && в фильтре, и я не могу сделать filter().filter().

Есть ли способ обработать поток один раз, чтобы найти, содержит ли список как автомобиль Civic, так и Focus?

ВАЖНОЕ ОБНОВЛЕНИЕ: Основная проблема с предоставленными решениями заключается в том, что все они гарантируют O (n), тогда как мое решение может быть выполнено после двух сравнений. Если в моем списке автомобилей говорят 10 миллионов автомобилей, тогда будет очень значительная стоимость исполнения. Мое решение, однако, не очень хорошо, но, возможно, это лучшее решение, разумное...

Ответ 1

Вы можете фильтровать поток на "Civic" or "Focus", а затем запустить коллекционер на getModel(), возвращая Set<String>. Затем вы можете проверить, содержит ли ваш набор оба ключа.

Set<String> models = reportElements.stream()
       .map(Car::getModel)
       .filter(model -> model.equals("Focus") || model.equals("Civic"))
       .collect(Collectors.toSet());
return models.contains("Focus") && models.contains("Civic");

Однако это обработает весь поток; он не будет "быстро преуспеть", если оба обнаружены.


Ниже приведен метод короткого замыкания с быстрым успехом. (Обновлено для включения комментариев и пояснений из комментариев ниже)

return reportElements.stream()
           .map(Car::getModel)
           .filter(model -> model.equals("Focus") || model.equals("Civic"))
           .distinct()
           .limit(2)
           .count() == 2;

Ломать потоковые операции по одному за раз, мы имеем:

           .map(Car::getModel)

Эта операция превращает поток автомобилей в поток моделей автомобилей. Мы делаем это для повышения эффективности. Вместо того чтобы называть car.getModel() несколько раз в разных местах в оставшейся части конвейера (дважды в filter(...) для проверки по каждой из желаемых моделей и снова для операции distinct()), мы применяем эту операцию отображения один раз. Обратите внимание, что это не создает "временную карту", ​​упомянутую в комментариях; он просто переводит автомобиль в модель автомобиля для следующего этапа трубопровода.

           .filter(model -> model.equals("Focus") || model.equals("Civic"))

Это фильтрует поток моделей автомобилей, позволяя пропускать только модели "Focus" и "Civic".

           .distinct()

Эта операция конвейера - это промежуточная операция с состоянием. Он запоминает каждую модель автомобиля, которую он видит во временном Set. (Вероятно, это временная карта, упомянутая в комментариях.) Только если модель не существует во временном наборе, будет ли он (a) добавлен в набор и (b) передан на следующий этап конвейера.

В этот момент в конвейере в потоке может быть не более двух элементов: "Фокус" или "Гражданский", либо ни один, ни тот и другой. Мы знаем это, потому что знаем, что filter(...) будет только передавать эти две модели, и мы знаем, что distinct() удалит любые дубликаты.

Однако этот конвейер потока сам не знает этого. Он будет продолжать передавать автомобильные объекты на этап map, который должен быть преобразован в строки модели, передать эти модели на этап filter и отправить на любой подходящий элемент на этап distinct. Он не может сказать, что это бесполезно, потому что он не понимает, что через алгоритм не может пройти ничего другого; он просто выполняет инструкции.

Но мы понимаем. Не более двух отдельных моделей могут проходить через стадию distinct(). Итак, мы следуем этому:

           .limit(2)

Это короткая замыкающая промежуточная операция состояния. Он поддерживает подсчет количества предметов, которые проходят, и после указанного количества он прекращает поток, заставляя все последующие элементы отбрасываться, даже не начиная по трубопроводу.

В этот момент в конвейере в потоке может быть не более двух элементов: "Фокус" или "Гражданский", либо ни один, ни тот и другой. Но если оба, то поток был усечен и находится в конце.

           .count() == 2;

Подсчитайте количество элементов, которые прошли через конвейер, и протестируйте его с нужным номером.

Если мы найдем обе модели, поток немедленно прекратится, count() вернет 2, и будет возвращен true. Если обе модели не присутствуют, конечно, поток обрабатывается до конца, count() будет возвращать значение меньше двух, и будет false.


Пример, используя бесконечный поток моделей. Каждая третья модель является "Civic", каждая 7-я модель является "Focus", остальные - "Model #":

boolean matched = IntStream.iterate(1, i -> i + 1)
    .mapToObj(i -> i % 3 == 0 ? "Civic" : i % 7 == 0 ? "Focus" : "Model "+i)
    .peek(System.out::println)
    .filter(model -> model.equals("Civic") || model.equals("Focus"))
    .peek(model -> System.out.println("  After filter:   " + model))
    .distinct()
    .peek(model -> System.out.println("  After distinct: " + model))
    .limit(2)
    .peek(model -> System.out.println("  After limit:    " + model))
    .count() == 2;
System.out.println("Matched = "+matched);

Вывод:

Model 1
Model 2
Civic
  After filter:   Civic
  After distinct: Civic
  After limit:    Civic
Model 4
Model 5
Civic
  After filter:   Civic
Focus
  After filter:   Focus
  After distinct: Focus
  After limit:    Focus
Matched = true

Обратите внимание, что 3 модели прошли через filter(), но только 2 прошли мимо distinct() и limit(). Что еще более важно, обратите внимание, что true был возвращен задолго до того, как был достигнут конец бесконечного потока моделей.


Обобщение решения, поскольку OP хочет что-то, что может работать с людьми, кредитными картами или IP-адресами и т.д., а критерии поиска, вероятно, не являются фиксированным набором из двух элементов:

Set<String> models = Set.of("Focus", "Civic");

return reportElements.stream()
           .map( Car::getModel )
           .filter( models::contains )
           .distinct()
           .limit( models.size() )
           .count() == models.size();

Здесь, при произвольном наборе models, может быть получено существование любого конкретного набора моделей автомобилей, не ограничиваясь только 2.

Ответ 2

Вы можете сделать:

reportElements.stream()
    .filter(car -> "Civic".equals(car.getModel()) || "Focus".equals(car.getModel()))
    .collect(Collectors.toMap(
            c -> c.getModel(),
            c -> c,
            (c1, c2) -> c1
    )).size() == 2;

или даже с Set

reportElements.stream()
    .filter(car -> "Civic".equals(car.getModel()) || "Focus".equals(car.getModel()))
    .map(car -> car.getModel())
    .collect(Collectors.toSet())
    .size() == 2;

и distinct

reportElements.stream()
    .filter(car -> "Civic".equals(car.getModel()) || "Focus".equals(car.getModel()))
    .map(car -> car.getModel())
    .distinct()
    .count() == 2L;

Ответ 3

Причина, по которой он "не чувствует себя хорошо", заключается в том, что вы вынуждаете поток API выполнять то, что он не хочет делать. Вам почти наверняка будет лучше традиционный цикл:

boolean hasFocus = false, hasCivic = false;
for (Car c : reportElements) {
    if ("Focus".equals(c.getModel())) hasFocus = true;
    if ("Civic".equals(c.getModel())) hasCivic = true;
    if (hasFocus & hasCivic) return true;
}
return false;