Java 8 Distinct por propiedad

En Java 8, ¿cómo puedo filtrar una colección utilizando Stream API verificando la distinción de una propiedad de cada objeto?

Por ejemplo, tengo una lista de objetos Person y quiero eliminar personas con el mismo nombre,

 persons.stream().distinct(); 

Usaré la comprobación de igualdad predeterminada para un objeto Person , así que necesito algo como,

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

Lamentablemente, el método distinct() no tiene esa sobrecarga. Sin modificar el control de igualdad dentro de la clase Person , ¿es posible hacer esto de forma sucinta?

Considere distinct para ser un filtro con estado . Aquí hay una función que devuelve un predicado que mantiene el estado sobre lo que se vio anteriormente, y que devuelve si el elemento dado se vio por primera vez:

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

Entonces puedes escribir:

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

Tenga en cuenta que si la secuencia se ordena y se ejecuta en paralelo, esto conservará un elemento arbitrario entre los duplicados, en lugar del primero, como lo hace distinct() .

(Esto es esencialmente lo mismo que mi respuesta a esta pregunta: Java Lambda Stream Distinct () en clave arbitraria? )

Una alternativa sería colocar a las personas en un mapa usando el nombre como clave:

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

Tenga en cuenta que la persona que se mantiene, en caso de un nombre duplicado, será el primero encontered.

Puede envolver los objetos de persona en otra clase, que solo compara los nombres de las personas. Después, desenvuelve los objetos envueltos para obtener una transmisión de persona otra vez. Las operaciones de la secuencia pueden verse de la siguiente manera:

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

La clase Wrapper podría verse de la siguiente manera:

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

Hay un enfoque más simple usando un TreeSet con un comparador personalizado.

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

También podemos usar RxJava (biblioteca de extensión reactiva muy poderosa)

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

o

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

Otra solución, usando Set . Puede que no sea la solución ideal, pero funciona

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

O si puede modificar la lista original, puede usar el método removeIf

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

Puede usar el método distinct(HashingStrategy) en las colecciones de Eclipse .

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

Si puede refactorizar persons para implementar una interfaz de colecciones de Eclipse, puede llamar al método directamente en la lista.

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

HashingStrategy es simplemente una interfaz de estrategia que le permite definir implementaciones personalizadas de iguales y hashcode.

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

Nota: soy un committer para las colecciones de Eclipse.

Extendiendo la respuesta de Stuart Marks, esto se puede hacer de una manera más breve y sin un mapa simultáneo (si no necesita secuencias paralelas):

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

Luego llame:

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

Recomiendo usar Vavr , si puedes. Con esta biblioteca puedes hacer lo siguiente:

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

Puede usar la biblioteca de StreamEx :

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

Enfoque similar que utilizó Saeed Zarinfam pero más estilo Java 8 🙂

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

Puede usar groupingBy collector:

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

Si quieres tener otra transmisión, puedes usar esto:

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

Hice una versión genérica:

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

Un ejemplo:

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

La forma más fácil de implementar esto es saltar sobre la función de ordenamiento, ya que proporciona un Comparator opcional que se puede crear utilizando la propiedad de un elemento. Luego tiene que filtrar los duplicados, lo que se puede hacer usando un Predicate statefull que utiliza el hecho de que para una secuencia ordenada todos los elementos iguales son adyacentes:

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

Por supuesto, un Predicate lleno de Predicate no es seguro para subprocesos, sin embargo, si esa es su necesidad, puede mover esta lógica a un Collector y dejar que la secuencia cumpla con la seguridad de subprocesos al usar su Collector . Esto depende de lo que quieras hacer con la secuencia de elementos distintos que no nos dijiste en tu pregunta.

Basándome en la respuesta de @josketres, creé un método de utilidad genérico:

Puede hacer que esto sea más amigable para Java 8 al crear un recostackdor .

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

Otra biblioteca que admite esto es jOOλ , y su Seq.distinct(Function) :

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

Bajo el capó , hace prácticamente lo mismo que la respuesta aceptada , sin embargo.

El código más simple que puedes escribir:

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

Tal vez será útil para alguien. Tenía un poco otro requisito. Al tener una lista de objetos A de un tercero, elimine todos los que tengan el mismo campo Ab para el mismo A.id ( A objeto A con la misma A.id en la lista). La respuesta de la partición de Stream por Tagir Valeev me inspiró a usar Collector personalizado que devuelve Map> . Simple flatMap hará el rest.

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

También se puede encontrar una lista distinta o única usando los dos métodos siguientes.

Método 1: usar Distinct

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

Método 2: usando HashSet

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