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í:
- Java 8 Distinct by property 23 respuestas
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.