🌊 Розділ 8 · Питання #25

Що таке операції anyMatch(), allMatch(), noneMatch()

Класичне запитання на технічних співбесідах: що поверне кожен із трьох методів, якщо стрім не містить жодного елемента (Stream.empty())?


🟢 Junior Level

Коротка відповідь для співбесіди (30 секунд)

anyMatch(), allMatch() та noneMatch() — це термінальні операції короткого замикання (short-circuiting), які перевіряють елементи потоку на відповідність заданому предикату Predicate<T> і повертають примітивний результат boolean:

  • anyMatch(Predicate) — повертає true, якщо хоча б один елемент задовольняє умову. Негайно завершує роботу при першому true.
  • allMatch(Predicate) — повертає true, якщо всі елементи задовольняють умову. Негайно завершує роботу при першому false.
  • noneMatch(Predicate) — повертає true, якщо жоден елемент не задовольняє умову. Негайно завершує роботу при першому true.

Приклади використання на Java 21

import java.util.List;

public class MatchOperationsDemo {

    public record Account(String iban, boolean blocked, double balance) {}

    public static void main(String[] args) {
        List<Account> accounts = List.of(
            new Account("DE101", false, 1500.0),
            new Account("DE102", false, 240.0),
            new Account("DE103", true, -50.0)
        );

        // 1. anyMatch: Чи є хоч один заблокований рахунок?
        boolean hasBlocked = accounts.stream()
            .anyMatch(Account::blocked);
        System.out.println("Є заблоковані: " + hasBlocked); // true

        // 2. allMatch: Чи всі рахунки мають додатний баланс?
        boolean allPositive = accounts.stream()
            .allMatch(a -> a.balance() > 0);
        System.out.println("Усі в плюсі: " + allPositive); // false

        // 3. noneMatch: Чи правда, що немає рахунків з від'ємним балансом нижче -1000?
        boolean noExtremeDebt = accounts.stream()
            .noneMatch(a -> a.balance() < -1000.0);
        System.out.println("Немає екстремальних боргів: " + noExtremeDebt); // true
    }
}

🟡 Middle Level

Поведінка на порожньому стрімі: концепція «Порожньої істинності» (Vacuous Truth)

Класичне запитання на технічних співбесідах: що поверне кожен із трьох методів, якщо стрім не містить жодного елемента (Stream.empty())?

Stream<String> emptyStream = Stream.empty();

boolean any  = Stream.empty().anyMatch(s -> true);  // false
boolean all  = Stream.empty().allMatch(s -> false); // true! (Навіть із предикатом s -> false)
boolean none = Stream.empty().noneMatch(s -> true); // true!

Математичне обґрунтування:

  1. anyMatch ($\exists x \in S : P(x)$): «Чи існує в множині $S$ хоча б один елемент, для якого істинний предикат $P$?». У порожній множині $\emptyset$ елементів немає взагалі, отже, відповідний елемент існувати не може $\rightarrow$ false.
  2. allMatch ($\forall x \in S : P(x)$): «Чи для всіх елементів множини $S$ істинний предикат $P$?». У математичній логіці твердження вважається істинним, якщо неможливо навести жодного спростовного контрприкладу (Vacuous Truth — порожня істинність). Контрприкладів у порожній множині немає $\rightarrow$ true.
  3. noneMatch ($\neg \exists x \in S : P(x)$): «Чи правда, що жоден елемент множини $S$ не задовольняє предикат $P$?». Оскільки в множині немає жодного елемента, жоден елемент не може порушити це твердження $\rightarrow$ true.

Ефективність за пам’яттю: anyMatch проти filter().findFirst()

Частий антипатерн у коді:

// ❌ Надлишкові алокації:
boolean exists = list.stream().filter(predicate).findFirst().isPresent();

// ✅ Оптимально:
boolean exists = list.stream().anyMatch(predicate);
  • Варіант із findFirst() створює в купі об’єкт-обгортку Optional<T>, а також проміжний вузол конвеєра filter.
  • anyMatch() працює безпосередньо через булевий прапорець акумулятора без створення об’єктів у heap ($O(1)$ allocations).

🔴 Senior Level

Внутрішня будова: MatchOps, MatchKind та ForkJoinPool

У вихідному коді OpenJDK усі три операції реалізовані через єдину абстракцію MatchOps та перерахування MatchKind:

enum MatchKind {
    ANY(true, true),      // stopOnPredicateMatches = true, shortCircuitResult = true
    ALL(false, false),    // stopOnPredicateMatches = false, shortCircuitResult = false
    NONE(true, false);    // stopOnPredicateMatches = true, shortCircuitResult = false

    final boolean stopOnPredicateMatches;
    final boolean shortCircuitResult;
}

Механіка в паралельних стрімах (parallelStream()):

  1. Обчислення оформлюються у вигляді дерева задач MatchTask (нащадок ForkJoinTask).
  2. Задачі розділяють спільний атомарний холдер результату (SharedResult).
  3. Щойно один із паралельних потоків знаходить замикаючу умову (наприклад, воркер 2 знайшов елемент false для allMatch):
    • Він виконує CAS-запис у SharedResult.
    • Задача викликає метод cancelLaterNodes(), розсилаючи сигнал скасування іншим дочірнім підзадачам у ForkJoinPool.
    • Термінальна операція негайно повертає підсумковий boolean, не чекаючи завершення інших воркерів.

Поведінка на нескінченних стрімах (Infinite Streams)

  1. Stream.generate(...).anyMatch(predicate):
    • Безпечно завершиться і поверне true, щойно генератор видасть перший елемент, який задовольняє умову.
  2. Stream.iterate(1, n -> n + 1).allMatch(n -> n > 0):
    • Зависне в нескінченному циклі (100% CPU). Оскільки предикат n > 0 істинний для всіх натуральних чисел, алгоритм буде вічно шукати хоча б один контрприклад false, який ніколи не з’явиться.
  3. Stream.iterate(1, n -> n + 1).allMatch(n -> n < 5):
    • Швидко завершиться і поверне false на числі 5, оскільки знайдено контрприклад.

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

«anyMatch, allMatch та noneMatch — термінальні операції короткого замикання, які повертають boolean. anyMatch зупиняється при першому true, allMatch — при першому false, noneMatch — при першому true. На порожньому стрімі anyMatch повертає false, а allMatch та noneMatch повертають true (математичний принцип Vacuous Truth). Вони працюють швидше, ніж filter().findFirst().isPresent(), оскільки не алокують об’єкт Optional. У parallelStream при знаходженні короткого замикання задача MatchTask скасовує решту потоків через спільний холдер результату.»


Поведінка операцій на різних стрімах

Метод Умова зупинки Результат на Stream.empty() Поведінка на нескінченному стрімі, якщо умову НЕ знайдено
anyMatch(p) Перший p.test(x) == true 🔴 false 💥 Нескінченний цикл (шукає true)
allMatch(p) Перший p.test(x) == false 🟢 true 💥 Нескінченний цикл (шукає false)
noneMatch(p) Перший p.test(x) == true 🟢 true 💥 Нескінченний цикл (шукає true)

Топ-4 каверзних запитання на інтерв’ю

1. Що поверне Stream.empty().allMatch(x -> false) і чому?

  • Метод поверне true. За законами математичної логіки твердження з квантором загальності ($\forall x \in S : P(x)$) над порожньою множиною $S = \emptyset$ вважається істинним, оскільки не існує жодного контрприкладу в множині, який порушував би предикат (принцип Vacuous Truth).

2. У чому різниця між stream.anyMatch(p) та stream.filter(p).findAny().isPresent()?

  • Семантично вони еквівалентні, але anyMatch(p) ефективніший: він не створює проміжний вузол конвеєра filter і не алокує об’єкт-контейнер Optional<T> у купі (Heap), безпосередньо повертаючи примітивний boolean.

3. Як перериваються інші паралельні потоки в parallelStream().allMatch() при знаходженні false?

  • Використовується внутрішній клас MatchTask. Перший потік, що виявив елемент, який не задовольняє предикат (false), атомарно записує прапорець завершення у спільний об’єкт SharedResult і викликає метод скасування cancelLaterNodes(), перериваючи обробку черг у ForkJoinPool.

4. Чи може allMatch() гарантувати суворий порядок перевірки елементів?

  • У послідовному стрімі елементи перевіряються суворо в порядку джерела (Encounter Order) до першого false.
  • У паралельному стрімі елементи перевіряються в різних потоках одночасно. Якщо кілька елементів порушують умову, стрім перерветься на тому елементі, який першим встигне обчислити будь-який із вільних робочих потоків.

Типові помилки та Red Flags

  • 🚩 Впевненість у тому, що allMatch() на порожньому стрімі поверне false.
  • 🚩 Використання filter().findFirst().isPresent() замість лаконічного та швидкого anyMatch().
  • 🚩 Запуск allMatch() або noneMatch() на нескінченному стрімі з предикатом, що не має точок короткого замикання.
  • 🚩 Розміщення важких побічних ефектів усередині предикатів match-операцій.

Пов’язані теми