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 будет считать объекты разными, даже если они «логически» одинаковы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ