2016-04-21 11 views
14

Mam strumień obiektów i chciałbym znaleźć ten z maksymalną wartością jakiegoś atrybutu, który jest drogi do wyliczenia.Strumień Java: znajdź element o wartości min/max atrybutu

Jako konkretny prosty przykład, powiedzmy, że mamy listę ciągów znaków i chcemy znaleźć najfajniejszą, biorąc pod uwagę funkcję coolnessIndex.

Poniższa powinno działać:

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

Teraz istnieją dwa problemy z tym. Po pierwsze, zakładając, że coolnessIndex jest kosztowny do obliczenia, prawdopodobnie nie będzie to bardzo efektywne. Przypuszczam, że metoda max będzie musiała wielokrotnie używać komparatora, który z kolei będzie wielokrotnie wywoływał numer coolnessIndex, a na końcu będzie wywoływany więcej niż raz dla każdego ciągu.

Po drugie, konieczność dostarczenia komparatora prowadzi do pewnej nadmiarowości w kodzie. Chciałbym zdecydowanie wolą składnię tak:

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

Jednak nie udało się znaleźć metodę dopasowania w Stream API. To mnie zaskakuje, ponieważ znalezienie min/maksimum przez atrybut wydaje się być wspólnym wzorcem. Zastanawiam się, czy istnieje lepszy sposób niż użycie komparatora (innego niż pętla for).

+2

Podobne, ale nie całkiem powielać: http://stackoverflow.com/questions/27606185/arg-max-in-java-8-streams (gdzie problemem jest zwięzłość kodu, a nie efektywność, i myślę, że zalecane rozwiązanie wciąż kończy się wywoływaniem odpowiednika "coolnessIndex" wielokrotnie). –

+0

Nie wierzę, że jest coś podobnego w interfejsie API strumieni Java. Możesz zaimplementować własną wersję 'maxByAttribute' (ktoś inny zrobił coś w tym rodzaju [tutaj] (https://gist.github.com/mapio/57299694ef94cc88dddb), ale nie sprawdzałem ich kodu) lub może użyć 'map', aby uzyskać strumień par (' s', 'coolnessIndex (s)'), a następnie 'max', ale Java AIUI nie ma przydatnej klasy pair, więc skończyłoby się na dużo kodu standardowego, nie mówiąc już o wszystkich dodatkowych przydziałach pamięci. –

+0

Możesz grupować według ciągu znaków-> chłód na mapie, a następnie wybrać najfajniejszy ciąg. Zobacz https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function- –

Odpowiedz

1

Dzięki wszystkim za sugestie. W końcu znalazłem rozwiązanie podoba mi się najbardziej w Efficiency of the way comparator works - odpowiedź z bayou.io:

mają ogólny cel cache metody:

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

To może być następnie wykorzystane w następujący sposób:

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

Jak na temat korzystania dwa strumienie, jeden stworzyć mapę ze wstępnie obliczone wartości i drugi za pomocą wejścia mapie ustaw znaleźć wartość maksymalna:

 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

Lub możesz użyć 'TreeMap' do zbierania wpisów w sposób posortowany według indeksu chłodu - to pomoże ci pozbyć się kolejnego strumienia. –

0

Jest to problem redukcji. Zmniejszenie listy do określonej wartości. Zasadniczo zmniejszanie działa na liście działającej na częściowym rozwiązaniu i pozycji na liście. W tym przypadku oznaczałoby to porównanie poprzedniej "wygrywającej" wartości z nową wartością z listy, która dwukrotnie obliczy kosztowną operację przy każdym porównaniu.

Zgodnie z https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html alternatywą jest użycie funkcji zbierania zamiast redukcji.

Niestandardowa klasa consumer umożliwia śledzenie drogich operacji, ponieważ zmniejsza listę. Konsument może ominąć wiele wywołań kosztownych obliczeń, pracując z zmiennym stanem.

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

Ta klasa przyjmuje ciąg i jeśli jest chłodniejszy od poprzedniego zwycięzcy, zastępuje go i zapisuje kosztowną obliczoną wartość.

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

Chciałbym utworzyć lokalną klasę (klasy zdefiniowanej wewnątrz metody-rzadkie, ale całkowicie legalne) i mapować obiekty, które, tak drogie atrybut obliczany jest dokładnie jeden raz dla każdego:

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

Możesz użyć idei zbierania wyników ze strumienia odpowiednio. Ograniczenie kosztownej funkcji obliczania chłodu powoduje, że rozważasz wywołanie tej funkcji dokładnie raz dla każdego elementu strumienia.

Java 8 udostępnia collect metodę na Stream i na wiele sposobów, w których można używać kolektorów.Wydaje się, że jeśli użyto TreeMap zbierać swoje wyniki, można zachować wyrazistość i jednocześnie pozostać taktowny wydajność:

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

Ten kod drukuje stooge maksimum chłodu i sposób coolness dokładnie nazywa raz dla każdego stooge. Model BinaryOperator, który działa jako mergeFunction ((a, b) ->a), można dodatkowo ulepszyć.

7

Oto wariant z wykorzystaniem Object[] jako krotki, nie najładniejszy kod, ale zwięzły

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

prostu utworzyć (obiekt, metryczne) pary pierwszy:

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

(są com.googlecode.totallylazy.Pair'S)

Powiązane problemy