web-dev-qa-db-fra.com

Java 8 Distinct par propriété

Dans Java 8, comment puis-je filtrer une collection à l'aide de l'API Stream en vérifiant la distinction d'une propriété de chaque objet?

Par exemple, j'ai une liste d'objet Person et je veux supprimer les personnes portant le même nom,

persons.stream().distinct();

Utilisera le contrôle d'égalité par défaut pour un objet Person, donc j'ai besoin de quelque chose comme,

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

Malheureusement, la méthode distinct() n'a pas cette surcharge. Sans modifier le contrôle d'égalité dans la classe Person, est-il possible de le faire de manière succincte?

306
RichK

distinct est un filtre stateful. Voici une fonction qui retourne un prédicat qui maintient l'état de ce qu'elle a vu précédemment et qui indique si l'élément donné a été vu pour la première fois:

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> seen.add(keyExtractor.apply(t));
}

Ensuite, vous pouvez écrire:

persons.stream().filter(distinctByKey(Person::getName))

Notez que si le flux est commandé et est exécuté en parallèle, cela conservera un élément arbitrary parmi les doublons, au lieu du premier, comme le fait distinct().

(Ceci est essentiellement identique à ma réponse à cette question: Java Lambda Stream Distinct () on key arbitraire? )

382
Stuart Marks

Une alternative serait de placer les personnes sur une carte en utilisant le nom comme clé:

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

Notez que la personne qui est conservée, en cas de nom dupliqué, sera la première personne rencontrée.

95
wha'eve'

Vous pouvez envelopper les objets personne dans une autre classe, qui compare uniquement les noms des personnes. Ensuite, vous déballez les objets enveloppés pour récupérer un flux de personne. Les opérations de flux peuvent ressembler à ceci:

persons.stream()
    .map(Wrapper::new)
    .distinct()
    .map(Wrapper::unwrap)
    ...;

La classe Wrapper pourrait ressembler à ceci:

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

Nous pouvons aussi utiliser RxJava (très puissant extension réactive bibliothèque)

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

ou

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

Il existe une approche plus simple utilisant un TreeSet avec un comparateur personnalisé.

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

Une autre solution, en utilisant Set. Peut-être pas la solution idéale, mais ça marche

Set<String> set = new HashSet<>(persons.size());
persons.stream().filter(p -> set.add(p.getName())).collect(Collectors.toList());

Ou si vous pouvez modifier la liste d'origine, vous pouvez utiliser removeIf method

persons.removeIf(p -> !set.add(p.getName()));
19
Santhosh

Vous pouvez utiliser la méthode distinct(HashingStrategy) dans Collections Eclipse .

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

Si vous pouvez refactoriser persons pour implémenter une interface de collections Eclipse, vous pouvez appeler la méthode directement dans la liste.

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

HashingStrategy est simplement une interface de stratégie qui vous permet de définir des implémentations personnalisées d’égal à égal et de hashcode.

public interface HashingStrategy<E>
{
    int computeHashCode(E object);
    boolean equals(E object1, E object2);
}

Remarque: je suis un partisan des collections Eclipse.

9
Craig P. Motlin

Je recommande d'utiliser Vavr , si vous le pouvez. Avec cette bibliothèque, vous pouvez effectuer les opérations suivantes:

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

Vous pouvez utiliser StreamEx library:

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

Vous pouvez utiliser groupingBy collector:

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

Si vous voulez avoir un autre flux, vous pouvez utiliser ceci:

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

J'ai créé une version générique:

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

Un exemple:

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

En prolongeant la réponse de Stuart Marks, cela peut être fait de manière plus courte et sans carte simultanée (si vous n'avez pas besoin de flux parallèles):

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    final Set<Object> seen = new HashSet<>();
    return t -> seen.add(keyExtractor.apply(t));
}

Puis appelez:

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

Une approche similaire à celle utilisée par Saeed Zarinfam mais davantage de Java 8:

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

Une autre bibliothèque supportant ceci est jOOλ , et sa Seq.distinct(Function<T,U>) method:

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

Sous le capot , il fait pratiquement la même chose que le réponse acceptée , cependant.

4
Tomasz Linkowski

La méthode la plus simple consiste à accéder à la fonction de tri car elle fournit déjà une variable optionnelle Comparator qui peut être créée à l’aide de la propriété d’un élément. Ensuite, vous devez filtrer les doublons, ce qui peut être fait en utilisant un état Predicate qui utilise le fait que pour un flux trié, tous les éléments égaux sont adjacents:

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 */;

Bien sûr, une variable Predicate avec state n'est pas thread-safe, mais si tel est votre besoin, vous pouvez déplacer cette logique dans une variable Collector et laisser le flux se charger de la sécurité des threads lorsque vous utilisez votre Collector. Cela dépend de ce que vous voulez faire avec le flux d’éléments distincts que vous ne nous avez pas dit dans votre question.

2
Holger

En me basant sur la réponse de @ josketres, j'ai créé une méthode utilitaire générique:

Vous pouvez rendre cela plus convivial avec Java 8 en créant un 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

La liste des objets distincts peut être trouvée en utilisant: -

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

Mon approche consiste à regrouper tous les objets ayant la même propriété, puis à réduire la taille des groupes à 1, puis à les collecter sous la forme d'une 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

Peut-être sera utile pour quelqu'un. J'ai eu un petit peu une autre exigence. Avoir une liste d'objets A de la 3ème partie supprime tous ceux qui ont le même champ A.b pour le même A.id (plusieurs objets A avec le même A.id dans la liste). Partition de flux réponse par Tagir Valeev m'a inspiré pour utiliser la variable personnalisée Collector qui renvoie Map<A.id, List<A>>. flatMap simple fera le reste.

 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

Dans mon cas, je devais contrôler quel était l'élément précédent. J'ai ensuite créé un prédicat stateful dans lequel je contrôlais si l'élément précédent était différent de l'élément actuel, dans ce cas je le conservais.

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

}

0
Flavio Oliva