Що робить операція filter()
Вона приймає функціональний інтерфейс java.util.function.Predicate
🟢 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)
Кожна проміжна операція модифікує характеристики потоку:
- Прапорець
SIZED— СКИДАЄТЬСЯ (NOT_SIZED): Компілятор і конвеєр більше не знають точної кількості елементів, які вийдуть із фільтра. Термінальні операції (наприклад,toArray()) більше не можуть виділити масив точного розміру заздалегідь. - Прапорці
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.
🎯 Шпаргалка для інтерв’ю
Питання для швидкої перевірки
- Чи є операція
filter()проміжною або термінальною? Лінива вона чи жадібна? Відповідь:filter()— це проміжна (intermediate) лінива (lazy) операція. Вона не виконує жодних обчислень над елементами джерела доти, доки на конвеєрі не буде викликана термінальна операція. - Як
filter()впливає на характеристики стріма (SIZED,SORTED,DISTINCT)? Відповідь: ХарактеристикаSIZEDскидається (розмір результуючого потоку стає невідомим). Проте характеристикиSORTED(порядок сортування) таDISTINCT(унікальність) гарантовано зберігаються, оскільки видалення елементів не порушує порядок і не породжує дублікатів. - Що робить реалізація методу
accept(T value)в інтерфейсіSinkдля операціїfilter()? Відповідь: Вона виконує перевіркуif (predicate.test(value)) downstream.accept(value);. Якщо предикат істинний, елемент передається наступній стадії конвеєра; якщо хибний — виклик завершується, і елемент відкидається. - Що станеться, якщо в нескінченному стрімі
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”: Порядок елементів ніколи не порушується операцією фільтрації.