values remove property from collection java lambda java-8

remove - java stream distinct by property



Java Lambda Stream Distinct() en clave arbitraria? (9)

La operación distinct es una operación de canalización con estado ; en este caso es un filtro con estado. Es un poco incómodo crearlos usted mismo, ya que no hay nada incorporado, pero una pequeña clase auxiliar debería hacer el truco:

/** * Stateful filter. T is type of stream element, K is type of extracted key. */ static class DistinctByKey<T,K> { Map<K,Boolean> seen = new ConcurrentHashMap<>(); Function<T,K> keyExtractor; public DistinctByKey(Function<T,K> ke) { this.keyExtractor = ke; } public boolean filter(T t) { return seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } }

No conozco tus clases de dominio, pero creo que, con esta clase auxiliar, podrías hacer lo que quieras así:

BigDecimal totalShare = orders.stream() .filter(new DistinctByKey<Order,CompanyId>(o -> o.getCompany().getId())::filter) .map(Order::getShare) .reduce(BigDecimal.ZERO, BigDecimal::add);

Desafortunadamente, la inferencia de tipos no pudo llegar lo suficientemente lejos dentro de la expresión, por lo que tuve que especificar explícitamente los argumentos de tipo para la clase DistinctByKey .

Esto implica más configuración que el enfoque de coleccionistas descrito por Louis Wasserman , pero tiene la ventaja de que los elementos distintos pasan inmediatamente en lugar de almacenarse hasta que se complete la colección. El espacio debe ser el mismo, ya que (inevitablemente) ambos enfoques terminan acumulando todas las claves distintas extraídas de los elementos de la secuencia.

ACTUALIZAR

Es posible deshacerse del parámetro de tipo K ya que en realidad no se usa para otra cosa que no sea almacenarlo en un mapa. Entonces Object es suficiente.

/** * Stateful filter. T is type of stream element. */ static class DistinctByKey<T> { Map<Object,Boolean> seen = new ConcurrentHashMap<>(); Function<T,Object> keyExtractor; public DistinctByKey(Function<T,Object> ke) { this.keyExtractor = ke; } public boolean filter(T t) { return seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } } BigDecimal totalShare = orders.stream() .filter(new DistinctByKey<Order>(o -> o.getCompany().getId())::filter) .map(Order::getShare) .reduce(BigDecimal.ZERO, BigDecimal::add);

Esto simplifica un poco las cosas, pero aún tenía que especificar el argumento de tipo al constructor. Intentar usar diamantes o un método de fábrica estático no parece mejorar las cosas. Creo que la dificultad es que el compilador no puede inferir parámetros de tipo genérico, para un constructor o una llamada a un método estático, cuando está en la expresión de instancia de una referencia de método. Oh bien.

(Otra variación de esto que probablemente lo simplifique es hacer que DistinctByKey<T> implements Predicate<T> y cambie el nombre del método a eval . Esto eliminaría la necesidad de usar una referencia de método y probablemente mejoraría la inferencia de tipos. Sin embargo, es poco probable para ser tan agradable como la solución a continuación.)

ACTUALIZACIÓN 2

No puedo dejar de pensar en esto. En lugar de una clase auxiliar, use una función de orden superior. ¡Podemos usar locales capturados para mantener el estado, por lo que ni siquiera necesitamos una clase separada! Bono, las cosas se simplifican, ¡así que la inferencia de tipos funciona!

public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) { Map<Object,Boolean> seen = new ConcurrentHashMap<>(); return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } BigDecimal totalShare = orders.stream() .filter(distinctByKey(o -> o.getCompany().getId())) .map(Order::getShare) .reduce(BigDecimal.ZERO, BigDecimal::add);

Esta pregunta ya tiene una respuesta aquí:

Con frecuencia me encontré con un problema con las expresiones lambda de Java en las que cuando quería distinguir () una secuencia en una propiedad o método arbitrario de un objeto, pero quería mantener el objeto en lugar de asignarlo a esa propiedad o método. Comencé a crear contenedores como se discutió aquí, pero comencé a hacerlo lo suficiente como para que se volviera molesto e hice muchas clases repetitivas.

Reuní esta clase de emparejamiento, que contiene dos objetos de dos tipos y le permite especificar la incrustación de los objetos izquierdo, derecho o ambos. Mi pregunta es ... ¿realmente no hay una función de flujo lambda incorporada para distinguir () en un proveedor clave de algún tipo? Eso realmente me sorprendería. Si no, ¿cumplirá esta clase esa función de manera confiable?

Así es como se llamaría

BigDecimal totalShare = orders.stream().map(c -> Pairing.keyLeft(c.getCompany().getId(), c.getShare())).distinct().map(Pairing::getRightItem).reduce(BigDecimal.ZERO, (x,y) -> x.add(y));

Aquí está la clase de emparejamiento

public final class Pairing<X,Y> { private final X item1; private final Y item2; private final KeySetup keySetup; private static enum KeySetup {LEFT,RIGHT,BOTH}; private Pairing(X item1, Y item2, KeySetup keySetup) { this.item1 = item1; this.item2 = item2; this.keySetup = keySetup; } public X getLeftItem() { return item1; } public Y getRightItem() { return item2; } public static <X,Y> Pairing<X,Y> keyLeft(X item1, Y item2) { return new Pairing<X,Y>(item1, item2, KeySetup.LEFT); } public static <X,Y> Pairing<X,Y> keyRight(X item1, Y item2) { return new Pairing<X,Y>(item1, item2, KeySetup.RIGHT); } public static <X,Y> Pairing<X,Y> keyBoth(X item1, Y item2) { return new Pairing<X,Y>(item1, item2, KeySetup.BOTH); } public static <X,Y> Pairing<X,Y> forItems(X item1, Y item2) { return keyBoth(item1, item2); } @Override public int hashCode() { final int prime = 31; int result = 1; if (keySetup.equals(KeySetup.LEFT) || keySetup.equals(KeySetup.BOTH)) { result = prime * result + ((item1 == null) ? 0 : item1.hashCode()); } if (keySetup.equals(KeySetup.RIGHT) || keySetup.equals(KeySetup.BOTH)) { result = prime * result + ((item2 == null) ? 0 : item2.hashCode()); } return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Pairing<?,?> other = (Pairing<?,?>) obj; if (keySetup.equals(KeySetup.LEFT) || keySetup.equals(KeySetup.BOTH)) { if (item1 == null) { if (other.item1 != null) return false; } else if (!item1.equals(other.item1)) return false; } if (keySetup.equals(KeySetup.RIGHT) || keySetup.equals(KeySetup.BOTH)) { if (item2 == null) { if (other.item2 != null) return false; } else if (!item2.equals(other.item2)) return false; } return true; } }

ACTUALIZAR:

Probamos la función de Stuart a continuación y parece funcionar muy bien. La siguiente operación distingue en la primera letra de cada cadena. La única parte que estoy tratando de descubrir es cómo ConcurrentHashMap mantiene solo una instancia para toda la transmisión

public class DistinctByKey { public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) { Map<Object,Boolean> seen = new ConcurrentHashMap<>(); return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } public static void main(String[] args) { final ImmutableList<String> arpts = ImmutableList.of("ABQ","ALB","CHI","CUN","PHX","PUJ","BWI"); arpts.stream().filter(distinctByKey(f -> f.substring(0,1))).forEach(s -> System.out.println(s)); }

La salida es ...

ABQ CHI PHX BWI


Más o menos tienes que hacer algo como

elements.stream() .collect(Collectors.toMap( obj -> extractKey(obj), obj -> obj, (first, second) -> first // pick the first if multiple values have the same key )).values().stream();


Otra forma de encontrar elementos distintos.

List<String> list = Lists.mutable.with("ABQ", "ALB", "CHI", "CUN", "PHX", "PUJ", "BWI"); ListIterate.distinct(list, HashingStrategies.fromFunction(s -> s.substring(0, 1))) .each(System.out::println);


Para responder a su pregunta en su segunda actualización:

La única parte que estoy tratando de descubrir es cómo ConcurrentHashMap mantiene solo una instancia para toda la transmisión:

public static <T> Predicate<T> distinctByKey(Function<? super T,Object> keyExtractor) { Map<Object,Boolean> seen = new ConcurrentHashMap<>(); return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; }

En su ejemplo de código, distinctByKey solo se invoca una vez, por lo que ConcurrentHashMap se creó solo una vez. Aquí hay una explicación:

La función distinctByKey es simplemente una función antigua que devuelve un objeto, y ese objeto resulta ser un predicado. Tenga en cuenta que un predicado es básicamente un fragmento de código que se puede evaluar más adelante. Para evaluar manualmente un predicado, debe llamar a un método en la interfaz de predicado , como test . Entonces, el predicado

t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null

es simplemente una declaración que no se evalúa realmente dentro de distinctByKey .

El predicado se pasa como cualquier otro objeto. Se devuelve y pasa a la operación de filter , que básicamente evalúa el predicado repetidamente contra cada elemento de la secuencia llamando a test .

Estoy seguro de que el filter es más complicado de lo que pensé, pero el punto es que el predicado se evalúa muchas veces fuera de distinctByKey . No hay nada especial * en distinctByKey ; es solo una función que ha llamado una vez, por lo que ConcurrentHashMap solo se crea una vez.

* Además de estar bien hecho, @ stuart-marks :)


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

MutableList<String> list = Lists.mutable.with("ABQ", "ALB", "CHI", "CUN", "PHX", "PUJ", "BWI"); list.distinct(HashingStrategies.fromFunction(s -> s.substring(0, 1))) .each(System.out::println);

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

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

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

List<String> uniqueObjects = ImmutableList.of("ABQ","ALB","CHI","CUN","PHX","PUJ","BWI") .stream() .collect(Collectors.groupingBy((p)->p.substring(0,1))) //expression .values() .stream() .flatMap(e->e.stream().limit(1)) .collect(Collectors.toList());

Nota: Soy un committer para Eclipse Collections.


Se puede hacer algo como

Set<String> distinctCompany = orders.stream() .map(Order::getCompany) .collect(Collectors.toSet());


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 variación de la segunda actualización de Stuart Marks. Usando un conjunto.

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


Set.add(element) devuelve verdadero si el conjunto aún no contenía element , de lo contrario falso. Entonces puedes hacer así.

Set<String> set = new HashSet<>(); BigDecimal totalShare = orders.stream() .filter(c -> set.add(c.getCompany().getId())) .map(c -> c.getShare()) .reduce(BigDecimal.ZERO, BigDecimal::add);

Si desea hacer esto en paralelo, debe usar el mapa concurrente.