procesamiento - Java 8 Distinto por propiedad
procesamiento de datos con streams de java se 8-parte 2 (20)
En Java 8, ¿cómo puedo filtrar una colección utilizando la API de Stream
al verificar 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();
Utilizará el control de igualdad predeterminado para un objeto Person
, por lo que necesito algo como:
persons.stream().distinct(p -> p.getName());
Desafortunadamente, el método distinct()
no tiene tal sobrecarga. Sin modificar el control de igualdad dentro de la clase Person
, ¿es posible hacerlo de manera sucinta?
Considere distinct
para ser un filtro de estado . Aquí hay una función que devuelve un predicado que mantiene un estado sobre lo que se vio anteriormente, y que devuelve si el elemento dado se vio por primera vez:
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> 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 preservará un elemento arbitrario de entre los duplicados, en lugar del primero, como lo hace la distinct()
.
(Esto es esencialmente lo mismo que mi respuesta a esta pregunta: Java Lambda Stream Distinct () en clave arbitraria? )
El código más simple que puedes escribir:
persons.stream().map(x-> x.getName()).distinct().collect(Collectors.toList());
Enfoque similar que Saeed Zarinfam usó pero más estilo Java 8 :)
persons.collect(groupingBy(p -> p.getName())).values().stream()
.map(plans -> plans.stream().findFirst().get())
.collect(toList());
Extendiendo la respuesta de Stuart Marks, esto se puede hacer de una manera más corta y sin un mapa concurrente (si no necesita transmisiones paralelas):
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
final Set<Object> seen = new HashSet<>();
return t -> seen.add(keyExtractor.apply(t));
}
Luego llame:
persons.stream().filter(distinctByKey(p -> p.getName());
Hay un enfoque más simple utilizando un TreeSet con un comparador personalizado.
persons.stream()
.collect(Collectors.toCollection(
() -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName()))
));
Hice una versión genérica:
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 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 a la función de clasificación, ya que ya proporciona un Comparator
opcional que puede crearse usando la propiedad de un elemento. Luego, debe filtrar los duplicados, lo que se puede hacer usando un Predicate
estado que usa el hecho de que para una secuencia ordenada todos los elementos iguales son adyacentes:
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 */;
Por supuesto, un Predicate
estado no es seguro para subprocesos, sin embargo, si esa es su necesidad, puede mover esta lógica a un Collector
y dejar que la corriente se encargue de la seguridad de los subprocesos cuando use su Collector
. Esto depende de lo que quiera hacer con el flujo de elementos distintos que no nos dijo en su pregunta.
La lista distinta o única se puede encontrar usando los siguientes dos métodos también.
Método 1: utilizando Distinct
yourObjectName.stream().map(x->x.yourObjectProperty).distinct.collect(Collectors.toList());
Método 2: utilizando HashSet
Set<E> set = new HashSet<>();
set.addAll(yourObjectName.stream().map(x->x.yourObjectProperty).collect(Collectors.toList()));
Otra biblioteca que admite esto es jOOλ , y su Seq.distinct(Function<T,U>)
:
Seq.seq(persons).distinct(Person::getName).toList();
Sin embargo, bajo el capó , hace prácticamente lo mismo que la respuesta aceptada .
Otra solución, utilizando Set
. Puede que no sea la solución ideal, pero funciona.
Set<String> 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 envolver los objetos de la persona en otra clase, que solo compara los nombres de las personas. Después, desenvuelve los objetos envueltos para que una persona vuelva a fluir. Las operaciones de flujo pueden verse como sigue:
persons.stream()
.map(Wrapper::new)
.distinct()
.map(Wrapper::unwrap)
...;
La clase Wrapper
podría tener el siguiente aspecto:
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();
}
}
Puedes 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)));
Puedes usar el método distinct(HashingStrategy)
en Eclipse Collections .
List<Person> persons = ...;
MutableList<Person> distinct =
ListIterate.distinct(persons, HashingStrategies.fromFunction(Person::getName));
Si puede refactorizar a persons
para implementar una interfaz de Eclipse Collections, puede llamar al método directamente en la lista.
MutableList<Person> persons = ...;
MutableList<Person> 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<E>
{
int computeHashCode(E object);
boolean equals(E object1, E object2);
}
Nota: Soy un comendador de Eclipse Collections.
Puedes usar la biblioteca de StreamEx :
StreamEx.of(persons)
.distinct(Person::getName)
.toList()
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
Sobre la base de la respuesta de @josketres, creé un método de utilidad genérico:
Podrías hacer esto más compatible con Java 8 creando 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;
}
}
Tal vez sea de utilidad para alguien. Tuve un poquito otro requisito. Al tener la lista de objetos A
de terceros, elimine todos los que tengan el mismo campo Ab
para el mismo A.id
(objeto A
múltiple con el mismo A.id
en la lista). La respuesta de la partición de flujo por Tagir Valeev me inspiró a usar el Collector
personalizado que devuelve el Map<A.id, List<A>>
. Simple flatMap
hará el resto.
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)); }
También podemos usar RxJava (biblioteca de extensión reactiva muy potente)
Observable.from(persons).distinct(Person::getName)
o
Observable.from(persons).distinct(p -> p.getName())
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á la primera encontrada.
Set<YourPropertyType> set = new HashSet<>();
list
.stream()
.filter(it -> set.add(it.getYourProperty()))
.forEach(it -> ...);