← Назад к вопросам

Что делает 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 может быть быстрее