Java 8 Distinto por propiedad


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

Por ejemplo tengo una lista de Person objeto 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());

Desafortunadamente el método distinct() no tiene tal sobrecarga. Sin modificar la comprobación de igualdad dentro de la clase Person es posible hacer esto sucintamente?

Author: Andrew Tobilko, 2014-05-16

20 answers

Considera que distinctes un filtro con estado . Aquí hay una función que devuelve un predicado que mantiene el estado sobre lo que se ha visto anteriormente, y que devuelve si el elemento dado fue visto 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 el flujo está ordenado y se ejecuta en paralelo, esto preservará un elemento arbitrario entre los duplicados, en lugar del primero, como lo hace distinct().

(Esto es esencialmente el igual que mi respuesta a esta pregunta: Java Lambda Stream Distinct() on arbitrary key?)

 320
Author: Stuart Marks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-17 05:27:43

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 encontered.

 83
Author: wha'eve',
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-16 22:56:00

Puede envolver los objetos person en otra clase, que solo compara los nombres de las personas. Después, desenvuelve los objetos envueltos para obtener una transmisión de persona de nuevo. Las operaciones de flujo podrían verse como sigue:

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

La clase Wrapper podría verse como sigue:

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();
    }
}
 70
Author: nosid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-18 07:45:38

Hay un enfoque más simple usando un conjunto de árboles con un comparador personalizado.

persons.stream()
    .collect(Collectors.toCollection(
      () -> new TreeSet<Person>((p1, p2) -> p1.getName().compareTo(p2.getName())) 
));
 21
Author: josketres,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-12 15:28:43

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())
 20
Author: frhack,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-24 23:52:37

Otra solución, usando 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()));
 11
Author: Santhosh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-18 07:56:03

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

List<Person> persons = ...;
MutableList<Person> 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<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 equals y hashcode.

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

Nota: Soy un committer para las Colecciones de Eclipse.

 9
Author: Craig P. Motlin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-11 21:51:27

Extendiendo la respuesta de Stuart Marks, esto se puede hacer de una manera más corta y sin un mapa concurrente (si no necesita flujos paralelos):

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

Luego llama:

persons.stream().filter(distinctByKey(p -> p.getName());
 6
Author: Wojciech Górski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-12-27 22:37:58

Te 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
 6
Author: Mateusz Rasiński,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-16 05:14:14

Puedes usar StreamEx biblioteca:

StreamEx.of(persons)
        .distinct(Person::getName)
        .toList()
 4
Author: Sllouyssgort,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-17 15:25:41

Enfoque similar que usó Saeed Zarinfam pero más estilo Java 8:)

persons.collect(groupingBy(p -> p.getName())).values().stream()
 .map(plans -> plans.stream().findFirst().get())
 .collect(toList());
 4
Author: asdasdsdf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-17 08:06:43

Puedes usar groupingBy collector:

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

Si quieres tener otro stream puedes usar esto:

persons.collect(groupingBy(p -> p.getName())).values().stream().map(l -> (l.get(0)));
 3
Author: Saeed Zarinfam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-30 05:39:22

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())
 3
Author: Guillaume Cornet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-07-12 15:12:55

La forma más fácil de implementar esto es saltar a la característica ordenar, ya que ya proporciona un Comparator opcional que se puede crear utilizando la propiedad de un elemento. Luego tienes que filtrar los duplicados, lo que se puede hacer usando un statefull Predicate 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 statefull 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 se encargue de la hilo-seguridad al usar su Collector. Esto depende de lo que quieras hacer con el flujo de elementos distintos que no nos dijiste en tu pregunta.

 2
Author: Holger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-19 08:58:47

Otra biblioteca que soporta esto es jOOλ , y su Seq.distinct(Function<T,U>) método:

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

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

 2
Author: Tomasz Linkowski,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-27 11:11:51

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

Podría hacer que esto sea más compatible con Java 8 creando un Recopilador .

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
Author: Garrett Smith,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-15 11:11:53

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

    persons.stream().map(x-> x.getName()).distinct().collect(Collectors.toList());
 1
Author: 2Big2BeSmall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-08 06:42:37

Tal vez sea útil para alguien. Tenía un poco otro requisito. Tener lista de objetos A de 3rd party elimina todos los que tienen el mismo campo A.b para el mismo A.id (objeto A múltiple con el mismo A.id en la lista). Stream partitionrespuesta de Tagir Valeev me inspiró a usar custom Collector que devuelve 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)); }
 0
Author: Aliaksei Yatsau,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-29 10:02:09
Set<YourPropertyType> set = new HashSet<>();
list
        .stream()
        .filter(it -> set.add(it.getYourProperty()))
        .forEach(it -> ...);
 0
Author: Andrew Novitskyi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-07 11:05:30

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

Método 1: usando Distinto

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

Método 2: usando HashSet

Set<E> set = new HashSet<>();
set.addAll(yourObjectName.stream().map(x->x.yourObjectProperty).collect(Collectors.toList()));
 -1
Author: Abdur Rahman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-04 06:30:02