web-dev-qa-db-fra.com

Java Stream: recherche un élément avec une valeur d'attribut min/max

J'ai un flux d'objets et j'aimerais trouver celui qui a la valeur maximale d'un attribut qui est coûteux à calculer.

Comme exemple simple spécifique, disons que nous avons une liste de chaînes et que nous voulons trouver la plus cool, à partir d’une fonction coolnessIndex.

Ce qui suit devrait fonctionner:

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

Maintenant, il y a deux problèmes avec cela. Premièrement, en supposant que la variable coolnessIndex est coûteuse à calculer, cela ne sera probablement pas très efficace. Je suppose que la méthode max devra utiliser le comparateur à plusieurs reprises, qui à son tour appellera le coolnessIndex de manière répétée et à la fin, il sera appelé plus d'une fois pour chaque chaîne.

Deuxièmement, le fait de devoir fournir le comparateur entraîne une certaine redondance dans le code. Je préférerais de loin une syntaxe comme celle-ci:

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

Cependant, je n'ai pas été en mesure de trouver une méthode correspondante dans l'API Stream. Cela me surprend, car trouver min/max par un attribut semble être un motif commun. Je me demande s'il existe un meilleur moyen que d'utiliser le comparateur (autre qu'une boucle for).

19
Jan Pomikálek

Merci à tous pour vos suggestions. Enfin, j’ai trouvé la solution que j’aime le plus chez L’efficacité du comparateur fonctionne - la réponse de bayou.io:

Avoir une méthode générale 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<>());
}

Cela pourrait alors être utilisé comme suit:

String coolestString = stringList
        .stream()
        .max(Comparator.comparing(cache(CoolUtil::coolnessIndex)))
        .orElse(null);
2
Jan Pomikálek

Voici une variante utilisant un Object[] comme tuple, pas le code le plus joli mais concis

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);
8
gustf
Stream<String> stringStream = stringList.stream();
String coolest = stringStream.reduce((a,b)-> 
    coolnessIndex(a) > coolnessIndex(b) ? a:b;
).get()
8
frhack

Pourquoi ne pas utiliser deux flux, l’un pour créer une carte avec les valeurs précalculées et l’autre en utilisant le jeu d’entrées de la carte pour trouver la valeur maximale:

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

Je créerais une classe locale (une classe définie dans une méthode - rare, mais parfaitement légale), et mapperais vos objets sur celle-ci, ainsi l'attribut onéreux est calculé exactement une fois pour chaque:

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
VGR

C'est un problème de réduction. Réduire une liste à une valeur spécifique. En général, réduire les travaux en bas de la liste opérant sur une solution partielle et un élément de la liste. Dans ce cas, cela reviendrait à comparer la valeur «gagnante» précédente à la nouvelle valeur de la liste, ce qui calculera l'opération onéreuse deux fois à chaque comparaison.

Selon https://docs.Oracle.com/javase/tutorial/collections/streams/reduction.html une alternative consiste à utiliser collecter au lieu de réduire.

Une classe personnalisée consommateur permettra de garder la trace des opérations coûteuses en réduisant la liste. Le consommateur peut contourner les multiples appels au calcul coûteux en travaillant avec un état mutable.

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

Cette classe accepte une chaîne et si elle est plus froide que le précédent gagnant, elle la remplace et enregistre la valeur calculée coûteuse.

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

créez simplement vos paires (objet, métrique) en premier:

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

(Ce sont ceux de com.googlecode.totallylazy.Pair)

0
Paul Janssens

Vous pouvez utiliser l’idée de collecter les résultats du flux de manière appropriée. La contrainte de la fonction de calcul onéreuse de la fraîcheur vous incite à appeler cette fonction une seule fois pour chaque élément du flux.

Java 8 fournit la méthode collect sur la Stream et une variété de façons d'utiliser les collecteurs. Il semble que si vous avez utilisé la variable TreeMap pour collecter vos résultats, vous pouvez conserver l'expressivité tout en restant attentif à l'efficacité:

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

Ce code imprime la stooge avec un maximum de fraîcheur et la méthode coolness est appelée exactement une fois pour chaque stooge. La BinaryOperator qui fonctionne comme la mergeFunction ((a, b) ->a) peut être encore améliorée. 

0
Kedar Mhaswade