← Назад к вопросам
Что делает distinct в Stream API?
2.3 Middle🔥 171 комментариев
#Stream API и функциональное программирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Что делает distinct в Stream API
Метод distinct() — это промежуточная операция в Stream API, которая удаляет дубликаты из потока.
1. Базовый пример
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5, 5);
List<Integer> unique = numbers.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(unique); // [1, 2, 3, 4, 5]
2. Как работает distinct()
distinct() использует метод equals() и hashCode() для определения, являются ли элементы одинаковыми:
List<String> fruits = Arrays.asList("apple", "banana", "apple", "cherry", "banana");
List<String> unique = fruits.stream()
.distinct() // Использует String.equals()
.collect(Collectors.toList());
System.out.println(unique); // [apple, banana, cherry]
3. С объектами
Для пользовательских объектов нужно переопределить equals() и hashCode():
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
List<Person> people = Arrays.asList(
new Person("John", 25),
new Person("Jane", 30),
new Person("John", 25), // Дубликат
new Person("Bob", 25)
);
List<Person> unique = people.stream()
.distinct() // Будет сравнивать через equals() и hashCode()
.collect(Collectors.toList());
System.out.println(unique.size()); // 3 (John 25 появится только один раз)
4. Внутренняя реализация
distinct() использует HashSet для отслеживания уже увиденных элементов:
// Примерно так работает distinct() внутри
public static <T> Stream<T> distinct(Stream<T> stream) {
Set<T> seen = new HashSet<>();
return stream.filter(elem -> seen.add(elem));
// Set.add() возвращает true только если элемента не было в наборе
}
5. Производительность
distinct() имеет линейную сложность O(n), но требует дополнительную память O(n):
List<Integer> million = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
million.add(i % 1000); // Повторяются значения от 0 до 999
}
long start = System.currentTimeMillis();
List<Integer> unique = million.stream()
.distinct() // Создаст HashSet с 1000 элементами
.collect(Collectors.toList());
long duration = System.currentTimeMillis() - start;
System.out.println("Size: " + unique.size()); // 1000
System.out.println("Time: " + duration + "ms");
6. distinct() с другими операциями
// С map() — получить уникальные имена
List<String> names = people.stream()
.map(Person::getName)
.distinct()
.collect(Collectors.toList());
// С filter() — сначала фильтруем, потом уникализируем
List<Person> adults = people.stream()
.filter(p -> p.getAge() >= 18)
.distinct()
.collect(Collectors.toList());
// С sorted() — отсортировать уникальные элементы
List<Integer> sortedUnique = numbers.stream()
.distinct()
.sorted()
.collect(Collectors.toList());
7. distinct() с пользовательским компаратором
Если нужна уникализация по определённому полю (не через equals()):
public static <T> Stream<T> distinctBy(Stream<T> stream, Function<? super T, ?> keyExtractor) {
Set<Object> seen = new HashSet<>();
return stream.filter(elem -> seen.add(keyExtractor.apply(elem)));
}
// Использование
List<Person> uniqueByName = people.stream()
.filter(new HashSet<>()::add) // Неправильно!
.collect(Collectors.toList());
// Правильно
List<Person> uniqueByName = distinctBy(people.stream(), Person::getName)
.collect(Collectors.toList());
8. Альтернатива через LinkedHashSet
Если нужно сохранить порядок элементов:
List<Integer> numbers = Arrays.asList(5, 1, 3, 1, 2, 3, 5, 4);
// Способ 1: distinct() (сохраняет порядок по умолчанию)
List<Integer> unique1 = numbers.stream()
.distinct()
.collect(Collectors.toList());
// [5, 1, 3, 2, 4]
// Способ 2: через LinkedHashSet
List<Integer> unique2 = new ArrayList<>(new LinkedHashSet<>(numbers));
// [5, 1, 3, 2, 4]
9. Частые ошибки
Ошибка 1: Забыли переопределить equals() и hashCode()
public class BadPerson {
private String name;
// equals() и hashCode() не переопределены!
}
List<BadPerson> people = Arrays.asList(
new BadPerson("John"),
new BadPerson("John") // Разные объекты в памяти
);
List<BadPerson> unique = people.stream()
.distinct() // Не удалит дубликат, так как это разные объекты!
.collect(Collectors.toList());
System.out.println(unique.size()); // 2 (вместо 1)
Ошибка 2: Используют с параллельными потоками неправильно
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3);
// Параллельный поток с distinct()
List<Integer> unique = numbers.parallelStream()
.distinct()
.collect(Collectors.toList());
// Работает, но медленнее из-за синхронизации в HashSet
10. distinct() в SQL vs Stream API
// SQL
SELECT DISTINCT name FROM users;
// Эквивалент в Stream
userRepository.findAll().stream()
.map(User::getName)
.distinct()
.collect(Collectors.toList());
// ЛУЧШЕ: использовать SQL запрос!
List<String> names = userRepository.findDistinctNames();
// @Query("SELECT DISTINCT u.name FROM User u")
// List<String> findDistinctNames();
11. Практический пример: Уникальные статьи по тегам
@Service
public class ArticleService {
public List<Article> getUniqueArticlesByTag(String tag) {
return articleRepository.findByTag(tag).stream()
.distinct() // Удалить дубликаты
.collect(Collectors.toList());
}
public List<String> getUniqueCategoriesFromArticles(List<Article> articles) {
return articles.stream()
.map(Article::getCategory)
.distinct()
.sorted()
.collect(Collectors.toList());
}
public Map<String, Long> getUniqueAuthorsCount(List<Article> articles) {
return articles.stream()
.map(Article::getAuthor)
.distinct()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
));
}
}
12. Производительность: Stream vs Set
List<Integer> million = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
million.add(i % 1000);
}
// Способ 1: Stream + distinct()
long start1 = System.nanoTime();
List<Integer> result1 = million.stream()
.distinct()
.collect(Collectors.toList());
long time1 = System.nanoTime() - start1;
// Способ 2: LinkedHashSet
long start2 = System.nanoTime();
List<Integer> result2 = new ArrayList<>(new LinkedHashSet<>(million));
long time2 = System.nanoTime() - start2;
System.out.println("Stream: " + time1 + "ns");
System.out.println("HashSet: " + time2 + "ns");
// LinkedHashSet часто быстрее для небольших наборов!
Вывод
distinct() в Stream API:
- Удаляет дубликаты, используя
equals()иhashCode() - Требует O(n) памяти и O(n) времени
- Сохраняет исходный порядок элементов
- Должен работать с правильно переопределёнными
equals()иhashCode() - Для БД запросов используйте SQL DISTINCT, не Stream API
- Для небольших наборов
LinkedHashSetможет быть быстрее