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

Що роблять операції findFirst() та findAny()

Обидві операції — findFirst() та findAny() — є термінальними операціями короткого замикання (short-circuiting), які знаходять елемент у потоці даних і повертають його обгорнутим...


🟢 Junior Level

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

Обидві операції — findFirst() та findAny() — є термінальними операціями короткого замикання (short-circuiting), які знаходять елемент у потоці даних і повертають його обгорнутим у контейнер Optional<T> (щоб безпечно обробити випадок, коли потік порожній):

  • findFirst() — повертає суворо перший елемент відповідно до порядку обходу джерела (Encounter Order).
  • findAny() — повертає будь-який відповідний елемент. В однопотоковому стрімі найчастіше повертає той самий перший елемент, але в багатопотоковому parallelStream() повертає елемент із того потоку ForkJoinPool, який фінішував першим.

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

import java.util.List;
import java.util.Optional;

public class FindFirstFindAnyDemo {

    public static void main(String[] args) {
        List<String> names = List.of("Іван", "Олексій", "Анна", "Борис");

        // 1. findFirst: суворо перше ім'я на літеру "О"
        Optional<String> firstA = names.stream()
            .filter(name -> name.startsWith("О"))
            .findFirst();

        System.out.println("Перше на 'О': " + firstA.orElse("Не знайдено")); // Олексій

        // 2. findAny: будь-яке ім'я на літеру "О"
        Optional<String> anyA = names.stream()
            .filter(name -> name.startsWith("О"))
            .findAny();

        System.out.println("Будь-яке на 'О': " + anyA.orElse("Не знайдено")); // Олексій (у seq)

        // 3. Обробка порожнього результату
        Optional<String> notFound = names.stream()
            .filter(name -> name.startsWith("Я"))
            .findFirst();

        System.out.println("Порожній пошук: " + notFound.isEmpty()); // true
    }
}

🟡 Middle Level

Роль Encounter Order (Порядку обходу)

Поведінка операцій залежить від характеристик джерела стріму (Spliterator.ORDERED):

Джерело даних Encounter Order (Порядок) findFirst() findAny() у parallelStream()
ArrayList, масив, LinkedHashSet Присутній (суворий) Завжди елемент із мінімальним індексом Той елемент, чий потік встиг завершитися першим
HashSet, HashMap.keySet() Відсутній (довільний) Перший елемент у поточній ітерації бакетів Будь-який знайдений елемент

Порівняння з anyMatch() та limit(1)

Часто виникає питання, яку операцію обрати для розв’язання задачі:

// 1. Лише перевірка факту наявності (boolean):
// Оптимально anyMatch() — нуль алокацій, немає створення Optional
boolean exists = users.stream().anyMatch(User::isAdmin);

// 2. Вилучення одного знайденого об'єкта:
// Використовується findFirst() / findAny(), що повертає Optional<T>
Optional<User> adminOpt = users.stream().filter(User::isAdmin).findFirst();

// 3. Продовження конвеєра обробки для одного елемента:
// Використовується проміжний limit(1), що повертає Stream<T>
List<User> oneUserList = users.stream().filter(User::isAdmin).limit(1).toList();

🔴 Senior Level

Механіка в ForkJoinPool: FindTask та скасування задач

У вихідному коді OpenJDK клас FindOp керує паралельним пошуком за допомогою спеціалізованої задачі FindTask:

Масив із 1 000 000 елементів у 4 робочих потоки:
Worker 1 [0..250k]:     Шукає...
Worker 2 [250k..500k]:  Шукає...
Worker 3 [500k..750k]:  ЗНАЙШОВ! ──► findAny() ОДРАЗУ повертає результат і скасовує 1, 2, 4!
                                 ──► findFirst() БЛОКУЄТЬСЯ і чекає завершення Worker 1 та 2!
Worker 4 [750k..1000k]: Шукає...
  1. findAny():
    • Алгоритм максимально оптимізований під паралелізм.
    • Щойно Worker 3 знаходить елемент, він виконує атомарний виклик shortCircuit(result).
    • Метод cancelLaterNodes() миттєво скасовує всі інші гілки дерева задач у ForkJoinPool, і стрім негайно завершується.
  2. findFirst():
    • Зобов’язаний гарантувати збереження вихідного порядку.
    • Навіть якщо Worker 3 знайшов відповідний елемент, він не може повернути його, доки Worker 1 і Worker 2 не завершать сканування лівих піддіапазонів (раптом там є раніший елемент!).
    • Це призводить до простоїв потоків, міжпотокової координації та суттєвого уповільнення.

Підступний Edge Case: NullPointerException всередині Optional

Внутрішня реалізація FindOp упаковує результат в Optional за допомогою Optional.of(result). Контейнер Optional у Java за специфікацією не може містити null:

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public class FindNullTrapDemo {

    public static void main(String[] args) {
        List<String> list = Arrays.asList("A", null, "B");

        try {
            // ❌ КАТАСТРОФА: викине NullPointerException!
            Optional<String> result = list.stream()
                .filter(Objects::isNull)
                .findFirst();
        } catch (NullPointerException e) {
            System.err.println("Впало з NPE, оскільки Optional не може зберігати null!");
        }
    }
}

Примітивні спеціалізації: OptionalInt, OptionalLong, OptionalDouble

Для усунення накладних витрат на створення об’єктів Optional<Integer> та автобоксингу у Highload-системах використовуються спеціалізовані примітивні стріми:

// Повертає OptionalInt (містить примітивний int, нуль алокацій у Heap!)
OptionalInt firstEven = IntStream.rangeClosed(1, 10_000_000)
    .filter(n -> n % 2 == 0)
    .findFirst();

int value = firstEven.orElse(0);

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

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

«findFirst() та findAny() — термінальні операції короткого замикання, які повертають Optional<T>. findFirst() гарантує повернення першого елемента згідно з порядком обходу джерела (Encounter Order), тоді як findAny() повертає будь-який знайдений елемент. У паралельних стрімах findAny() працює на порядки швидше, оскільки не чекає завершення потоків, що обробляють раніші чанки даних. Важлива пастка: якщо знайдений елемент дорівнює null, обидві операції викинуть NullPointerException, оскільки Optional забороняє збереження null.»


Порівняння findFirst() та findAny()

Параметр findFirst() findAny()
Тип, що повертається Optional<T> Optional<T>
Гарантія порядку (Encounter Order) Суворо гарантується Не гарантується
Результат в однопотоковому стрімі Перший знайдений Зазвичай перший знайдений
Швидкість у parallelStream Повільна (чекає на ліві чанки) Максимальна (перший, хто фінішував)
Реакція на null Викидає NullPointerException Викидає NullPointerException

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

1. Чому в паралельних стрімах findAny() працює швидше, ніж findFirst()?

  • findAny() не прив’язаний до вихідного порядку елементів у колекції. Щойно будь-який робочий потік ForkJoinPool знаходить збіг, він відразу перериває решту задач і повертає результат. findFirst() зобов’язаний повернути елемент з найменшим індексом: якщо потік знайшов елемент у середині або кінці, він зобов’язаний чекати завершення всіх потоків, що обробляють початок колекції.

2. Що станеться, якщо стрім містить елементи null і ми викликаємо findFirst()?

  • Якщо знайдений елемент дорівнює null, метод викине NullPointerException. Всередині реалізації FindOp повернення оформлюється через фабричний метод Optional.of(result), який забороняє значення null. Для пошуку факту наявності null слід використовувати anyMatch(Objects::isNull).

3. Чи можуть findFirst() та findAny() повернути різні результати в однопотоковому стрімі?

  • У впорядкованому джерелі (List, масив) у послідовному стрімі обидва методи практично завжди повертають один і той самий перший елемент. Проте специфікація формально дозволяє findAny() повернути будь-який елемент навіть у послідовному стрімі, якщо джерело не є впорядкованим (HashSet).

4. Як примітивні стріми оптимізують операції пошуку?

  • Методи findFirst() та findAny() на IntStream, LongStream та DoubleStream повертають примітивні контейнери OptionalInt, OptionalLong та OptionalDouble. Вони зберігають значення у примітивному полі без пакування в посилальні класи-обгортки (Integer), повністю усуваючи навантаження на збирач сміття (GC).

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

  • 🚩 Використання findFirst() у багатопотокових паралельних стрімах, коли порядок не важливий (замість швидкого findAny()).
  • 🚩 Використання findFirst().isPresent() замість ефективного anyMatch().
  • 🚩 Спроба знайти null-елементи через filter(Objects::isNull).findFirst(), що призводить до NullPointerException.
  • 🚩 Виклик .get() на повернутому Optional без попередньої перевірки isPresent() або використання orElse().

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