Java 8 Различают по свойству
В Java 8 как я могу фильтровать коллекцию с помощью API Stream
, проверяя отличимость свойства каждого объекта?
Например, у меня есть список объектов Person
, и я хочу удалить людей с тем же именем,
persons.stream().distinct();
Будет использовать проверку равенства по умолчанию для объекта Person
, поэтому мне нужно что-то вроде
persons.stream().distinct(p -> p.getName());
К сожалению, метод distinct()
не имеет такой перегрузки. Без изменения проверки равенства внутри класса Person
можно ли это сделать лаконично?
Ответы
Ответ 1
Рассмотрим distinct
как фильтр с сохранением состояния. Вот функция, которая возвращает предикат, который поддерживает состояние того, что он видел ранее, и возвращает, видел ли данный элемент в первый раз:
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
Затем вы можете написать:
persons.stream().filter(distinctByKey(Person::getName))
Обратите внимание, что если поток упорядочен и выполняется параллельно, это сохранит произвольный элемент из числа дубликатов вместо первого, как это делает distinct()
.
(Это по сути то же самое, что мой ответ на этот вопрос: Java Lambda Stream Distinct() на произвольном ключе?)
Ответ 2
Альтернативой было бы размещение лиц на карте с использованием имени в качестве ключа:
persons.collect(toMap(Person::getName, p -> p, (p, q) -> p)).values();
Обратите внимание, что Лицо, которое хранится, в случае дублированного имени, будет первым измененным.
Ответ 3
Вы можете обернуть объекты человека в другой класс, который сравнивает только имена людей. Затем вы разворачиваете обернутые объекты, чтобы снова получить поток людей. Операции потока могут выглядеть следующим образом:
persons.stream()
.map(Wrapper::new)
.distinct()
.map(Wrapper::unwrap)
...;
Класс Wrapper
может выглядеть следующим образом:
class Wrapper {
private final Person person;
public Wrapper(Person person) {
this.person = person;
}
public Person unwrap() {
return person;
}
public boolean equals(Object other) {
if (other instanceof Wrapper) {
return ((Wrapper) other).person.getName().equals(person.getName());
} else {
return false;
}
}
public int hashCode() {
return person.getName().hashCode();
}
}
Ответ 4
Другое решение, используя Set
. Не может быть идеальным решением, но оно работает
Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
Или, если вы можете изменить исходный список, вы можете использовать метод removeIf
persons.removeIf(p -> !set.add(p.getName()));
Ответ 5
Существует более простой подход, использующий TreeSet с пользовательским компаратором.
persons.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName()))
));
Ответ 6
Мы также можем использовать RxJava (очень мощную библиотеку реактивного расширения)
Observable.from(persons).distinct(Person::getName)
или
Observable.from(persons).distinct(p -> p.getName())
Ответ 7
Вы можете использовать метод distinct(HashingStrategy)
в Eclipse Collections.
List<Person> persons = ...;
MutableList<Person> distinct =
ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));
Если вы можете реорганизовать persons
для реализации интерфейса Eclipse Collections, вы можете вызвать метод непосредственно в списке.
MutableList<Person> persons = ...;
MutableList<Person> distinct =
persons.distinct(HashingStrategies.fromFunction(Person::getName));
HashingStrategy - это просто интерфейс стратегии, который позволяет вам определить пользовательские реализации equals и hashcode.
public interface HashingStrategy<E>
{
int computeHashCode(E object);
boolean equals(E object1, E object2);
}
Примечание. Я являюсь коммиттером для коллекций Eclipse.
Ответ 8
Вы можете использовать groupingBy
коллектору:
persons.collect(Collectors.groupingBy(p -> p.getName())).values().forEach(t -> System.out.println(t.get(0).getId()));
Если вы хотите иметь другой поток, вы можете использовать это:
persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));
Ответ 9
Я рекомендую использовать Vavr, если можете. С помощью этой библиотеки вы можете делать следующее:
io.vavr.collection.List.ofAll(persons)
.distinctBy(Person::getName)
.toJavaSet() // or any another Java 8 Collection
Ответ 10
Вы можете использовать библиотеку StreamEx:
StreamEx.of(persons)
.distinct(Person::getName)
.toList()
Ответ 11
Расширение ответа Stuart Marks, это можно сделать короче и без параллельной карты (если вам не нужны параллельные потоки):
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
final Set<Object> seen = new HashSet<>();
return t -> seen.add(keyExtractor.apply(t));
}
Затем вызовите:
persons.stream().filter(distinctByKey(p -> p.getName());
Ответ 12
Я сделал общую версию:
private <T, R> Collector<T, ?, Stream<T>> distinctByKey(Function<T, R> keyExtractor) {
return Collectors.collectingAndThen(
toMap(
keyExtractor,
t -> t,
(t1, t2) -> t1
),
(Map<R, T> map) -> map.values().stream()
);
}
Пример:
Stream.of(new Person("Jean"),
new Person("Jean"),
new Person("Paul")
)
.filter(...)
.collect(distinctByKey(Person::getName)) // return a stream of Person with 2 elements, jean and Paul
.map(...)
.collect(toList())
Ответ 13
Подобный подход, который использовал Саид Заринфам, но больше стиля Java 8 :)
persons.collect(Collectors.groupingBy(p -> p.getName())).values().stream()
.map(plans -> plans.stream().findFirst().get())
.collect(toList());
Ответ 14
Set<YourPropertyType> set = new HashSet<>();
list
.stream()
.filter(it -> set.add(it.getYourProperty()))
.forEach(it -> ...);
Ответ 15
Другая библиотека, которая поддерживает это, - jOOλ и ее Seq.distinct(Function<T,U>)
:
Seq.seq(persons).distinct(Person::getName).toList();
Тем не менее, под капотом он делает практически то же самое, что и принятый ответ.
Ответ 16
Список отдельных объектов можно найти с помощью:
List distinctPersons = persons.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person:: getName))),
ArrayList::new));
Ответ 17
Самый простой способ реализовать это - перейти к функции сортировки, поскольку он уже предоставляет необязательный Comparator
, который может быть создан с использованием свойства elements. Затем вам нужно отфильтровать дубликаты, которые могут быть выполнены с помощью statefull Predicate
, который использует тот факт, что для отсортированного потока все равные элементы смежны:
Comparator<Person> c=Comparator.comparing(Person::getName);
stream.sorted(c).filter(new Predicate<Person>() {
Person previous;
public boolean test(Person p) {
if(previous!=null && c.compare(previous, p)==0)
return false;
previous=p;
return true;
}
})./* more stream operations here */;
Конечно, statefull Predicate
не является потокобезопасным, однако, если это необходимо, вы можете переместить эту логику в Collector
и позволить потоку заботиться о безопасности потоков при использовании Collector
. Это зависит от того, что вы хотите сделать с потоком отдельных элементов, о которых вы не сообщили нам в своем вопросе.
Ответ 18
Мой подход заключается в том, чтобы сгруппировать все объекты с одинаковым свойством вместе, затем обрезать группы до размера 1 и, наконец, собрать их как List
.
List<YourPersonClass> listWithDistinctPersons = persons.stream()
//operators to remove duplicates based on person name
.collect(Collectors.groupingBy(p -> p.getName()))
.values()
.stream()
//cut short the groups to size of 1
.flatMap(group -> group.stream().limit(1))
//collect distinct users as list
.collect(Collectors.toList());
Ответ 19
Основываясь на ответе @josketres, я создал общий метод утилиты:
Вы можете сделать это более удобным для Java 8, создав Collector.
public static <T> Set<T> removeDuplicates(Collection<T> input, Comparator<T> comparer) {
return input.stream()
.collect(toCollection(() -> new TreeSet<>(comparer)));
}
@Test
public void removeDuplicatesWithDuplicates() {
ArrayList<C> input = new ArrayList<>();
Collections.addAll(input, new C(7), new C(42), new C(42));
Collection<C> result = removeDuplicates(input, (c1, c2) -> Integer.compare(c1.value, c2.value));
assertEquals(2, result.size());
assertTrue(result.stream().anyMatch(c -> c.value == 7));
assertTrue(result.stream().anyMatch(c -> c.value == 42));
}
@Test
public void removeDuplicatesWithoutDuplicates() {
ArrayList<C> input = new ArrayList<>();
Collections.addAll(input, new C(1), new C(2), new C(3));
Collection<C> result = removeDuplicates(input, (t1, t2) -> Integer.compare(t1.value, t2.value));
assertEquals(3, result.size());
assertTrue(result.stream().anyMatch(c -> c.value == 1));
assertTrue(result.stream().anyMatch(c -> c.value == 2));
assertTrue(result.stream().anyMatch(c -> c.value == 3));
}
private class C {
public final int value;
private C(int value) {
this.value = value;
}
}
Ответ 20
Может быть, это будет полезно для кого-то. У меня было немного другое требование. Имея список объектов A
от третьей стороны, удалите все, у которых есть одно и то же поле Ab
для того же A.id
(множественный объект A
с тем же A.id
в списке). Поток раздел Ответ на Тагир Валеев вдохновил меня, чтобы использовать пользовательский Collector
, который возвращает Map<A.id, List<A>>
. Простая flatMap
сделает все остальное.
public static <T, K, K2> Collector<T, ?, Map<K, List<T>>> groupingDistinctBy(Function<T, K> keyFunction, Function<T, K2> distinctFunction) {
return groupingBy(keyFunction, Collector.of((Supplier<Map<K2, T>>) HashMap::new,
(map, error) -> map.putIfAbsent(distinctFunction.apply(error), error),
(left, right) -> {
left.putAll(right);
return left;
}, map -> new ArrayList<>(map.values()),
Collector.Characteristics.UNORDERED)); }
Ответ 21
В моем случае мне нужно было контролировать то, что было предыдущим элементом. Затем я создал предикат с состоянием, в котором я контролировал, отличался ли предыдущий элемент от текущего элемента, в этом случае я его сохранил.
public List<Log> fetchLogById(Long id) {
return this.findLogById(id).stream()
.filter(new LogPredicate())
.collect(Collectors.toList());
}
public class LogPredicate implements Predicate<Log> {
private Log previous;
public boolean test(Log atual) {
boolean isDifferent = previouws == null || verifyIfDifferentLog(current, previous);
if (isDifferent) {
previous = current;
}
return isDifferent;
}
private boolean verifyIfDifferentLog(Log current, Log previous) {
return !current.getId().equals(previous.getId());
}
}
Ответ 22
Самый простой код, который вы можете написать:
persons.stream().map(x-> x.getName()).distinct().collect(Collectors.toList());
Ответ 23
Если вы хотите, чтобы список людей был простым,
Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());
Кроме того, если вы хотите найти отдельный или уникальный список имен, , а не Person, вы также можете использовать следующие два метода.
Способ 1: использование distinct
persons.stream().map(x->x.getName()).distinct.collect(Collectors.toList());
Метод 2: использование HashSet
Set<E> set = new HashSet<>();
set.addAll(person.stream().map(x->x.getName()).collect(Collectors.toList()));