web-dev-qa-db-fra.com

Java Streams: Comment faire un "séparer et trier" efficace?

Supposons que j'ai un Stream<T> et que je souhaite obtenir uniquement des éléments distincts et triés.

L’approche naïve consisterait à faire ce qui suit:

Stream.of(...)
    .sorted()
    .distinct()

ou peut-être l'inverse:

Stream.of(...)
    .distinct()
    .sorted()

Étant donné que le code source du JDK ne permet pas vraiment de mettre en œuvre ces deux solutions, je m'interrogeais sur les possibilités de consommation de mémoire et de performances.

Ou serait-il encore plus efficace d'écrire mon propre filtre comme suit?

Stream.of(...)
    .sorted()
    .filter(noAdjacentDuplicatesFilter())

public static Predicate<Object> noAdjacentDuplicatesFilter() {
    final Object[] previousValue = {new Object()};

    return value -> {
        final boolean takeValue = !Objects.equals(previousValue[0], value);
        previousValue[0] = value;
        return takeValue;
    };
}
18

Lorsque vous chaînez une opération distinct() après sorted(), l'implémentation utilisera la nature triée des données et évite de créer une variable HashSet interne, qui peut être démontrée par le programme suivant.

public class DistinctAndSort {
    static int COMPARE, EQUALS, HASHCODE;
    static class Tracker implements Comparable<Tracker> {
        static int SERIAL;
        int id;
        Tracker() {
            id=SERIAL++/2;
        }
        public int compareTo(Tracker o) {
            COMPARE++;
            return Integer.compare(id, o.id);
        }
        public int hashCode() {
            HASHCODE++;
            return id;
        }
        public boolean equals(Object obj) {
            EQUALS++;
            return super.equals(obj);
        }
    }
    public static void main(String[] args) {
        System.out.println("adjacent sorted() and distinct()");
        Stream.generate(Tracker::new).limit(100)
              .sorted().distinct()
              .forEachOrdered(o -> {});
        System.out.printf("compareTo: %d, EQUALS: %d, HASHCODE: %d%n",
                          COMPARE, EQUALS, HASHCODE);
        COMPARE=EQUALS=HASHCODE=0;
        System.out.println("now with intermediate operation");
        Stream.generate(Tracker::new).limit(100)
            .sorted().map(x -> x).distinct()
            .forEachOrdered(o -> {});
        System.out.printf("compareTo: %d, EQUALS: %d, HASHCODE: %d%n",
                          COMPARE, EQUALS, HASHCODE);
    }
}

qui va imprimer

adjacent sorted() and distinct()
compareTo: 99, EQUALS: 99, HASHCODE: 0
now with intermediate operation
compareTo: 99, EQUALS: 100, HASHCODE: 200

L’opération intermédiaire, aussi simple que map(x -> x), ne peut pas être reconnue par l’implémentation Stream; elle doit donc supposer que les éléments risquent de ne pas être triés par rapport au résultat de la fonction de mappage.

Rien ne garantit que ce type d'optimisation se produise. Cependant, il est raisonnable de supposer que les développeurs de l'implémentation Stream ne supprimeront pas cette optimisation et tenteront même d'ajouter d'autres optimisations. optimisations futures.

De plus, ce que vous avez créé est un «prédicat stateful», qui est fortement déconseillé et qui, bien sûr, se cassera s’il est utilisé avec un flux parallèle.

Si vous ne faites pas confiance à l’API Stream pour effectuer cette opération de manière suffisamment efficace, vous feriez mieux de mettre en œuvre cette opération particulière sans l’API Stream.

18
Holger

Clause de non-responsabilité: Je sais que les tests de performance sont difficiles, en particulier sur la JVM, avec des réchauffements nécessaires et un environnement contrôlé sans autre processus en cours d'exécution.

Si je le teste, j'obtiens ces résultats. Il semble donc que votre implémentation bénéficie de l'exécution parallèle. (Fonctionne sur i7 avec 4 cœurs + hyperthreading).

Donc ".distinct().sorted()" semble être plus lent. Comme prévu/expliqué par Holger

Round 1 (Warm up?)
3938
2449
5747
Round 2
2834
2620
3984
Round 3 Parallel
831
4343
6346
Round 4 Parallel
825
3309
6339

Utiliser le code:

package test.test;

import Java.util.Collections;
import Java.util.List;
import Java.util.Objects;
import Java.util.function.Predicate;
import Java.util.stream.Collectors;
import Java.util.stream.IntStream;

public class SortDistinctTest {

    public static void main(String[] args) {
        IntStream range = IntStream.range(0, 6_000_000);
        List<Integer> collect = range.boxed().collect(Collectors.toList());
        Collections.shuffle(collect);

        long start = System.currentTimeMillis();

        System.out.println("Round 1 (Warm up?)");
        collect.stream().sorted().filter(noAdjacentDuplicatesFilter()).collect(Collectors.counting());
        long fst = System.currentTimeMillis();
        System.out.println(fst - start);

        collect.stream().sorted().distinct().collect(Collectors.counting());
        long snd = System.currentTimeMillis();
        System.out.println(snd - fst);

        collect.stream().distinct().sorted().collect(Collectors.counting());
        long end = System.currentTimeMillis();
        System.out.println(end - snd);

        System.out.println("Round 2");
        collect.stream().sorted().filter(noAdjacentDuplicatesFilter()).collect(Collectors.counting());
        fst = System.currentTimeMillis();
        System.out.println(fst - end);

        collect.stream().sorted().distinct().collect(Collectors.counting());
        snd = System.currentTimeMillis();
        System.out.println(snd - fst);

        collect.stream().distinct().sorted().collect(Collectors.counting());
        end = System.currentTimeMillis();
        System.out.println(end - snd);

        System.out.println("Round 3 Parallel");
        collect.stream().parallel().sorted().filter(noAdjacentDuplicatesFilter()).collect(Collectors.counting());
        fst = System.currentTimeMillis();
        System.out.println(fst - end);

        collect.stream().parallel().sorted().distinct().collect(Collectors.counting());
        snd = System.currentTimeMillis();
        System.out.println(snd - fst);

        collect.stream().parallel().distinct().sorted().collect(Collectors.counting());
        end = System.currentTimeMillis();
        System.out.println(end - snd);

        System.out.println("Round 4 Parallel");
        collect.stream().parallel().sorted().filter(noAdjacentDuplicatesFilter()).collect(Collectors.counting());
        fst = System.currentTimeMillis();
        System.out.println(fst - end);

        collect.stream().parallel().sorted().distinct().collect(Collectors.counting());
        snd = System.currentTimeMillis();
        System.out.println(snd - fst);

        collect.stream().parallel().distinct().sorted().collect(Collectors.counting());
        end = System.currentTimeMillis();
        System.out.println(end - snd);

    }

    public static Predicate<Object> noAdjacentDuplicatesFilter() {
        final Object[] previousValue = { new Object() };

        return value -> {
            final boolean takeValue = !Objects.equals(previousValue[0], value);
            previousValue[0] = value;
            return takeValue;
        };

    }

}
0
Viktor Mellgren