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

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

Вона приймає функціональний інтерфейс java.util.function.Predicate (метод boolean test(T t)): якщо предикат повертає true, елемент пропускається далі конвеєром; якщо false —...


🟢 Junior Level

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

filter(Predicate<? super T> predicate) — це проміжна (intermediate) лінива операція Stream API, призначена для відбору елементів потоку даних за заданою умовою.

Вона приймає функціональний інтерфейс java.util.function.Predicate<T> (метод boolean test(T t)): якщо предикат повертає true, елемент пропускається далі конвеєром; якщо false — елемент відкидається.

Операція є без збереження стану (Stateless): кожен елемент перевіряється автономно, незалежно від інших, що робить операцію винятково продуктивною та безпечною для паралельної обробки.


Базові приклади на Java 21

import java.util.List;
import java.util.Objects;

public class FilterBasicsDemo {

    record Employee(String name, int age, String department) {}

    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Alice", 28, "IT"),
            new Employee("Bob", 45, "HR"),
            new Employee("Charlie", 32, "IT"),
            new Employee("Diana", 19, "Sales")
        );

        // 1. Фільтрація за числовою та рядковою умовою
        List<String> itSeniors = employees.stream()
            .filter(e -> "IT".equals(e.department()))
            .filter(e -> e.age() >= 30)
            .map(Employee::name)
            .toList();

        System.out.println("IT співробітники 30+: " + itSeniors); // [Charlie]

        // 2. Ідіоматична фільтрація null-значень
        List<String> rawData = List.of("java", null, "stream", null, "api");
        List<String> cleanedData = rawData.stream()
            .filter(Objects::nonNull) // Захист від NullPointerException
            .map(String::toUpperCase)
            .toList();

        System.out.println("Cleaned: " + cleanedData); // [JAVA, STREAM, API]
    }
}

🟡 Middle Level

Внутрішня будова: Sink.ChainedReference в OpenJDK

Усередині бібліотеки java.util.stream операція filter не створює проміжних колекцій. Вона загортає нижчележачий обробник конвеєра в клас Sink.ChainedReference:

// Спрощена реалізація всередині OpenJDK (ReferencePipeline.java):
@Override
public Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> downstream) {
    return new Sink.ChainedReference<P_OUT, P_OUT>(downstream) {
        @Override
        public void accept(P_OUT u) {
            // Якщо предикат істинний - проштовхуємо елемент наступному етапу (downstream)
            if (predicate.test(u)) {
                downstream.accept(u);
            }
            // Якщо хибний - елемент просто ігнорується (відкинутий)
        }
    };
}

Архітектурний принцип Early Filtering (Рання фільтрація)

Порядок стадій у конвеєрі критично впливає на продуктивність. Фільтруючі операції повинні розташовуватися якомога ближче до джерела даних, до ресурсомістких трансформацій:

// ❌ ПОГАНО: важкий мапінг (парсинг JSON, криптографія) виконується для ВСІХ 1 000 000 елементів!
stream.map(this::heavyCryptographicHash)
      .filter(hash -> hash.startsWith("0000"))
      .toList();

// ✅ ОПТИМАЛЬНО: спочатку відсікаємо 99% зайвих даних легким предикатом
stream.filter(this::isCandidate)
      .map(this::heavyCryptographicHash)
      .filter(hash -> hash.startsWith("0000"))
      .toList();

Композиція предикатів: Кілька filter() vs Predicate.and()

У Java є два шляхи об’єднання умов:

// Варіант 1: Ланцюжок незалежних викликів filter()
stream.filter(Objects::nonNull)
      .filter(user -> user.age() > 18)
      .filter(User::isActive);

// Варіант 2: Єдиний композитний предикат через функціональні методи
Predicate<User> validAdult = Objects::nonNull;
validAdult = validAdult.and(u -> u.age() > 18).and(User::isActive);

stream.filter(validAdult);

Порівняння підходів:

  • Ланцюжок filter() (Варіант 1): Найбільш читабельний, ідіоматичний для Java. Завдяки оптимізації Loop Fusion та JIT-інлайнінгу компілятор C2 стискає ланцюжок у єдиний машинний цикл без втрати продуктивності.
  • Композиція Predicate.and() (Варіант 2): Переважніша, коли предикати формуються динамічно (наприклад, у конструкторах запитів або UI-фільтрах).

🔴 Senior Level

Вплив на прапорці характеристик конвеєра (Stream Flags)

Кожна проміжна операція модифікує характеристики потоку:

  1. Прапорець SIZED — СКИДАЄТЬСЯ (NOT_SIZED): Компілятор і конвеєр більше не знають точної кількості елементів, які вийдуть із фільтра. Термінальні операції (наприклад, toArray()) більше не можуть виділити масив точного розміру заздалегідь.
  2. Прапорці SORTED та DISTINCT — ЗБЕРІГАЮТЬСЯ: Фільтрація видаляє елементи, але не змінює їхній відносний порядок і не створює дублікатів. Якщо вихідний стрім був відсортований (SORTED), конвеєр після filter() зберігає ознаку відсортованості, позбавляючи наступні стадії повторного сортування!

Симбіоз із Short-Circuiting операціями

У зв’язці з операціями короткого замикання (findFirst(), findAny(), limit()) операція filter не тестує всю колекцію:

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

Integer firstEven = numbers.stream()
    .filter(n -> {
        System.out.println("Тестуємо: " + n);
        return n % 2 == 0;
    })
    .findFirst()
    .orElseThrow();

Вивід у консолі:

Тестуємо: 1
Тестуємо: 2

Щойно елемент 2 пройшов предикат, операція findFirst() сигналізує cancellationRequested() == true, і перевірка елементів 3..10 навіть не починається.


Архітектурний антипатерн: In-Memory Database Filtering

Однією з найбільш руйнівних практик у мікросервісах є заміна фільтрації бази даних на фільтрацію в Java:

// ❌ КАТАСТРОФА ДЛЯ ПРОДУКТИВНОСТІ:
// Вивантажуємо 500 000 рядків із PostgreSQL у пам'ять Java-сервісу через Hibernate...
List<Order> orders = orderRepository.findAll().stream()
    .filter(o -> o.getStatus() == OrderStatus.NEW)
    .toList(); // OutOfMemoryError, гігантський трафік БД, мережевий затор

// ✅ ПРАВИЛЬНО: Перенесення фільтрації на рівень СУБД (SQL Pushdown)
List<Order> orders = orderRepository.findAllByStatus(OrderStatus.NEW);

Фільтрація у Stream API призначена для роботи з локальними колекціями в пам’яті або потоками обчислень, але не повинна підміняти індексовані SQL-запити WHERE.


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

Питання для швидкої перевірки

  1. Чи є операція filter() проміжною або термінальною? Лінива вона чи жадібна? Відповідь: filter() — це проміжна (intermediate) лінива (lazy) операція. Вона не виконує жодних обчислень над елементами джерела доти, доки на конвеєрі не буде викликана термінальна операція.
  2. Як filter() впливає на характеристики стріма (SIZED, SORTED, DISTINCT)? Відповідь: Характеристика SIZED скидається (розмір результуючого потоку стає невідомим). Проте характеристики SORTED (порядок сортування) та DISTINCT (унікальність) гарантовано зберігаються, оскільки видалення елементів не порушує порядок і не породжує дублікатів.
  3. Що робить реалізація методу accept(T value) в інтерфейсі Sink для операції filter()? Відповідь: Вона виконує перевірку if (predicate.test(value)) downstream.accept(value);. Якщо предикат істинний, елемент передається наступній стадії конвеєра; якщо хибний — виклик завершується, і елемент відкидається.
  4. Що станеться, якщо в нескінченному стрімі Stream.iterate(0, i -> i + 1) викликати filter(i -> i < 0).findFirst()? Відповідь: Метод зависне в нескінченному циклі. Оскільки в стрімі генеруються лише невід’ємні числа ($0, 1, 2 \dots$), предикат i < 0 ніколи не поверне true, і конвеєр нескінченно споживатиме CPU в марних пошуках першого збігу.

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

  • ❌ Твердження: “Метод filter() одразу створює відфільтрований список елементів”: Метод лінивий і обчислює елементи потоково по одному за раз.
  • ❌ Мутація зовнішніх змінних усередині предиката: Порушує принцип чистоти функцій і призводить до станів перегонів у паралельних стрімах.
  • ❌ Фільтрація сотень тисяч сутностей у Java замість SQL-запиту WHERE: Призводить до вичерпання купи (OutOfMemoryError) та деградації мережі.
  • ❌ Твердження: “filter() скидає порядок сортування SORTED”: Порядок елементів ніколи не порушується операцією фільтрації.

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