Вопрос 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()» — неверно, оверхед на merge может превысить выгоду

Связанные темы:

  • [[21. Что такое lazy evaluation в Stream]]
  • [[24. Как работает короткое замыкание (short-circuiting) в Stream]]
  • [[26. Что делают операции findFirst() и findAny()]]
  • [[27. Как собрать Stream в Map]]