Питання 3 · Розділ 8

Що робить операція filter()?

Приймає Predicate — функціональний інтерфейс з методом boolean test(T t).

Мовні версії: English Russian Ukrainian

🟢 Junior Level

filter(Predicate) — це проміжна операція, яка залишає лише ті елементи, які задовольняють умову.

Чому краще за if всередині циклу: filter можна комбінувати в ланцюжки, міняти порядок операцій і легко паралелити без зміни логіки. if всередині циклу — жорстко зашитий в імперативний стиль.

Приймає Predicate<T> — функціональний інтерфейс з методом boolean test(T t).

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Залишаємо лише парні числа
List<Integer> even = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
// Результат: [2, 4, 6, 8, 10]

// Фільтрація null-елементів
list.stream()
    .filter(Objects::nonNull)
    .forEach(System.out::println);

Важливо: filter сам по собі не запускає обробку — потрібна terminal операція.

🟡 Middle Level

Внутрішня реалізація

filter створює Sink.ChainedReference, метод accept якого:

Sink.ChainedReference — внутрішній клас, який огортає наступний Sink у ланцюжку і викликає його accept() лише якщо Predicate повертає true.

public void accept(T t) {
    if (predicate.test(t)) {
        downstream.accept(t); // передаємо далі
    }
    // інакше — елемент "поглинається"
}

Stateless природа

Фільтр не залежить від інших елементів — кожен обробляється незалежно. Це робить filter ідеальним для parallelStream.

Рання фільтрація (Early Filtering)

Чим раніше відсієте зайві дані, тим менше роботи у наступних дорогих операцій:

// ДОБРЕ — фільтр на початку
stream.filter(Objects::nonNull)
      .filter(s -> s.startsWith("A"))
      .map(expensiveMapping)

// ПОГАНО — expensiveMapping викличеться для всіх елементів
stream.map(expensiveMapping)
      .filter(Objects::nonNull)

Ланцюжки фільтрів vs складний предикат

// Варіант А — читабельніший
stream.filter(Objects::nonNull)
      .filter(s -> s.startsWith("A"))

// Варіант Б — трохи швидший
stream.filter(s -> s != null && s.startsWith("A"))

Для більшості випадків пріоритетніша читабельність (Варіант А).

🔴 Senior Level

Predicate Complexity та продуктивність

Якщо предикат виконує важку логіку (регулярки, виклик зовнішнього кешу), він буде викликаний для кожного елемента. Кешуйте результати важких обчислень.

Взаємодія з short-circuiting

filter працює в парі з операціями короткого замикання (findFirst, limit). Якщо фільтр знайшов відповідний елемент, конвеєр зупиниться — решта елементів не обробляються.

Null-checks як стандарт де-факто

filter(Objects::nonNull) — стандартний патерн для очищення даних перед обробкою, запобігає NPE у наступних map.

Edge Cases

  • Side Effects у предикаті: Ніколи не змінюйте зовнішні змінні всередині filter — це порушує функціональну парадигму і призведе до багів у паралельних стрімах
  • Optional.filter: У класу Optional теж є filter(), працює аналогічно — перетворює заповнений Optional на порожній при неспівпадінні умови

Діагностика

Filtering Metrics: Якщо фільтр відсіює 99.9% даних у Java-коді — перенесіть фільтрацію на рівень SQL-запиту.

Logging з peek():

stream.peek(e -> log.debug("Before: " + e))
      .filter(predicate)
      .peek(e -> log.debug("After: " + e))

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

Обов’язково знати:

  • filter(Predicate) — проміжна операція, залишає елементи, що задовольняють умову
  • Stateless — кожен елемент обробляється незалежно, ідеально для parallelStream
  • Рання фільтрація: чим раніше filter, тим менше роботи у наступних дорогих операцій
  • Працює з short-circuit операціями (findFirst, limit) — конвеєр зупиняється при знаходженні
  • filter(Objects::nonNull) — стандартний патерн для захисту від NPE
  • Фільтр сам не запускає обробку — потрібна terminal операція

Часті уточнюючі запитання:

  • filter vs if всередині циклу? — filter декларативний, легко комбінувати і паралелити
  • Ланцюжок фільтрів чи один складний предикат? — Ланцюжок читабельніший, один предикат трохи швидший
  • Що якщо предикат важкий? — Кешуйте результати, викликається для кожного елемента
  • Чи можна змінювати зовнішні змінні в предикаті? — Ні, це порушує функціональну парадигму

Червоні прапорці (НЕ говорити):

  • «filter одразу обробляє всі елементи» — ні, лінивий до terminal операції
  • «Side effects у предикаті — це нормально» — ні, призводить до багів у паралельних стрімах
  • «filter сортує елементи» — ні, лише відфільтровує, порядок зберігається
  • «Якщо фільтр відсіює 99% — це ок в Java-коді» — краще перенести на рівень SQL

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

  • [[2. В чому різниця між intermediate та terminal операціями]]
  • [[4. Що робить операція map()]]
  • [[9. Що таке паралельні стріми]]
  • [[5. Що робить операція collect()]]