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);
// Виведення: [Аліса, Боб, Єва, Денис, Гліб]

Приклад: унікальні електронні адреси користувачів

Припустімо, у нас є клас 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 + ">";
    }
}

Список користувачів із дублікатами електронних адрес:

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"), // дублікат електронної адреси!
    new User("Гліб", "gleb@mail.com"),
    new User("Єва2", "eva@mail.com")      // дублікат електронної адреси!
);

Якщо викликати просто 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 користувачів із найкоротшою електронною адресою:

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 унікальні електронні адреси, які містять літеру «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 вважатиме об’єкти різними, навіть якщо вони «логічно» однакові.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ