Stream.distinct: примеры (JAVA)

Stream.distinct: принципы работы и примеры
Раздел: Потоки данных (Stream API) - промежуточные операции
Stream.distinct: Stream

Общее описание Stream.distinct

Метод Stream.distinct() является промежуточной операцией Java Stream API и возвращает новый поток, содержащий уникальные элементы исходного потока по эквивалентности объектов (сравнение через equals и согласованный hashCode). Метод не принимает аргументов и возвращает Stream<T> для ссылочных потоков. Для примитивных специализированных потоков доступны аналогичные методы: IntStream.distinct(), LongStream.distinct(), DoubleStream.distinct().

Ключевые свойства

  • Тип: промежуточная, состояние сохраняющая операция (stateful).
  • Аргументы: отсутствуют.
  • Возвращаемое значение: объект Stream<T> с оставшимися после фильтрации уникальными элементами.
  • Критерий уникальности: метод equals и hashCode элементов.
  • Порядок: для упорядоченных стримов порядок встречаемости сохраняется; для неупорядоченных потоков порядок не гарантируется.
  • Параллелизм: поддерживается, но выполнение требует накопления уже увиденных элементов и может привести к дополнительным накладным расходам и использованию памяти.

Поведение требует корректной реализации equals и hashCode для пользовательских типов. При работе с очень большими или потенциально бесконечными потоками появление большого множества уникальных элементов может вызвать существенное потребление памяти, так как реализация хранит уже встреченные значения.

Короткие примеры применения

Несложные случаи удаления повторов и демонстрация результатов.

Пример 1: строки

List<String> list = Arrays.asList("a", "b", "a", "c", "b");
List<String> uniq = list.stream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(uniq);
[a, b, c]

Пример 2: IntStream

int[] arr = {1,2,1,3,2};
int[] uniq = IntStream.of(arr)
    .distinct()
    .toArray();
System.out.println(Arrays.toString(uniq));
[1, 2, 3]

Пример 3: объекты без equals/hashCode

class Point { int x,y; Point(int x,int y){this.x=x;this.y=y;} }
List<Point> pts = Arrays.asList(new Point(0,0), new Point(0,0));
List<Point> u = pts.stream().distinct().collect(Collectors.toList());
System.out.println(u.size());
2

Пример 4: объекты с equals/hashCode

class Point { int x,y; Point(int x,int y){this.x=x;this.y=y;} 
 public boolean equals(Object o){ if(!(o instanceof Point)) return false; Point p=(Point)o; return x==p.x && y==p.y; }
 public int hashCode(){ return Objects.hash(x,y); }
}
List<Point> pts2 = Arrays.asList(new Point(0,0), new Point(0,0));
List<Point> u2 = pts2.stream().distinct().collect(Collectors.toList());
System.out.println(u2.size());
1

Пример 5: параллельный поток

List<String> data = Arrays.asList("x","y","x","z","y");
List<String> res = data.parallelStream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(res);
[x, y, z]  // порядок может совпадать, но не гарантируется для неупорядоченных источников

Похожие приёмы в Java и их особенности

  • Collectors.toSet() - собирает уникальные элементы в набор; не гарантирует порядок (в случае HashSet). Для сохранения порядка можно использовать Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), ArrayList::new).
  • filter с внешним множеством - распространённый приём: track.add(x) в filter для сохранения только первых вхождений; требует аккуратности при параллельной обработке и использования потокобезопасных коллекций.
  • distinctByKey (пользовательская реализация) - реализация через ConcurrentHashMap для уникальности по ключу, когда сравнивать надо не весь объект, а часть полей.
  • TreeSet - уникальность по сравнению через Comparator; полезен, когда требуется сортировка и уникальность по заданному сравнению.

Выбор зависит от требований к порядку, критериям сравнения и параллелизму.

Аналоги в других языках и отличия

  • JavaScript
  • const arr = ['a','b','a'];
    const uniq1 = Array.from(new Set(arr));
    const uniq2 = arr.filter((v,i)=>arr.indexOf(v)===i);
    console.log(uniq1);
    console.log(uniq2);
    [ 'a', 'b' ]
    [ 'a', 'b' ]

    Set убирает повторы, но в старых средах indexOf-фильтр сохраняет порядок первых вхождений. Отличие от Java: критерий основан на ===; для объектов требуется ключ.

  • Python
  • lst = ['a','b','a']
    uniq = list(dict.fromkeys(lst))
    print(uniq)
    ['a', 'b']

    dict.fromkeys сохраняет порядок. set(lst) теряет порядок. Для объектов требуется сравниваемый ключ или хешируемость.

  • PHP
  • $a = ['a','b','a'];
    print_r(array_values(array_unique($a)));
    Array ( [0] => a [1] => b )
  • SQL
  • SELECT DISTINCT column FROM table;
    Возвращает строки без дублей; порядок определяется выражением ORDER BY если задан.
  • C# (LINQ)
  • var uniq = list.Distinct().ToList();
    Console.WriteLine(string.Join(",", uniq));
    a,b

    C# использует IEqualityComparer для настройки сравнения, похоже на Java.

  • Go
  • arr := []string{"a","b","a"}
    seen := map[string]bool{}
    uniq := []string{}
    for _, v := range arr { if !seen[v] { seen[v]=true; uniq = append(uniq, v) } }
    fmt.Println(uniq)
    [a b]
  • Kotlin
  • val l = listOf("a","b","a")
    println(l.distinct())
    [a, b]

    Kotlin имеет distinct и distinctBy для ключа, отличие от Java - встроенные расширения коллекций.

В большинстве языков решение сводится к использованию множества (set) или фильтрации с проверкой первого вхождения. Отличия в деталях сравнения (ссылка, значения, хеширование) и сохранении порядка.

Типичные ошибки при применении

  • Ожидание удаления дублей для объектов без корректного equals/hashCode. Пример ниже показывает, что без переопределения методы считаются разными.
class User { String name; User(String n){name=n;} }
List<User> list = Arrays.asList(new User("A"), new User("A"));
System.out.println(list.stream().distinct().count());
2  // ожидаем 1, но equals не переопределён
  • Использование с бесконечными источниками: distinct сохраняет все увиденные элементы, что может привести к утечке памяти или бесконечному выполнению.
  • Ожидание детерминированного порядка в неупорядоченных потоках или при параллельной обработке. Для гарантии порядка требуется упорядоченный источник или дополнительные меры.
  • Попытки применить distinct с пользовательской логикой сравнения - метод не принимает компаратор. Для сравнения по ключу требуется маппинг и дополнительная фильтрация или distinctByKey на основе внешнего множества.

Пример ошибочной попытки уникальности по полю без вспомогательных средств:

records.stream().distinct( /* нет перегрузки */ );
Ошибка компиляции - метод distinct не принимает аргументы

Изменения и история метода

Метод присутствует с введения Stream API в Java 8 и сохраняет стабильный интерфейс: отсутствуют новые параметры или перегрузки для distinct в последующих релизах. Добавлены аналогичные реализации для специализированных потоков (IntStream, LongStream, DoubleStream) одновременно с появлением Stream API. Внутренняя реализация могла получать оптимизации в более новых релизах JVM, но поведение спецификации осталось прежним.

Расширенные и нетривиальные примеры

Уникальность по ключу через filter и ConcurrentHashMap (поддержка параллельных потоков)

Пример java
Map<String,Boolean> seen = new ConcurrentHashMap<>();
List<Person> uniqueByEmail = people.parallelStream()
    .filter(p -> seen.putIfAbsent(p.getEmail(), Boolean.TRUE) == null)
    .collect(Collectors.toList());
System.out.println(uniqueByEmail);
[Person{name='A',email='a@x'}, Person{name='B',email='b@x'}]  // только первые уникальные по email

distinct после map: уникальность по результату функции

Пример java
List<String> emails = people.stream()
    .map(Person::getEmail)
    .distinct()
    .collect(Collectors.toList());
System.out.println(emails);
[a@x, b@x]

Сохранение порядка при сборе в набор

Пример java
List<String> preserved = list.stream()
    .distinct()
    .collect(Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), ArrayList::new));
System.out.println(preserved);
[a, b, c]

Уникальность с компаратором через TreeSet

Пример java
List<Person> uniqueSorted = people.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person::getEmail))),
        ArrayList::new));
System.out.println(uniqueSorted);
[Person{email='a@x'}, Person{email='b@x'}]  // уникальность и сортировка по email

Работа с большим объёмом данных - память и производительность

При больших наборах distinct ведёт к росту структуры хранения всех увиденных уникальных значений. Для уменьшения памяти возможны подходы: предварительная агрегация на стороне источника, использование внешнего хранилища или оконная обработка с периодической очисткой. В некоторых сценариях предпочтительнее подсчёт уникальных по ключу через братья-алгоритмы (approximate distinct), например HyperLogLog, если допускается приближённый результат.

Пример approximate distinct (концептуально)

Пример java
// Концепт: использование сторонней библиотеки, псевдокод
HyperLogLog hll = new HyperLogLog();
stream.forEach(hll::offer);
System.out.println(hll.cardinality());
~123456  // приближённое количество уникальных элементов

Distinct и lazy evaluation

distinct не выполняется до терминальной операции. В цепочках с limit порядок применения distinct и limit влияет на результат: first distinct then limit - будут первые уникальные; first limit then distinct - ограничение применяется прежде, что даёт другие элементы.

Пример java
List<Integer> list = Arrays.asList(1,1,2,2,3,3);
System.out.println(list.stream().distinct().limit(2).collect(Collectors.toList()));
System.out.println(list.stream().limit(2).distinct().collect(Collectors.toList()));
[1, 2]
[1]

джава Stream.distinct function comments

En
Stream.distinct Returns a stream consisting of the distinct elements