Що роблять операції distinct(), sorted(), limit(), skip()
Усі чотири методи є проміжними (intermediate) операціями зі станом (Stateful), які змінюють склад, порядок або кількість елементів у потоці:
🟢 Junior Level
Коротка відповідь для співбесіди (30 секунд)
Усі чотири методи є проміжними (intermediate) операціями зі станом (Stateful), які змінюють склад, порядок або кількість елементів у потоці:
distinct()— відфільтровує дублікати, залишаючи лише унікальні елементи (порівняння йде за контрактомequals()таhashCode()).sorted()— сортує елементи в природному порядку (Comparable) або за допомогою переданогоComparator.limit(long maxSize)— усікає потік, пропускаючи далі не більше першихmaxSizeелементів (операція короткого замикання).skip(long n)— відкидає першіnелементів потоку, пропускаючи вниз по конвеєру ті, що залишилися.
Приклади використання на Java 21
import java.util.Comparator;
import java.util.List;
public class StatefulOpsDemo {
public record Product(String name, double price) {}
public static void main(String[] args) {
List<Integer> numbers = List.of(5, 2, 8, 2, 9, 1, 5, 8, 3);
// 1. Комбінування distinct, sorted, skip та limit
List<Integer> result = numbers.stream()
.distinct() // [5, 2, 8, 9, 1, 3] (видалено дублікати)
.sorted() // [1, 2, 3, 5, 8, 9] (відсортовано за зростанням)
.skip(1) // [2, 3, 5, 8, 9] (пропустили мінімальний елемент)
.limit(3) // [2, 3, 5] (взяли наступні 3 елементи)
.toList();
System.out.println("Результат обробки чисел: " + result);
// 2. Сортування об'єктів з Comparator
List<Product> products = List.of(
new Product("Клавіатура", 3500.0),
new Product("Миша", 1800.0),
new Product("Монітор", 24000.0)
);
List<Product> cheapToExpensive = products.stream()
.sorted(Comparator.comparingDouble(Product::price))
.toList();
System.out.println("Сортування за ціною: " + cheapToExpensive);
}
}
🟡 Middle Level
Поділ на Stateless та Stateful операції
В архітектурі Stream API проміжні операції фундаментально діляться на два типи:
Stateless (map, filter):
Елемент 1 ──► [Filter] ──► [Map] ──► Downstream (потокова передача без буферів)
Stateful (sorted, distinct):
Елемент 1 ──┐
Елемент 2 ──┼──► [ Внутрішній буфер пам'яті ] ──► (Бар'єр: очікування кінця стріма)
Елемент 3 ──┘
- Stateless (без стану):
filter,map,flatMap,peek. Для кожного елемента рішення приймається на льоту. Пам’ять не витрачається ($O(1)$). - Stateful (зі станом):
distinct(): зберігає стан уже побачених елементів у внутрішній хеш-множині (LinkedHashSet/ConcurrentHashMap). Вимагає $O(N)$ додаткової пам’яті.sorted(): виступає повним бар’єром конвеєра (Pipeline Barrier). Метод не може віддати жодного елемента downstream, поки не вичитає абсолютно всі елементи джерела в масив і не відсортує його (через TimSort / Dual-Pivot Quicksort).limit(n)/skip(n): зберігають лічильник оброблених елементів і підтримують коротке замикання.
Оптимізації Stream Flags (StreamOpFlag)
Якщо джерело даних уже володіє необхідними прапорцями, рантайм Stream API пропускає операції без виконання (no-op):
import java.util.List;
import java.util.TreeSet;
public class StreamFlagsOptimizationDemo {
public static void main(String[] args) {
TreeSet<String> sortedSet = new TreeSet<>(List.of("C", "A", "B"));
// TreeSet з самого початку має прапорці DISTINCT та SORTED у Spliterator:
List<String> list = sortedSet.stream()
.distinct() // NO-OP: JVM пропускає операцію (колекція вже Set)
.sorted() // NO-OP: JVM пропускає операцію (колекція вже відсортована)
.toList();
System.out.println(list); // [A, B, C]
}
}
🔴 Senior Level
3 критичні пастки продуктивності у Production
1. Антипатерн «Top-N через sorted().limit(K)»
Класична помилка — сортування мільйона об’єктів заради топ-5:
// ❌ ПОГАНО: Сортує ВСІ 1 000 000 елементів зі складністю $O(N \log N)$
List<User> top5 = users.stream()
.sorted(Comparator.comparing(User::getRating).reversed())
.limit(5)
.toList();
Senior-рішення: Для малих $K$ ($K \ll N$) використовувати PriorityQueue (Min-Heap) фіксованого розміру $K$. Складність знижується з $O(N \log N)$ до $O(N \log K)$, а споживання пам’яті — з $O(N)$ до $O(K)$.
2. Зависання на нескінченних стрімах (Infinite Stream Hang)
// 💥 Катастрофа: OutOfMemoryError / зависання потоку!
Stream.iterate(0, i -> i + 1)
.sorted() // Бар'єр зависне назавжди, намагаючись буферизувати нескінченність!
.limit(10)
.toList();
// ✅ ПРАВИЛЬНО: limit() ЗОБОВ'ЯЗАНИЙ стояти ДО бар'єра sorted():
Stream.iterate(0, i -> i + 1)
.limit(10) // Робить стрім скінченним!
.sorted()
.toList();
3. Пагінація через skip(N).limit(M)
Використання stream.skip(100_000).limit(20) для пагінації в коді змушує стрім поштучно протягнути 100 000 елементів крізь усі фільтри. Пагінація повинна виконуватися на рівні СУБД (LIMIT/OFFSET або Keyset Pagination).
Поведінка в parallelStream()
У паралельному режимі stateful-операції втрачають ефективність:
distinct()у parallel: вимагає синхронізації черезConcurrentHashMapабо дорогого злиття часткових сетів уcombiner.sorted()у parallel: використовуєArrays.parallelSort(), який дає виграш лише на обсягах від $100\,000$ елементів.limit()таskip()у parallel: якщо джерело впорядковане (ORDERED), потоки змушені буферизувати результати і координуватися для дотримання порядку. Виклик.unordered()передlimit()знімає це блокування, дозволяючи повернути будь-які перші $N$ готових елементів.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
«
distinct(),sorted(),limit()таskip()— це проміжні stateful-операції. На відміну від stateless-операцій (map,filter), вони вимагають збереження внутрішнього стану.distinct()використовуєequals/hashCodeі вимагає $O(N)$ пам’яті під хеш-таблицю.sorted()— повний бар’єр конвеєра, що накопичує всі елементи перед сортуванням; викликsorted()на нескінченному стрімі без попередньогоlimit()призводить доOutOfMemoryError. УparallelStreamдля прискоренняlimit()на впорядкованих колекціях рекомендується викликати.unordered().»
Матриця характеристик операцій
| Операція | Складність за часом | Пам’ять | Чи є бар’єром? | Вплив на нескінченний стрім |
|---|---|---|---|---|
distinct() |
$O(N)$ | $O(N)$ (хеш-таблиця) | Ні (віддає в міру знаходження) | Може зависнути при пошуку унікальних |
sorted() |
$O(N \log N)$ | $O(N)$ (масив елементів) | Так (повний бар’єр) | Зависає / OOM (не можна викликати) |
limit(n) |
$O(1)$ per item | $O(1)$ | Ні (коротке замикання) | Робить нескінченний стрім скінченним |
skip(n) |
$O(1)$ per item | $O(1)$ | Ні | Пропускає перші $n$ елементів |
Топ-4 каверзних запитання на інтерв’ю
1. Чому sorted() називають бар’єром конвеєра (pipeline barrier)?
- Тому що алгоритм сортування не може визначити, який елемент має бути першим, доки не порівняє його з усіма іншими елементами стріма. Тому
sorted()повністю зупиняє вертикальне проходження елементів (Loop Fusion), накопичує всі елементи джерела у внутрішній масив, сортує його і лише потім починає передавати елементи наступному вузлу.
2. Що станеться при виклику Stream.generate(Math::random).sorted().limit(5)?
- Програма зависне на 100% завантаженні CPU і завершиться з
OutOfMemoryError. Операціяsorted()спробує вичитати нескінченний стрім до кінця перед тим, як віддати бодай один елемент уlimit(5). Якщо ж змінити порядок на.limit(5).sorted(), код відпрацює миттєво, оскількиlimitзробить стрім скінченним до потрапляння в бар’єр сортування.
3. Як distinct() визначає рівність елементів і які вимоги висуваються до об’єктів?
distinct()використовує стандартні методиequals()таhashCode(), зберігаючи унікальні елементи у внутрішньому наборі. Якщо у класу не перевизначеніequalsтаhashCode, порівняння відбуватиметься за посиланням (==), і логічно однакові об’єкти не будуть відфільтровані як дублікати.
4. Навіщо викликати .unordered() перед .limit() у паралельному стрімі?
- Якщо стрім упорядкований (
ORDERED),limit(n)зобов’язаний повернути рівно ті елементи, які йшли першими у вихідній колекції. Це змушує паралельні гілкиForkJoinPoolсинхронізуватися і скидати результати правих гілок. Виклик.unordered()дозволяє стріму повернути будь-які $n$ елементів, які першими встигли обробити паралельні потоки, що в рази прискорює виконання.
Типові помилки та Red Flags
- 🚩 Виклик
sorted()на нескінченному стрімі до викликуlimit(). - 🚩 Використання
sorted().limit(K)для знаходження топ-3 елементів на мільйонних колекціях замістьPriorityQueue. - 🚩 Використання
distinct()для класів із некоректним або відсутнімhashCode()таequals(). - 🚩 Спроба використовувати
stream.skip(offset).limit(pageSize)для пагінації великих таблиць БД на рівні бекенда замість SQLLIMIT/OFFSET.