Stream.distinct: примеры (JAVA)
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: критерий основан на ===; для объектов требуется ключ.
lst = ['a','b','a']
uniq = list(dict.fromkeys(lst))
print(uniq)
['a', 'b']
dict.fromkeys сохраняет порядок. set(lst) теряет порядок. Для объектов требуется сравниваемый ключ или хешируемость.
$a = ['a','b','a'];
print_r(array_values(array_unique($a)));
Array ( [0] => a [1] => b )
SELECT DISTINCT column FROM table;
Возвращает строки без дублей; порядок определяется выражением ORDER BY если задан.
var uniq = list.Distinct().ToList();
Console.WriteLine(string.Join(",", uniq));
a,b
C# использует IEqualityComparer для настройки сравнения, похоже на Java.
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]
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 (поддержка параллельных потоков)
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: уникальность по результату функции
List<String> emails = people.stream()
.map(Person::getEmail)
.distinct()
.collect(Collectors.toList());
System.out.println(emails);
[a@x, b@x]
Сохранение порядка при сборе в набор
List<String> preserved = list.stream()
.distinct()
.collect(Collectors.collectingAndThen(Collectors.toCollection(LinkedHashSet::new), ArrayList::new));
System.out.println(preserved);
[a, b, c]
Уникальность с компаратором через TreeSet
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 (концептуально)
// Концепт: использование сторонней библиотеки, псевдокод
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 - ограничение применяется прежде, что даёт другие элементы.
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]