JavaRush /Курсы /JAVA 25 SELF /Подмножества Stream API: distinct, limit, skip

Подмножества Stream API: distinct, limit, skip

JAVA 25 SELF
30 уровень , 2 лекция
Открыта

1. Метод distinct: удаляем дубликаты

В реальных задачах часто требуется не только фильтровать и преобразовывать данные, но и выбирать из коллекции только уникальные элементы, ограничивать размер результата или, наоборот, пропускать первые несколько элементов. Например:

  • Получить список уникальных имён пользователей.
  • Взять только первых 10 записей для отображения на странице.
  • Пропустить первые 5 элементов (например, при реализации постраничной навигации — «показать со 2-й страницы»).

Для таких задач в Stream API есть специальные методы: distinct, limit, skip.

Как работает distinct?

Метод distinct() возвращает новый поток, в котором удалены все дубликаты элементов. Дубликаты определяются с помощью методов equals и hashCode соответствующего класса.

Представьте, что вы — нумизмат, и вам важно, чтобы в основной коллекции не было дубликатов. Все повторяющиеся монетки пойдут в обменный фонд. Вот как раз distinct() способен отсеять дубликаты из основной коллекции.

Пример: уникальные имена пользователей

List<String> names = List.of(
    "Алиса", "Боб", "Алиса", "Ева", "Боб", "Денис", "Глеб", "Ева"
);

Получим список уникальных имён:

List<String> uniqueNames = names.stream()
    .distinct()
    .collect(Collectors.toList());

System.out.println(uniqueNames);
// Вывод: [Алиса, Боб, Ева, Денис, Глеб]

Пример: уникальные email пользователей

Допустим, у нас есть класс User:

public class User {
    String name;
    String email;

    // Конструктор, геттеры, toString() — для удобства
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    @Override
    public String toString() {
        return name + " <" + email + ">";
    }
}

Список пользователей с дубликатами email:

List<User> users = List.of(
    new User("Алиса", "alice@mail.com"),
    new User("Боб", "bob@mail.com"),
    new User("Ева", "eva@mail.com"),
    new User("Алиса2", "alice@mail.com"), // дубликат email!
    new User("Глеб", "gleb@mail.com"),
    new User("Ева2", "eva@mail.com")      // дубликат email!
);

Если вызвать просто users.stream().distinct(), то дубликаты не удалятся, потому что у объектов User по умолчанию методы equals и hashCode не переопределены. В этом случае distinct работает только для ссылочных совпадений.

Решение: Переопределить equals и hashCode так, чтобы уникальность определялась по email.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    User user = (User) o;
    return Objects.equals(email, user.email);
}

@Override
public int hashCode() {
    return Objects.hash(email);
}

Теперь:

List<User> uniqueUsers = users.stream()
    .distinct()
    .collect(Collectors.toList());

uniqueUsers.forEach(System.out::println);
// Алиса <alice@mail.com>
// Боб <bob@mail.com>
// Ева <eva@mail.com>
// Глеб <gleb@mail.com>

Важно: Для своих классов всегда переопределяйте equals и hashCode, если хотите, чтобы distinct работал «по‑человечески»!

2. Метод limit: ограничиваем количество элементов

Метод limit(long maxSize) возвращает новый поток, который содержит не более maxSize первых элементов исходного потока.

Аналогия: Вы пришли в кондитерскую и хотите попробовать только 3 пирожных из 100. limit(3) — и вам дают ровно три первых, остальные — «увы, не сегодня».

Пример: первые 3 имени

List<String> firstThree = names.stream()
    .limit(3)
    .collect(Collectors.toList());

System.out.println(firstThree);
// Вывод: [Алиса, Боб, Алиса]

Пример с нашим приложением: топ‑3 новых пользователей

List<User> firstUsers = users.stream()
    .limit(3)
    .collect(Collectors.toList());

firstUsers.forEach(System.out::println);
// Алиса <alice@mail.com>
// Боб <bob@mail.com>
// Ева <eva@mail.com>

Пример с сортировкой

Можно комбинировать с сортировкой — например, взять топ‑2 пользователя с самым коротким email:

List<User> top2ShortEmail = users.stream()
    .sorted(Comparator.comparingInt(u -> u.email.length()))
    .limit(2)
    .collect(Collectors.toList());

top2ShortEmail.forEach(System.out::println);
// Боб <bob@mail.com>
// Ева <eva@mail.com>

3. Метод skip: пропускаем первые элементы

Метод skip(long n) возвращает новый поток, где пропущены первые n элементов исходного потока.

Пример: пропустить первые 2 имени

List<String> afterTwo = names.stream()
    .skip(2)
    .collect(Collectors.toList());

System.out.println(afterTwo);
// Вывод: [Алиса, Ева, Боб, Денис, Глеб, Ева]

Пример: постраничная навигация (pagination)

Часто на сайте нужно показывать, например, «по 3 пользователя на страницу». Для второй страницы нужно пропустить первых 3, взять следующие 3:

int pageSize = 3;
int pageNumber = 2; // Вторая страница

List<User> page = users.stream()
    .skip(pageSize * (pageNumber - 1))
    .limit(pageSize)
    .collect(Collectors.toList());

page.forEach(System.out::println);
// Алиса2 <alice@mail.com>
// Глеб <gleb@mail.com>
// Ева2 <eva@mail.com>

4. Комбинирование distinct, limit, skip

Эти методы можно и нужно комбинировать — в зависимости от задачи.

Пример: получить 2 уникальных имени, начиная с третьего по порядку

List<String> result = names.stream()
    .distinct() // Убираем дубликаты: [Алиса, Боб, Ева, Денис, Глеб]
    .skip(2)    // Пропускаем Алису и Боба: [Ева, Денис, Глеб]
    .limit(2)   // Берём только двух: [Ева, Денис]
    .collect(Collectors.toList());

System.out.println(result);
// Вывод: [Ева, Денис]

Пример с фильтрацией

Допустим, нужно получить первые 2 уникальных email, которые содержат букву «a»:

List<String> emails = users.stream()
    .map(user -> user.email)
    .filter(email -> email.contains("a"))
    .distinct()
    .limit(2)
    .collect(Collectors.toList());

System.out.println(emails);
// Вывод: [alice@mail.com, eva@mail.com]

5. Практика: задачи на применение

Задача 1. Получить список уникальных имён пользователей, длина которых больше 3 символов

List<String> longUniqueNames = names.stream()
    .filter(name -> name.length() > 3)
    .distinct()
    .collect(Collectors.toList());

System.out.println(longUniqueNames);
// Например: [Алиса, Денис]

Задача 2. Получить 3‑й и 4‑й уникальный email из списка пользователей

List<String> thirdAndFourthEmail = users.stream()
    .map(user -> user.email)
    .distinct()
    .skip(2)
    .limit(2)
    .collect(Collectors.toList());

System.out.println(thirdAndFourthEmail);
// Например: [eva@mail.com, gleb@mail.com]

Задача 3. Получить первые 5 уникальных чисел, которые больше 10

List<Integer> numbers = List.of(5, 12, 17, 5, 23, 17, 42, 19, 12, 8);

List<Integer> result = numbers.stream()
    .filter(n -> n > 10)
    .distinct()
    .limit(5)
    .collect(Collectors.toList());

System.out.println(result);
// Вывод: [12, 17, 23, 42, 19]

6. Визуальная схема: порядок применения операций

graph TD
    A[Исходный список] --> B[filter]
    B --> C[distinct]
    C --> D[skip]
    D --> E[limit]
    E --> F[collect]

Комментарий:
Обычно сначала применяют filter, затем убирают дубликаты distinct, потом используют skip и limit, а в конце — collect. Но порядок можно менять, если задача требует.

7. Типичные ошибки при работе с distinct, limit, skip

Ошибка №1: Ожидание, что distinct удаляет дубликаты по «любому» признаку.
На самом деле, distinct работает по методу equals объекта. Если вы хотите уникальность по какому-то полю (например, только по email), а не по всему объекту — нужно либо переопределить equals/hashCode, либо использовать хитрости с Collectors.toMap() или дополнительной фильтрацией.

Ошибка №2: Нарушение порядка операций.
Если сначала применить limit, а потом distinct, то дубликаты могут остаться — ведь вы ограничили поток до первых N элементов, а среди них могут быть одинаковые.

Ошибка №3: Пропуск через skip больше, чем есть элементов.
Если вы попробуете пропустить больше элементов, чем есть в потоке, то результат будет просто пустым списком — ошибки не будет, но результат может удивить.

Ошибка №4: Неучтённая производительность.
Методы distinct, limit, skip могут быть неэффективны на очень больших потоках, особенно если поток неупорядоченный или если элементы сложные. В 99% бытовых задач это не проблема, но если вы работаете с миллионами записей — стоит задуматься.

Ошибка №5: Забытая переопределённая логика equals/hashCode для своих классов.
Если вы работаете с пользовательскими объектами (например, User), то без переопределения этих методов distinct будет считать объекты разными, даже если они «логически» одинаковы.

1
Задача
JAVA 25 SELF, 30 уровень, 2 лекция
Недоступна
Управление Очередью на Эксклюзивную Вечеринку 🎉
Управление Очередью на Эксклюзивную Вечеринку 🎉
1
Задача
JAVA 25 SELF, 30 уровень, 2 лекция
Недоступна
Формирование Топ-4 Уникальных Показателей Продаж 💰
Формирование Топ-4 Уникальных Показателей Продаж 💰
Комментарии (6)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ksanders Уровень 32
29 декабря 2025
Вторая задача повышенной сложности - прямо в комментариях решение прописано
Anton Pohodin Уровень 26
20 октября 2025
Ну и легкотня... Strem вообще не сложный в понимании и освоении. Лично для меня "внедрение зависимостей" (DI) и концепция PECS были намного труднее в практике применения.
I'll kick them all Уровень 5
3 октября 2025
Аналогия: Представьте себе вечеринку, на которую случайно пригласили одного и того же человека несколько раз. distinct() — это строгий охранник на входе, который говорит: "Ты уже заходил, следующий!" Как-то по AI-йному ахах.
Сергей Уровень 40
1 октября 2025
кайф
Сергей Уровень 66
2 октября 2025
Абсолютный кайф! Особенно после мучений на старом курсе ахаха
I'll kick them all Уровень 5
3 октября 2025
Streams на старом курсе отлично работают кроме парочки нововведений вида toList(), toSet() из Java 16. Большинство задач там я через них и решал, так что не надо тут =)