Java Lambda Stream Distinct () su chiave arbitraria?

Ho spesso riscontrato un problema con le espressioni lambda Java quando volevo distinguere () un stream su una proprietà o un metodo arbitrario di un object, ma volevo mantenere l’object anziché mapparlo a quella proprietà o metodo. Ho iniziato a creare i contenitori come discusso qui, ma ho iniziato a farlo abbastanza da diventare fastidioso e ho fatto un sacco di lezioni sulle piastre.

Ho riunito questa class Pairing, che contiene due oggetti di due tipi e ti consente di specificare la trasparenza fuori da sinistra, destra o da entrambi gli oggetti. La mia domanda è … c’è davvero nessuna funzione di stream lambda incorporata per distinguere () su un fornitore chiave di qualche tipo? Questo mi sorprenderebbe davvero. In caso contrario, questa class soddisferà tale funzione in modo affidabile?

Ecco come si chiamerebbe

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

Ecco la lezione di accoppiamento

  public final class Pairing { 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  Pairing keyLeft(X item1, Y item2) { return new Pairing(item1, item2, KeySetup.LEFT); } public static  Pairing keyRight(X item1, Y item2) { return new Pairing(item1, item2, KeySetup.RIGHT); } public static  Pairing keyBoth(X item1, Y item2) { return new Pairing(item1, item2, KeySetup.BOTH); } public static  Pairing 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; } } 

AGGIORNARE:

Testata la funzione di Stuart sotto e sembra funzionare alla grande. L’operazione sotto distingue sulla prima lettera di ogni stringa. L’unica parte che sto cercando di capire è come ConcurrentHashMap mantiene solo un’istanza per l’intero stream

 public class DistinctByKey { public static  Predicate distinctByKey(Function keyExtractor) { Map seen = new ConcurrentHashMap(); return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; } public static void main(String[] args) { final ImmutableList 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)); } 

L’uscita è …

 ABQ CHI PHX BWI 

L’operazione distinct è un’operazione di pipeline stateful ; in questo caso è un filtro stateful. È un po ‘scomodo crearli da soli, poiché non c’è niente di integrato, ma una piccola class di supporto dovrebbe fare il trucco:

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

Non conosco le tue classi di dominio, ma penso che, con questa class di supporto, potresti fare ciò che vuoi in questo modo:

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

Sfortunatamente l’inferenza di tipo non è riuscita ad arrivare abbastanza lontano all’interno dell’espressione, quindi ho dovuto specificare esplicitamente gli argomenti di tipo per la class DistinctByKey .

Ciò comporta più setup rispetto all’approccio dei collezionisti descritto da Louis Wasserman , ma questo ha il vantaggio che gli elementi distinti passano immediatamente invece di essere bufferizzati fino al completamento della raccolta. Lo spazio dovrebbe essere lo stesso, poiché (inevitabilmente) entrambi gli approcci finiscono per accumulare tutte le chiavi distinte estratte dagli elementi del stream.

AGGIORNARE

È ansible sbarazzarsi del parametro di tipo K dal momento che non è effettivamente utilizzato per qualcosa di diverso da essere memorizzato in una mappa. Quindi l’ Object è sufficiente.

 /** * Stateful filter. T is type of stream element. */ static class DistinctByKey { Map seen = new ConcurrentHashMap<>(); Function keyExtractor; public DistinctByKey(Function 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(o -> o.getCompany().getId())::filter) .map(Order::getShare) .reduce(BigDecimal.ZERO, BigDecimal::add); 

Questo semplifica un po ‘le cose, ma dovevo ancora specificare l’argomento type per il costruttore. Cercare di usare il diamante o un metodo statico non sembra migliorare le cose. Penso che la difficoltà è che il compilatore non può inferire parametri di tipo generico – per un costruttore o una chiamata al metodo statico – quando uno è nell’espressione di istanza di un riferimento al metodo. Oh bene.

(Un’altra variante su questo che probabilmente sarebbe semplificata è rendere DistinctByKey implements Predicate e rinominare il metodo in eval . Ciò eliminerebbe la necessità di utilizzare un riferimento al metodo e probabilmente migliorerebbe l’inferenza del tipo. per essere bello come la soluzione qui sotto.)

AGGIORNAMENTO 2

Non posso smettere di pensarci. Invece di una class helper, usa una funzione di ordine superiore. Possiamo usare i locali catturati per mantenere lo stato, quindi non abbiamo nemmeno bisogno di una class separata! Bonus, le cose sono semplificate quindi il tipo di inferenza funziona!

 public static  Predicate distinctByKey(Function keyExtractor) { Map 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); 

Devi più o meno fare qualcosa come

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

Una variazione sul secondo aggiornamento di Stuart Marks. Utilizzando un set.

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

Possiamo anche usare RxJava (libreria di estensione retriggers molto potente)

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

o

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

Per rispondere alla tua domanda nel secondo aggiornamento:

L’unica parte che sto cercando di capire è come ConcurrentHashMap mantiene solo un’istanza per l’intero stream:

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

Nell’esempio del codice, distinctByKey viene richiamato una sola volta, quindi ConcurrentHashMap è stato creato una sola volta. Ecco una spiegazione:

La funzione distinctByKey è solo una semplice funzione che restituisce un object e quell’object è un predicato. Tieni presente che un predicato è fondamentalmente un pezzo di codice che può essere valutato in seguito. Per valutare manualmente un predicato, è necessario chiamare un metodo nell’interfaccia di Predicato come test . Quindi, il predicato

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

è semplicemente una dichiarazione che non è effettivamente valutata all’interno di distinctByKey .

Il predicato viene trasmesso come qualsiasi altro object. Viene restituito e passato all’operazione di filter , che in pratica valuta il predicato ripetutamente su ogni elemento dello stream chiamando il test .

Sono sicuro che il filter è più complicato di quanto pensassi, ma il punto è che il predicato viene valutato molte volte al di fuori di distinctByKey . Non c’è niente di speciale * su distinctByKey ; è solo una funzione che hai chiamato una volta, quindi la ConcurrentHashMap viene creata una sola volta.

* Oltre ad essere ben fatto, @ stuart-marks 🙂

Puoi utilizzare il metodo distinct(HashingStrategy) in Eclipse Collections .

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

Se è ansible effettuare il refactoring list per implementare un’interfaccia Eclipse Collections, è ansible chiamare il metodo direttamente nell’elenco.

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

HashingStrategy è semplicemente un’interfaccia di strategia che ti consente di definire implementazioni personalizzate di equals e hashcode.

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

Nota: sono un committer per le raccolte di Eclipse.

Set.add(element) restituisce true se il set non contiene già un element , altrimenti false. Quindi puoi fare così.

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

Se si desidera fare questo parallelo, è necessario utilizzare la mappa simultanea.

Può essere fatto qualcosa di simile

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

Un altro modo per trovare elementi distinti

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