Java 8 Distinto per proprietà

In Java 8 come è ansible filtrare una raccolta utilizzando l’API Stream controllando la distinzione di una proprietà di ciascun object?

Ad esempio, ho una lista di oggetti Person e voglio rimuovere persone con lo stesso nome,

 persons.stream().distinct(); 

Userà il controllo di uguaglianza di default per un object Person , quindi ho bisogno di qualcosa del tipo,

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

Sfortunatamente il metodo distinct() non ha tale sovraccarico. È ansible fare questo sinteticamente senza modificare il controllo di uguaglianza all’interno della class Person ?

Considerare distinct per essere un filtro stateful . Ecco una funzione che restituisce un predicato che mantiene lo stato su ciò che è visto in precedenza e che restituisce se l’elemento dato è stato visto per la prima volta:

 public static  Predicate distinctByKey(Function keyExtractor) { Set seen = ConcurrentHashMap.newKeySet(); return t -> seen.add(keyExtractor.apply(t)); } 

Quindi puoi scrivere:

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

Notare che se lo stream è ordinato ed è eseguito in parallelo, questo preserverà un elemento arbitrario tra i duplicati, invece del primo, come distinct() .

(Questo è essenzialmente lo stesso della mia risposta a questa domanda: Java Lambda Stream Distinct () su chiave arbitraria? )

Un’alternativa sarebbe quella di posizionare le persone in una mappa usando il nome come chiave:

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

Si noti che la Persona che viene mantenuta, in caso di un nome duplicato, sarà la prima incoerente.

Puoi avvolgere gli oggetti personali in un’altra class, che confronta solo i nomi delle persone. Successivamente, si scartano gli oggetti avvolti per ottenere di nuovo un stream di persone. Le operazioni di streaming potrebbero essere come segue:

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

La class Wrapper potrebbe apparire come segue:

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

C’è un approccio più semplice usando un TreeSet con un comparatore personalizzato.

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

Possiamo anche usare RxJava (libreria di estensione retriggers molto potente)

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

o

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

Un’altra soluzione, usando Set . Potrebbe non essere la soluzione ideale, ma funziona

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

O se è ansible modificare l’elenco originale, è ansible utilizzare il metodo removeIf

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

Puoi utilizzare il metodo distinct(HashingStrategy) in Eclipse Collections .

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

Se puoi debind le persons a implementare un’interfaccia Eclipse Collections, puoi chiamare il metodo direttamente nell’elenco.

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

HashingStrategy è semplicemente un’interfaccia di strategia che ti consente di definire implementazioni personalizzate di equals e hashcode.

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

Nota: sono un committer per le raccolte di Eclipse.

Estendendo la risposta di Stuart Marks, questo può essere fatto in un modo più breve e senza una mappa concorrente (se non hai bisogno di flussi paralleli):

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

Quindi chiama:

 persons.stream().filter(distinctByKey(p -> p.getName()); 

Consiglio di usare Vavr , se puoi. Con questa libreria puoi fare quanto segue:

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

Puoi usare la libreria StreamEx :

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

Approccio simile a quello usato da Saeed Zarinfam, ma più stile Java 8 🙂

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

Puoi utilizzare groupingBy collector:

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

Se vuoi avere un altro stream puoi usare questo:

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

Ho fatto una versione generica:

 private  Collector> distinctByKey(Function keyExtractor) { return Collectors.collectingAndThen( toMap( keyExtractor, t -> t, (t1, t2) -> t1 ), (Map map) -> map.values().stream() ); } 

Un esempio:

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

Il modo più semplice per implementare questo è saltare sulla funzionalità di ordinamento in quanto fornisce già un Comparator opzionale che può essere creato utilizzando la proprietà di un elemento. Quindi devi filtrare i duplicati che possono essere fatti usando un Predicate statefull che usa il fatto che per un stream ordinato tutti gli elementi uguali sono adiacenti:

 Comparator c=Comparator.comparing(Person::getName); stream.sorted(c).filter(new Predicate() { 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 */; 

Ovviamente, un Predicate statefull non è thread-safe, tuttavia se questo è necessario, è ansible spostare questa logica in un servizio di Collector e lasciare che lo stream si occupi della sicurezza del thread quando si utilizza il servizio di Collector . Questo dipende da cosa vuoi fare con il stream di elementi distinti che non hai detto nella tua domanda.

Basandosi sulla risposta di @josketres, ho creato un metodo di utilità generico:

Potresti rendere questo più adatto a Java 8 creando un Collector .

 public static  Set removeDuplicates(Collection input, Comparator comparer) { return input.stream() .collect(toCollection(() -> new TreeSet<>(comparer))); } @Test public void removeDuplicatesWithDuplicates() { ArrayList input = new ArrayList<>(); Collections.addAll(input, new C(7), new C(42), new C(42)); Collection 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 input = new ArrayList<>(); Collections.addAll(input, new C(1), new C(2), new C(3)); Collection 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; } } 

Un’altra libreria che supporta questo è jOOλ e il suo Seq.distinct(Function) :

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

Sotto il cofano , fa praticamente la stessa cosa della risposta accettata , però.

Il codice più semplice che puoi scrivere:

  persons.stream().map(x-> x.getName()).distinct().collect(Collectors.toList()); 

Forse sarà utile per qualcuno. Ho avuto un po ‘di un altro requisito. Avendo una lista di oggetti A di terze parti rimuovi tutti quelli che hanno lo stesso campo Ab per lo stesso A.id (più A object A con lo stesso A.id nell’elenco). La risposta della partizione dello streaming di Tagir Valeev mi ha ispirato a utilizzare il servizio di Collector personalizzato che restituisce la Map> . flatMap semplice farà il resto.

  public static  Collector>> groupingDistinctBy(Function keyFunction, Function distinctFunction) { return groupingBy(keyFunction, Collector.of((Supplier>) 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)); } 

Un elenco distinto o univoco può essere trovato usando anche i seguenti due metodi.

Metodo 1: utilizzando Distinto

 yourObjectName.stream().map(x->x.yourObjectProperty).distinct.collect(Collectors.toList()); 

Metodo 2: utilizzo di HashSet

 Set set = new HashSet<>(); set.addAll(yourObjectName.stream().map(x->x.yourObjectProperty).collect(Collectors.toList()));