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 вважатиме об’єкти різними, навіть якщо вони «логічно» однакові.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ