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 -> ...);

Ответ 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()));