Предположим, у меня есть некоторая коллекция, как я могу вернуть только те значения, которые появляются ровно один раз? Я не хочу сокращать повторяющиеся значения до одной записи, я хочу полностью их исключить — они не подходят для моего варианта использования.
Например, список, содержащий 1, 2, 3, 1, 4, 3, 5, 1
, должен возвращать только 2, 4, 5
. Я мог бы просмотреть список, подсчитать элементы, а затем удалить все, что появляется более одного раза, но мне интересно, есть ли .stream()
решение, которое мне не хватает.
Чтобы вернуть только те элементы, которые появляются ровно один раз, вы можете использовать Stream для достижения этой цели:
List<Integer> list = Arrays.asList(1, 2, 3, 1, 4, 3, 5, 1);
//Find Number of occurence of each element in the List
Map<Integer, Long> counts = list.stream()
.collect(Collectors.groupingBy(e -> e, Collectors.counting()));
// Filter elements that appear only once
List<Integer> result = list.stream()
.filter(element -> counts.get(element) == 1)
.toList();
Вы можете напрямую передавать набор записей карты и фильтровать по значению. Таким образом вы можете избежать поиска каждого элемента.
возможно list.stream().collect(groupingBy(identity(), counting())).entrySet().stream().filter(e -> e.getValue()==1).map(Map.Entry::getKey).toList()
Для чего-то подобного я предпочитаю императивный подход с использованием наборов.
seen
.Поскольку наборы хешируются и не могут содержать дубликаты, добавление и удаление происходит более эффективно по сравнению со списками.
List<Integer> list =
new ArrayList<>(Arrays.asList(1, 2, 3, 1, 4, 3, 5, 1));
System.out.println("Original list = " + list);
Set<Integer> seen = new HashSet<>();
Set<Integer> result = new HashSet<>();
for(int v : list) {
result.add(v); // try and add the value
if (!seen.add(v)) { // if already seen, remove it.
result.remove(v);
}
}
System.out.println("Result = " + result);
принты
Original list = [1, 2, 3, 1, 4, 3, 5, 1]
Result = [2, 4, 5]
При необходимости набор результатов можно затем поместить в список.
Потоковая версия
Вот поток, эквивалентный приведенному выше.
seen
и result
. record Info(Set<Integer> seen, Set<Integer> result){}
List<Integer> resultList =
new ArrayList<>(list.stream().reduce(
new Info(new HashSet<>(), new HashSet<>()),
(info, val) -> {
info.result.add(val);
if (!info.seen.add(val)) {
info.result.remove(val);
}
return info;
},
(a, b) -> a).result);
System.out.println(resultList);
Если бы я делал это, я бы придерживался императивного решения, поскольку оно проще, а потоковое решение на самом деле не более декларативное, но более сложное.
Мне нравится пытаться ответить на однострочный поток. Я бы никогда не написал это так в коде, которым поделились с другими. Использование ссылок на методы отражает ценность промежуточных наборов:
List<Integer> unique = list.stream()
.filter(Predicate.not(list.stream().filter(Predicate.not(new HashSet<>()::add)).collect(Collectors.toSet())::contains))
.toList();
HashSet
просмотренных элементов используется для создания Set
дубликатов, что, в свою очередь, используется для фильтрации уникальных элементов в исходном списке.
Что происходит выше, станет понятнее, если провести рефакторинг, выделив промежуточные этапы как локальные переменные:
// Define a set to record all seen integers:
Set<Integer> seen = new HashSet<>();
// Create a set containing all duplicated integers
// this relies on seen.add(x) returning false for each duplicate item:
Set<Integer> dups = list.stream()
.filter(Predicate.not(seen::add))
.collect(Collectors.toSet());
// Finally scan the list and eliminate every entry which is a duplicate:
List<Integer> unique = list.stream()
.filter(Predicate.not(dups::contains))
.toList();
Для небольших списков:
list.stream().filter(i -> Collections.frequency(list, i) == 1).toList();