web-dev-qa-db-fra.com

Comment forcer max à renvoyer TOUTES les valeurs maximales dans un Java Stream?

J'ai testé un peu la fonction max sur Java 8 lambdas et streams, et il semble que dans le cas où max est exécuté, même si plusieurs comparé à 0, il renvoie un élément arbitraire parmi les candidats liés sans autre considération.

Existe-t-il une astuce ou une fonction évidente pour un tel comportement maximal attendu, de sorte que toutes les valeurs maximales soient retournées? Je ne vois rien dans l'API mais je suis sûr qu'il doit exister quelque chose de mieux que de comparer manuellement.

Par exemple:

// myComparator is an IntegerComparator
Stream.of(1, 3, 5, 3, 2, 3, 5)
    .max(myComparator)
    .forEach(System.out::println);
// Would print 5, 5 in any order.
27
Whimusical

Je crois que l'OP utilise un comparateur pour partitionner l'entrée en classes d'équivalence, et le résultat souhaité est une liste de membres de la classe d'équivalence qui est le maximum selon ce comparateur.

Malheureusement, l'utilisation des valeurs de int comme exemple de problème est un terrible exemple. Toutes les valeurs égales de int sont fongibles, il n'y a donc aucune notion de préservation de l'ordre des valeurs équivalentes. Un meilleur exemple est peut-être d'utiliser des longueurs de chaîne, où le résultat souhaité est de renvoyer une liste de chaînes à partir d'une entrée qui ont toutes la plus longue longueur dans cette entrée.

Je ne connais aucun moyen de le faire sans stocker au moins des résultats partiels dans une collection.

Étant donné une collection d'entrées, disons

List<String> list = ... ;

il est assez simple de le faire en deux passes, la première pour obtenir la longueur la plus longue et la seconde pour filtrer les chaînes qui ont cette longueur:

int longest = list.stream()
                  .mapToInt(String::length)
                  .max()
                  .orElse(-1);

List<String> result = list.stream()
                          .filter(s -> s.length() == longest)
                          .collect(toList());

Si l'entrée est un flux qui ne peut pas être traversé plus d'une fois , il est possible de calculer le résultat en une seule passe en utilisant un collecteur. Écrire un tel collecteur n'est pas difficile, mais c'est un peu fastidieux car il y a plusieurs cas à traiter. Une fonction d'assistance qui génère un tel collecteur, étant donné un comparateur, est la suivante:

static <T> Collector<T,?,List<T>> maxList(Comparator<? super T> comp) {
    return Collector.of(
        ArrayList::new,
        (list, t) -> {
            int c;
            if (list.isEmpty() || (c = comp.compare(t, list.get(0))) == 0) {
                list.add(t);
            } else if (c > 0) {
                list.clear();
                list.add(t);
            }
        },
        (list1, list2) -> {
            if (list1.isEmpty()) {
                return list2;
            } 
            if (list2.isEmpty()) {
                return list1;
            }
            int r = comp.compare(list1.get(0), list2.get(0));
            if (r < 0) {
                return list2;
            } else if (r > 0) {
                return list1;
            } else {
                list1.addAll(list2);
                return list1;
            }
        });
}

Cela stocke les résultats intermédiaires dans un ArrayList. L'invariant est que tous les éléments d'une telle liste sont équivalents en termes de comparateur. Lors de l'ajout d'un élément, s'il est inférieur aux éléments de la liste, il est ignoré; si elle est égale, elle est ajoutée; et s'il est supérieur, la liste est vidée et le nouvel élément est ajouté. La fusion n'est pas trop difficile non plus: la liste avec les éléments les plus grands est retournée, mais si leurs éléments sont égaux, les listes sont ajoutées.

Étant donné un flux d'entrée, c'est assez facile à utiliser:

Stream<String> input = ... ;

List<String> result = input.collect(maxList(comparing(String::length)));
33
Stuart Marks

J'ai implémenté une solution de collecteur plus générique avec un collecteur en aval personnalisé. Certains lecteurs pourraient probablement le trouver utile:

public static <T, A, D> Collector<T, ?, D> maxAll(Comparator<? super T> comparator, 
                                                  Collector<? super T, A, D> downstream) {
    Supplier<A> downstreamSupplier = downstream.supplier();
    BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
    BinaryOperator<A> downstreamCombiner = downstream.combiner();
    class Container {
        A acc;
        T obj;
        boolean hasAny;

        Container(A acc) {
            this.acc = acc;
        }
    }
    Supplier<Container> supplier = () -> new Container(downstreamSupplier.get());
    BiConsumer<Container, T> accumulator = (acc, t) -> {
        if(!acc.hasAny) {
            downstreamAccumulator.accept(acc.acc, t);
            acc.obj = t;
            acc.hasAny = true;
        } else {
            int cmp = comparator.compare(t, acc.obj);
            if (cmp > 0) {
                acc.acc = downstreamSupplier.get();
                acc.obj = t;
            }
            if (cmp >= 0)
                downstreamAccumulator.accept(acc.acc, t);
        }
    };
    BinaryOperator<Container> combiner = (acc1, acc2) -> {
        if (!acc2.hasAny) {
            return acc1;
        }
        if (!acc1.hasAny) {
            return acc2;
        }
        int cmp = comparator.compare(acc1.obj, acc2.obj);
        if (cmp > 0) {
            return acc1;
        }
        if (cmp < 0) {
            return acc2;
        }
        acc1.acc = downstreamCombiner.apply(acc1.acc, acc2.acc);
        return acc1;
    };
    Function<Container, D> finisher = acc -> downstream.finisher().apply(acc.acc);
    return Collector.of(supplier, accumulator, combiner, finisher);
}

Ainsi, par défaut, il peut être collecté pour répertorier:

public static <T> Collector<T, ?, List<T>> maxAll(Comparator<? super T> comparator) {
    return maxAll(comparator, Collectors.toList());
}

Mais vous pouvez également utiliser d'autres collecteurs en aval:

public static String joinLongestStrings(Collection<String> input) {
    return input.stream().collect(
            maxAll(Comparator.comparingInt(String::length), Collectors.joining(","))));
}
8
Tagir Valeev

Je grouperais par valeur et stockerais les valeurs dans un TreeMap afin d'avoir mes valeurs triées, alors j'obtiendrais la valeur maximum en obtenant la dernière entrée comme suivante:

Stream.of(1, 3, 5, 3, 2, 3, 5)
    .collect(groupingBy(Function.identity(), TreeMap::new, toList()))
    .lastEntry()
    .getValue()
    .forEach(System.out::println);

Sortie:

5
5
4
Nicolas Filotto

Si j'ai bien compris, vous voulez la fréquence de la valeur max dans le Stream.

Une façon d'y parvenir serait de stocker les résultats dans un TreeMap<Integer, List<Integer> Lorsque vous collectez des éléments du flux. Ensuite, vous saisissez la dernière clé (ou la première selon le comparateur que vous donnez) pour obtenir la valeur qui contiendra la liste des valeurs maximales.

List<Integer> maxValues = st.collect(toMap(i -> i,
                     Arrays::asList,
                     (l1, l2) -> Stream.concat(l1.stream(), l2.stream()).collect(toList()),
                     TreeMap::new))
             .lastEntry()
             .getValue();

Le collecter à partir de la Stream(4, 5, -2, 5, 5) vous donnera un List [5, 5, 5].

Une autre approche dans le même esprit serait d'utiliser un groupe par opération combiné avec le collecteur counting():

Entry<Integer, Long> maxValues = st.collect(groupingBy(i -> i,
                TreeMap::new,
                counting())).lastEntry(); //5=3 -> 5 appears 3 times

Fondamentalement, vous obtenez d'abord un Map<Integer, List<Integer>>. Ensuite, le collecteur counting() en aval renverra le nombre d'éléments dans chaque liste mappé par sa clé, ce qui donnera une carte. De là, vous saisissez l'entrée maximale.

Les premières approches nécessitent de stocker tous les éléments du flux. Le second est meilleur (voir le commentaire de Holger) car le List intermédiaire n'est pas construit. Dans les deux approches, le résultat est calculé en une seule passe.

Si vous obtenez la source d'une collection, vous souhaiterez peut-être utiliser Collections.max Une fois pour trouver la valeur maximale suivie de Collections.frequency Pour trouver combien de fois cette valeur apparaît.

Il nécessite deux passes mais utilise moins de mémoire car vous n'avez pas à construire la structure de données.

L'équivalent de flux serait coll.stream().max(...).get(...) suivi de coll.stream().filter(...).count().

3
Alexis C.

Je ne sais pas vraiment si vous essayez de

  • (a) trouver le nombre d'occurrences de l'élément maximum, ou
  • (b) Trouver toutes les valeurs maximales dans le cas d'un Comparator qui n'est pas cohérent avec equals.

Un exemple de (a) serait [1, 5, 4, 5, 1, 1] -> [5, 5].

Un exemple de (b) serait:

Stream.of("Bar", "FOO", "foo", "BAR", "Foo")
      .max((s, t) -> s.toLowerCase().compareTo(t.toLowerCase()));

que vous voulez donner [Foo, foo, Foo], plutôt que simplement FOO ou Optional[FOO].

Dans les deux cas, il existe des moyens intelligents de le faire en un seul passage. Mais ces approches sont d'une valeur douteuse car vous devrez suivre les informations inutiles en cours de route. Par exemple, si vous commencez par [2, 0, 2, 2, 1, 6, 2], ce ne sera que lorsque vous atteindrez 6 que vous vous rendriez compte qu'il n'était pas nécessaire de suivre tous les 2s.

Je pense que la meilleure approche est la plus évidente; utilisez max, puis itérez à nouveau les éléments en mettant tous les liens dans une collection de votre choix. Cela fonctionnera pour (a) et (b).

2
Paul Boddington