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

Що роблять операції distinct(), sorted(), limit(), skip()?

Усі чотири операції — проміжні (intermediate), але вони відрізняються від filter та map:

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

🟢 Junior Level

Усі чотири операції — проміжні (intermediate), але вони відрізняються від filter та map:

distinct() — видаляє дублікати:

List.of(1, 2, 2, 3, 3, 3).stream().distinct().collect(toList());
// [1, 2, 3]

sorted() — сортує елементи:

List.of(3, 1, 2).stream().sorted().collect(toList());
// [1, 2, 3]

// sorted() у parallelStream: спочатку всі елементи збираються, потім сортуються, // потім розподіляються по воркерах. overhead на merge може перевищити вигоду.

limit(n) — бере перші n елементів:

List.of(1, 2, 3, 4, 5).stream().limit(3).collect(toList());
// [1, 2, 3]

skip(n) — пропускає перші n елементів:

List.of(1, 2, 3, 4, 5).stream().skip(2).collect(toList());
// [3, 4, 5]

🟡 Middle Level

Stateful операції (зі станом)

Stateful операції — потребують знання ВСІХ елементів пайплайну, а не поточного. distinct() повинен побачити всі елементи, щоб знайти унікальні. sorted() повинен відсортувати весь набір.

distinct() та sorted()stateful. Вони вимагають буферизації:

  • distinct() — створює внутрішній HashSet для відстеження унікальних елементів
  • sorted() — це бар’єр у конвеєрі. Стрим збирає ВСІ елементи, сортує, потім передає далі

Memory: Обидві споживають пам’ять O(N) — можуть призвести до росту купи та GC-пауз на великих даних.

Short-circuit операції

limit() та skip() — операції короткого замикання:

  • limit(n) — сигналізує про припинення роботи через прапорець скасування
  • skip(n) — має внутрішній лічильник, “поглинає” елементи

Оптимізація порядку

// ПОГАНО — сортуємо мільйон, потім беремо 10
stream.sorted().limit(10)...

// ДОБРЕ — спочатку фільтруємо, зменшуючи N
stream.filter(relevant).sorted().limit(10)...

🔴 Senior Level

Коли НЕ використовувати

  • distinct() — якщо дані вже унікальні (HashSet на вході) — зайва перевірка
  • sorted() — якщо порядок не важливий (використовуйте findAny замість findFirst)
  • limit(n) після sorted() — сортування всього набору заради перших n (використовуйте PriorityQueue)
  • skip(n) для пагінації на великих даних — O(n) пропуск, краще keyset pagination

Проблеми паралелізму

У parallelStream ці операції стають вузьким місцем:

  • limit() та skip(): Вимагають жорсткої синхронізації між потоками
  • distinct(): Об’єднання хеш-сетів з різних потоків дороге
  • sorted(): Паралельне сортування ефективне лише на дуже великих масивах

Edge Cases

  • distinct() на мутабельних об’єктах: Якщо змінили поле об’єкта після проходження distinct — порушення контракту унікальності
  • Стабільність sorted(): Стрими гарантують стабільне сортування (збереження порядку рівних елементів), якщо джерело впорядковане

Діагностика

  • jmap -histo: Покаже роздуття HashSet або Object[] при distinct/sorted на великих даних
  • Infinite stream check: Якщо нескінченний стрим (Stream.iterate) завис — перевірте, чи стоїть limit() до або після важких фільтрів

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

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

  • distinct() та sorted() — stateful операції, вимагають буферизації всіх елементів у пам’яті (O(N))
  • distinct() використовує внутрішній HashSet для відстеження унікальності
  • sorted() — бар’єр у конвеєрі: збирає ВСІ елементи, сортує, потім передає далі
  • limit() та skip() — short-circuit операції, не вимагають буферизації
  • Порядок важливий: filter().sorted().limit() ефективніше за sorted().limit()
  • limit() після sorted() — сортування всього набору заради перших n (використовуйте PriorityQueue)
  • skip() для пагінації на великих даних — O(n), краще keyset pagination
  • У parallelStream stateful операції стають вузьким місцем через синхронізацію

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

  • Чим stateful операції відрізняються від stateless? — Stateless (filter, map) обробляють кожен елемент незалежно; stateful (distinct, sorted) повинні бачити всі елементи цілком.
  • Чому sorted() — бар’єр? — Стрим не може відсортувати елементи, поки не збере їх усі — це блокує конвеєр до повного збору даних.
  • Що гірше для продуктивності — distinct чи sorted? — sorted() важче, оскільки вимагає повної буферизації та самого сортування; distinct() використовує HashSet з O(1) перевіркою.
  • Чому limit() після sorted() — антипатерн? — Ви сортуєте весь набір даних, а потім берете перші n — краще відфільтрувати та обмежити до сортування.

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

  • «sorted() обробляє елементи по одному» — неправда, це бар’єр, потрібні всі елементи
  • «distinct() не споживає додаткову пам’ять» — неправда, створює внутрішній HashSet
  • «limit() після sorted() — оптимально» — неправда, сортування всього набору заради n елементів надлишкове
  • «parallelStream завжди прискорює sorted()» — неправда, overhead на merge може перевищити вигоду

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

  • [[21. Що таке lazy evaluation в Stream]]
  • [[24. Як працює коротке замикання (short-circuiting) в Stream]]
  • [[26. Що роблять операції findFirst() та findAny()]]
  • [[27. Як зібрати Stream в Map]]