2016-04-21 27 views
14

Ho un flusso di oggetti e vorrei trovare quello con il valore massimo di qualche attributo che è costoso da calcolare.Java Stream: trova un elemento con un valore min/max di un attributo

Come esempio semplice specifico, diciamo che abbiamo una lista di stringhe e vogliamo trovare quella più bella, data una funzione coolnessIndex.

Il seguente dovrebbe funzionare:

String coolestString = stringList 
     .stream() 
     .max((s1, s2) -> Integer.compare(coolnessIndex(s1), coolnessIndex(s2))) 
     .orElse(null); 

Ora, ci sono due problemi con questo. Innanzitutto, supponendo che lo coolnessIndex sia costoso da calcolare, probabilmente non sarà molto efficiente. Suppongo che il metodo max dovrà utilizzare ripetutamente il comparatore, che a sua volta chiamerà ripetutamente lo coolnessIndex e alla fine verrà chiamato più di una volta per ogni stringa.

In secondo luogo, dovendo fornire ai cavi del comparatore una certa ridondanza nel codice. Io preferirei di gran lunga la sintassi simile a questo:

String coolestString = stringList 
     .stream() 
     .maxByAttribute(s -> coolnessIndex(s)) 
     .orElse(null); 

Tuttavia, non sono stato in grado di trovare un metodo di corrispondenza nel Stream API. Questo mi sorprende, dal momento che trovare min/max da un attributo sembra uno schema comune. Mi chiedo se c'è un modo migliore di usare il comparatore (diverso da un ciclo for).

+2

correlati ma non del tutto duplicare: http://stackoverflow.com/questions/27606185/arg-max-in-java-8-streams (dove la la preoccupazione è la brevità del codice piuttosto che l'efficienza, e penso che la soluzione raccomandata finisca per chiamare ripetutamente l'equivalente di 'coolnessIndex'). –

+0

Non credo che ci sia qualcosa di equivalente a questo nell'API di Java Streaming. Potresti implementare la tua versione di 'maxByAttribute' (qualcun altro ha fatto qualcosa del genere [qui] (https://gist.github.com/mapio/57299694ef94cc88dddb) ma non ho guardato il loro codice), o tu potrebbe usare 'map' per ottenere un flusso di coppie (' s', 'coolnessIndex (s)') e poi 'max' quelli - ma AIUI Java non ha una classe di coppia a portata di mano per cui si finisce con molto codice boilerplate, per non parlare di tutte le allocazioni extra di memoria. –

+0

È possibile raggruppare per stringa-> risultati di coolness in una mappa e quindi scegliere la stringa più bella. Vedi https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function- –

risposta

1

Grazie a tutti per i suggerimenti. Finalmente ho trovato la soluzione mi piace di più al Efficiency of the way comparator works - la risposta da bayou.io:

Avere un generale metodo di scopo cache:

public static <K,V> Function<K,V> cache(Function<K,V> f, Map<K,V> cache) 
{ 
    return k -> cache.computeIfAbsent(k, f); 
} 

public static <K,V> Function<K,V> cache(Function<K,V> f) 
{ 
    return cache(f, new IdentityHashMap<>()); 
} 

Questo potrebbe quindi essere utilizzato come segue:

String coolestString = stringList 
     .stream() 
     .max(Comparator.comparing(cache(CoolUtil::coolnessIndex))) 
     .orElse(null); 
1

ne dite di usare due flussi, uno per creare una mappa con i valori precalcolati e un secondo con l'ingresso della mappa impostato per trovare il valore massimo:

 String coolestString = stringList 
      .stream() 
      .collect(Collectors.toMap(Function.identity(), Test::coolnessIndex)) 
      .entrySet() 
      .stream() 
      .max((s1, s2) -> Integer.compare(s1.getValue(), s2.getValue())) 
      .orElse(null) 
      .getKey(); 
+0

Oppure puoi usare 'TreeMap' per raccogliere le voci in un modo ordinato per indice di freschezza - questo ti aiuta a sbarazzarti di un altro stream. –

0

Questo è un problema di riduzione. Ridurre un elenco fino a un valore specifico. In generale, ridurre i lavori in basso nell'elenco operando su una soluzione parziale e un elemento nell'elenco. In questo caso, ciò significherebbe confrontare il precedente valore "vincente" con il nuovo valore dall'elenco che calcolerà l'operazione costosa due volte su ciascun confronto.

Secondo https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html un'alternativa è quella di utilizzare raccoglie anziché ridurre.

Una classe personalizzata consumer consentirà di tenere traccia delle costose operazioni in quanto riduce la lista. Il consumatore può aggirare le chiamate multiple al calcolo costoso lavorando con lo stato mutabile.

class Cooler implements Consumer<String>{ 

    String coolestString = ""; 
    int coolestValue = 0; 

    public String coolest(){ 
     return coolestString; 
    } 
    @Override 
    public void accept(String arg0) { 
     combine(arg0, expensive(arg0)); 
    } 

    private void combine (String other, int exp){ 
     if (coolestValue < exp){ 
      coolestString = other; 
      coolestValue = exp; 
     } 
    } 
    public void combine(Cooler other){ 
     combine(other.coolestString, other.coolestValue); 
    } 
} 

Questa classe accetta una stringa e se è più fresco rispetto al precedente vincitore, lo sostituisce e salva il valore calcolato costoso.

Cooler cooler = Stream.of("java", "php", "clojure", "c", "lisp") 
       .collect(Cooler::new, Cooler::accept, Cooler::combine); 
System.out.println(cooler.coolest()); 
0

vorrei creare una classe locale (una classe definita all'interno di un metodo-rara, ma perfettamente legale), e mappare gli oggetti a questo, così l'attributo costoso viene calcolata esattamente una volta per ogni:

class IndexedString { 
    final String string; 
    final int index; 

    IndexedString(String s) { 
     this.string = Objects.requireNonNull(s); 
     this.index = coolnessIndex(s); 
    } 

    String getString() { 
     return string; 
    } 

    int getIndex() { 
     return index; 
    } 
} 

String coolestString = stringList 
    .stream() 
    .map(IndexedString::new) 
    .max(Comparator.comparingInt(IndexedString::getIndex)) 
    .map(IndexedString::getString) 
    .orElse(null); 
0

È possibile utilizzare l'idea di raccogliendo i risultati dal flusso in modo appropriato. Il vincolo della costosa funzione di calcolo della freddezza ti fa considerare di chiamare quella funzione esattamente una volta per ogni elemento del flusso.

Java 8 fornisce il metodo collect su Stream e una varietà di modi in cui è possibile utilizzare i raccoglitori.Sembra che se si è utilizzato il TreeMap per raccogliere i risultati, è possibile mantenere l'espressività e allo stesso tempo rimanere premuroso di efficienza:

public class Expensive { 
    static final Random r = new Random(); 
    public static void main(String[] args) { 
     Map.Entry<Integer, String> e = 
     Stream.of("larry", "moe", "curly", "iggy") 
       .collect(Collectors.toMap(Expensive::coolness, 
              Function.identity(), 
              (a, b) -> a, 
             () -> new TreeMap<> 
              ((x, y) -> Integer.compare(y, x)) 
         )) 
       .firstEntry(); 
     System.out.println("coolest stooge name: " + e.getKey() + ", coolness: " + e.getValue()); 
    } 

    public static int coolness(String s) { 
     // simulation of a call that takes time. 
     int x = r.nextInt(100); 
     System.out.println(x); 
     return x; 
    } 
} 

Questo codice stampa il stooge con la massima freddezza e il metodo coolness viene chiamato esattamente una volta per ogni stooge. Lo BinaryOperator che funziona come mergeFunction ((a, b) ->a) può essere ulteriormente migliorato.

7

Ecco una variante con un Object[] come una tupla, non il codice più bella, ma conciso

String coolestString = stringList 
     .stream() 
     .map(s -> new Object[] {s, coolnessIndex(s)}) 
     .max(Comparator.comparingInt(a -> (int)a[1])) 
     .map(a -> (String)a[0]) 
     .orElse(null); 
5
Stream<String> stringStream = stringList.stream(); 
String coolest = stringStream.reduce((a,b)-> 
    coolnessIndex(a) > coolnessIndex(b) ? a:b; 
).get() 
0

basta creare il vostro (oggetto, metriche) coppie di primi:

public static <T> Optional<T> maximizeOver(List<T> ts, Function<T,Integer> f) { 
    return ts.stream().map(t -> Pair.pair(t, f.apply(t))) 
     .max((p1,p2) -> Integer.compare(p1.second(), p2.second())) 
     .map(Pair::first); 
} 

(quelli sono di com.googlecode.totallylazy.Pair)