2015-12-13 7 views
7

Ho una situazione in cui sono presenti oggetti Player in un progetto di sviluppo e l'attività sta semplicemente misurando la distanza e restituendo risultati che rientrano in una determinata soglia. Certo, voglio utilizzare gli stream nel modo più conciso possibile.Filtraggio di uno stream in base ai valori in una raccolta toMap

Attualmente, ho una soluzione che mappa il torrente, e poi filtra attraverso un iteratore:

Stream<Player> str = /* source of my player stream I'm filtering */; 
Map<Player, Double> dists = str.collect(Collectors.toMap(...)); //mapping function 
Iterator<Map.Entry<Player, Double>> itr = map.entrySet().iterator(); 
while (itr.hasNext()) { 
    if (itr.next().getValue() <= radiusSquared) { 
     itr.remove(); 
    } 
} 

Tuttavia, quello che vorrei realizzare è qualcosa che svolge questo filtraggio mentre si utilizza il flusso su, qualcosa che dice "se questo predicato fallisce, non raccogliere", per tentare e salvare la seconda iterazione. Inoltre, non voglio calcolare le distanze due volte, quindi fare un filtro tramite la funzione di mappatura, e quindi re-mapping non è una soluzione plausibile.

L'unica soluzione reale possibile che ho pensato è mappare a un Pair<A, B>, ma se c'è il supporto nativo per qualche forma di flusso binario, sarebbe meglio.

Esiste un supporto nativo per questo nell'API di flusso di java?

+2

mostrare la vostra funzione di mappatura così – AdamSkywalker

+0

@AdamSkywalker Non sono sicuro di come sia rilevante, ci vogliono gli oggetti 'Player' dal flusso e gli usi un metodo irrilevante per ottenere la distanza. Il modo in cui ciò viene fatto non è realmente rilevante per la domanda attuale (filtraggio basato sul valore restituito). lo pseudo-codice saggio è fondamentalmente lo stesso di 'streamPlayer.distance (origine)' dove 'origin' è al di fuori del flusso – Rogue

+3

Che dire dell'utilizzo di [' Stream # filter'] (https://docs.oracle.com/javase/ 8/docs/api/java/util/stream/Stream.html # filtro java.util.function.Predicate-)? –

risposta

4

Il filtraggio di uno Map in seguito non è così male come sembra, tenere presente che l'iterazione su uno Map non implica lo stesso costo di esecuzione di una ricerca (ad esempio hashing).

Ma invece di

Iterator<Map.Entry<Player, Double>> itr = map.entrySet().iterator(); 
while (itr.hasNext()) { 
    if (itr.next().getValue() <= radiusSquared) { 
     itr.remove(); 
    } 
} 

si può semplicemente utilizzare

map.values().removeIf(value -> value <= radiusSquared); 

Anche se ti ostini a averlo come parte dell'operazione collect, si può fare come operazione di postfix:

Map<Player, Double> dists = str.collect(
    Collectors.collectingAndThen(Collectors.toMap(p->p, p->calculate(p)), 
    map -> { map.values().removeIf(value -> value <= radiusSquared); return map; })); 

Evitare di inserire put voci indesiderate in primo luogo possibile, ma implica ripercorrendo manualmente ciò che l'attuale toMap collettore fa:

Map<Player, Double> dists = str.collect(
    HashMap::new, 
    (m, p) -> { double value=calculate(p); if(value > radiusSquared) m.put(p, value); }, 
    Map::putAll); 
+0

Ho dimenticato che #values ​​era una vista e non una copia, molto più pulita in termini di codice :) – Rogue

0

Se il vostro obiettivo è solo quello di avere un'iterazione e calcolare distanze solo una volta, allora si potrebbe fare questo:

Stream<Player> str = /* source of my player stream I'm filtering */; 
Map<Player, Double> dists = new HashMap<>(); 
str.forEach(p -> { 
    double distance = /* calculate distance */; 
    if (distance <= radiusSquared) { 
     dists.put(p, distance); 
    } 
}); 

No collettore più, ma è così importante?

+0

Non importante, no. Questo è anche fattibile, essenzialmente capovolge più oggetti 'Pair' con più operazioni' put'. – Rogue

+0

Ma ci sono comunque molte operazioni 'put', giusto? Anche se sono nascosti all'interno di un collezionista. –

+0

Ho appena guardato le fonti 'Collector' e' Map' e sembra ancora peggio. Collector non solo fa più operazioni 'put', ma controlla anche se c'è un valore per una chiave, e se c'è, lancia un'eccezione. Potrebbe essere una buona cosa, ma se sei sicuro che non ci sono giocatori duplicati, fare più mosse manualmente è più efficiente. –

2

Nota che il vostro vecchio stile iteratore-loop potrebbe essere riscritto in Java-8 utilizzando Collection.removeIf:

map.values().removeIf(dist -> dist <= radiusSquared); 

Quindi in realtà non così male. Non dimenticare che keySet() e values() sono modificabili.

Se si desidera risolvere questo utilizzando una singola pipeline (ad esempio, la maggior parte delle voci devono essere rimosse), quindi cattive notizie per voi. Sembra che l'attuale Stream API non ti permetta di farlo senza l'uso esplicito della classe con semantica della coppia. E 'abbastanza naturale per creare un'istanza Map.Entry, anche se l'opzione già esistente è AbstractMap.SimpleEntry che ha piuttosto lungo e il nome sgradevole:

str.map(player -> new AbstractMap.SimpleEntry(player, getDistance(player))) 
    .filter(entry -> entry.getValue() > radiusSquared) 
    .toMap(Entry::getKey, Entry::getValue); 

Si noti che è probabile che in Java-9 ci sarà Map.entry() metodo statico, quindi si può usare Map.entry(player, getDistance(player)). Vedi JEP-269 per i dettagli.

Come al solito la mia libreria StreamEx ha qualche zucchero sintattico per risolvere questo problema in modo più pulito:

StreamEx.of(str).mapToEntry(player -> getDistance(player)) 
       .filterValues(dist -> dist > radiusSquared) 
       .toMap(); 

E per quanto riguarda i commenti che: sì, toMap() collettore utilizza uno per un inserto, ma non preoccupatevi : gli inserti di massa per mappare raramente migliorano la velocità. Non è nemmeno possibile pre-dimensionare la tabella hash (se la mappa è basata su hash) poiché non si sa molto sugli elementi da inserire. Probabilmente vuoi inserire un milione di oggetti con la stessa chiave: allocare la tabella hash per milioni di voci solo per scoprire che avrai una sola voce dopo l'inserimento sarebbe troppo dispendioso.

Problemi correlati