it-swarm.com.ru

Java 8 Различается по свойству

В Java 8, как я могу отфильтровать коллекцию, используя API Stream, проверив отличимость свойства каждого объекта?

Например, у меня есть список объекта Person, и я хочу удалить людей с тем же именем,

persons.stream().distinct();

Будет использовать проверку равенства по умолчанию для объекта Person, поэтому мне нужно что-то вроде

persons.stream().distinct(p -> p.getName());

К сожалению, метод distinct() не имеет такой перегрузки. Без изменения проверки равенства внутри класса Person возможно ли сделать это кратко?

306
RichK

Считайте distinct фильтром stateful. Вот функция, которая возвращает предикат, который поддерживает состояние того, что он видел ранее, и который возвращает, был ли данный элемент виден в первый раз:

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 () для произвольного ключа? )

382
Stuart Marks

Альтернативой может быть размещение людей на карте с использованием имени в качестве ключа:

persons.collect(toMap(Person::getName, p -> p, (p, q) -> p)).values();

Обратите внимание, что лицо, которое сохраняется, в случае дублирования имени, будет первым зарегистрированным.

95
wha'eve'

Вы можете обернуть объекты человека в другой класс, который сравнивает только имена людей. После этого вы разворачиваете обернутые объекты, чтобы снова получить поток людей. Потоковые операции могут выглядеть следующим образом:

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();
    }
}
76
nosid

Мы также можем использовать RxJava (очень мощное реактивное расширение библиотека)

Observable.from(persons).distinct(Person::getName)

или же

Observable.from(persons).distinct(p -> p.getName())
23
frhack

Существует более простой подход с использованием TreeSet с пользовательским компаратором.

persons.stream()
    .collect(Collectors.toCollection(
      () -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName())) 
));
22
josketres

Другое решение, использующее 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()));
19
Santhosh

Вы можете использовать метод distinct(HashingStrategy) в Коллекции Eclipse .

List<Person> persons = ...;
MutableList<Person> distinct =
    ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));

Если вы можете выполнить рефакторинг persons для реализации интерфейса коллекций Eclipse, вы можете вызвать метод непосредственно в списке.

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 Collections.

9
Craig P. Motlin

Я рекомендую использовать Vavr , если можете. С помощью этой библиотеки вы можете сделать следующее:

io.vavr.collection.List.ofAll(persons)
                       .distinctBy(Person::getName)
                       .toJavaSet() // or any another Java 8 Collection
8
Mateusz Rasiński

Вы можете использовать StreamEx library:

StreamEx.of(persons)
        .distinct(Person::getName)
        .toList()
7
Sllouyssgort

Вы можете использовать groupingBy сборщик:

persons.collect(groupingBy(p -> p.getName())).values().forEach(t -> System.out.println(t.get(0).getId()));

Если вы хотите иметь другой поток, вы можете использовать это:

persons.collect(groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));
6
Saeed Zarinfam

Я сделал общую версию:

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())
6
Guillaume Cornet

Расширяя ответ Стюарта Маркса, это можно сделать более коротким способом и без одновременной карты (если вам не нужны параллельные потоки):

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());
5
Wojciech Górski
Set<YourPropertyType> set = new HashSet<>();
list
        .stream()
        .filter(it -> set.add(it.getYourProperty()))
        .forEach(it -> ...);
4
Andrew Novitskyi

Подобный подход, который использовал Саид Заринфам, но больше стиля Java 8 :)

persons.collect(groupingBy(p -> p.getName())).values().stream()
 .map(plans -> plans.stream().findFirst().get())
 .collect(toList());
4
asdasdsdf

Другая библиотека, которая поддерживает это, - jOOλ , и ее Seq.distinct(Function<T,U>) метод:

Seq.seq(persons).distinct(Person::getName).toList();

Под капотом , он делает практически то же самое, что и принятый ответ , хотя.

4
Tomasz Linkowski

Самый простой способ реализовать это - использовать функцию сортировки, поскольку она уже предоставляет необязательную переменную Comparator, которую можно создать с помощью свойства элемента. Затем необходимо отфильтровать дубликаты, что можно сделать с помощью 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. Это зависит от того, что вы хотите сделать с потоком отдельных элементов, которые вы не сказали нам в своем вопросе.

2
Holger

Основываясь на ответе @ 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;
    }
}
1
Garrett Smith

Список отдельных объектов можно найти с помощью: -

 List distnictPersons = persons.stream()
                    .collect(Collectors.collectingAndThen(
                            Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Person:: getName))),
                            ArrayList::new));
1
Naveen Dhalaria

Мой подход заключается в том, чтобы сгруппировать все объекты с одинаковым свойством вместе, затем обрезать группы до размера 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());
0
uneq95

Может быть кому-нибудь пригодится. У меня было немного другое требование. Имея список объектов A от стороннего производителя, удалите все объекты, имеющие одинаковое поле A.b для одного и того же 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)); }
0
Aliaksei Yatsau