9
votes

Comment obtenir plusieurs valeurs d'un objet à l'aide d'une seule opération de flux?

Je souhaite déterminer la surface minimale requise pour afficher une collection de points. Le moyen le plus simple est de parcourir la collection comme ceci:

int minX = points.stream().mapToInt(point -> point.x).min().orElse(-1);
int maxX = points.stream().mapToInt(point -> point.x).max().orElse(-1);
int minY = points.stream().mapToInt(point -> point.y).min().orElse(-1);
int maxY = points.stream().mapToInt(point -> point.y).max().orElse(-1);

J'apprends à connaître les flux. Pour faire de même, vous pouvez faire ce qui suit:

int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minY = Integer.MAX_VALUE;
int maxY = Integer.MIN_VALUE;
for (Point point: points) {
    if (point.x < minX) {
        minX = point.x;
    }
    if (point.x > maxX) {
        maxX = point.x;
    }
    if (point.y < minY) {
        minY = point.y;
    }
    if (point.y > maxY) {
        maxY = point.y;
    }
}

Les deux donnent le même résultat. Cependant, bien que l'approche des flux soit élégante, elle est beaucoup plus lente (comme prévu).

Existe-t-il un moyen d'obtenir minX , maxX , minY et maxY en une seule opération de flux?


2 commentaires

Créez un Collector avec un type personnalisé qui décrit toutes les positions, pas beaucoup mieux que votre approche itérative imo (extraite dans sa propre méthode par exemple).


@AkinerAlkan: maxX est en fait défini sur Integer.MIN_VALUE . Cela signifie que la condition if (point.x> maxX) deviendra définitivement true (sauf si toutes les valeurs x sont égales à Integer.MIN_VALUE , ce qui est hautement improbable).


5 Réponses :


1
votes

Vous pouvez utiliser 2 flux en utilisant Stream :: reduction pour obtenir un point avec un minimum et un point avec un maximum. Je ne recommande pas de concaténer les résultats en un seul flux car il peut être difficile de distinguer la différence entre le minimum, le maximum et les coordonnées.

Point min = points
    .stream()
    .reduce((l, r) -> new Point(Math.min(l.y, r.y), Math.min(l.y, r.y))
    .orElse(new Point(-1, -1));

Point max = points
    .stream()
    .reduce((l, r) -> new Point(Math.max(l.y, r.y), Math.max(l.y, r.y))
    .orElse(new Point(-1, -1));

En tant que BinaryOperator utilisez les deux Points suivants et un opérateur ternaire pour connaître le minimum / maximum qui est passé à un nouvel objet Point et renvoyé en utilisant Facultatif :: orElse avec les coordonnées par défaut -1, -1 .


0 commentaires

9
votes

Vous pouvez diviser par deux les itérations avec summaryStatistics () tout en gardant un code simple:

for (Point p : points) {
    minX = Math.min(p.x, minX);
    minY = Math.min(p.y, minY);
    maxY = Math.max(p.x, maxX);
    maxY = Math.max(p.y, maxY);
}

Et faire la même chose avec point.y .
Vous pouvez prendre en compte de cette manière:

Function<ToIntFunction<Point>, IntSummaryStatistics> statFunction =
        intExtractor -> points.stream()
                              .mapToInt(p -> intExtractor.applyAsInt(pp))
                              .summaryStatistics();

IntSummaryStatistics statX = statFunction.apply(p -> p.x);
IntSummaryStatistics statY = statFunction.apply(p -> p.y);

Un collecteur personnalisé est une possibilité, mais notez que vous devez implémenter la partie combineur qui rendra votre code plus difficile à lire.
Donc, mais si vous avez besoin d'utiliser un flux parallèle, vous devez rester à la manière impérative.
Bien que vous puissiez améliorer votre code actuel en vous appuyant sur les fonctions Math.min et Math.max :

IntSummaryStatistics stat = points.stream().mapToInt(point -> point.x).summaryStatistics();
int minX = stat.getMin();
int maxX = stat.getMax();


4 commentaires

Alternativement, au lieu de créer un objet Function , vous pouvez simplement extraire une méthode: public static IntSummaryStatistics getSummaryStatistics (Collection points, ToIntFunction getter) {return points.stream (). mapToInt (getter) .summaryStatistics (); }


@Ricola Bien sûr. C'est une possibilité parfois très fine. Mais dans certains autres cas, il arrive que de nombreuses méthodes de votre classe reposent sur l'extraction de fonctions. Définir une méthode (même privée) pour chacun pourrait rendre la lecture du code plus difficile en démultipliant le nombre de méthodes.


Je comprends, mais je dirais que si l'extraction de méthodes dans votre classe rend votre classe trop grande ou difficile à comprendre, le problème est peut-être que votre classe en fait trop et devrait être divisée en composants plus petits.


@Ricola C'est peut-être la raison. Mais parfois, ce n'est pas le problème. Supposons que vous ayez "juste" 5 méthodes publiques qui nécessitent une fonction paramétrable spécifique. En ajoutant 5 méthodes privées qui remplacent les fonctions de variables locales, vous augmentez leur portée. En conséquence, il crée une indirection de lecture dans le code, mais notez qu'il peut également créer des effets secondaires tels que l'invocation de la «mauvaise» méthode. C'est pourquoi si la fonction en tant que variable locale est déjà directement lisible, je me demande sur la pertinence de l'extraire dans une méthode. Après c'est bien sûr subjectif.



8
votes

Par analogie avec IntSummaryStatistics , créez une classe PointStatistics qui collecte les informations dont vous avez besoin. Il définit deux méthodes: une pour enregistrer les valeurs d'un Point , une pour combiner deux Statistics.

Benchmark                        Mode  Cnt    Score     Error  Units

customCollector                  avgt   10  714.016 ± 151.558  ms/op
forEach                          avgt   10   54.334 ±   9.820  ms/op
fourStreams                      avgt   10  699.599 ± 138.332  ms/op
statistics                       avgt   10  148.649 ±  26.248  ms/op
twoStreams                       avgt   10  429.050 ±  72.879  ms/op

Ensuite, vous pouvez collecter un Diffusez dans un PointStatistics.

Benchmark                        Mode  Cnt   Score   Error  Units

customCollector                  avgt   10  68.117 ± 4.822  ms/op
forEach                          avgt   10   3.939 ± 0.559  ms/op
fourStreams                      avgt   10  57.800 ± 4.817  ms/op
statistics                       avgt   10   9.904 ± 1.048  ms/op
twoStreams                       avgt   10  32.303 ± 2.498  ms/op

UPDATE

J'ai été complètement déconcerté par la conclusion tirée par OP, alors j'ai décidé d'écrire JMH benchmarks.

Paramètres de référence:

Benchmark                        Mode  Cnt  Score   Error  Units

customCollector                  avgt   10  6.760 ± 0.789  ms/op
forEach                          avgt   10  0.255 ± 0.033  ms/op
fourStreams                      avgt   10  5.115 ± 1.149  ms/op
statistics                       avgt   10  0.887 ± 0.114  ms/op
twoStreams                       avgt   10  2.869 ± 0.567  ms/op


3 commentaires

Génial! Merci pour l'analyse comparative. Une idée de la raison pour laquelle mon collecteur personnalisé est si lent? C'est drôle de voir que le forEach fonctionne toujours mieux. Est-ce que cela change lorsque vous passez à un parallelStrream?


@MWB Il utilise un HashMap


@MWB oui, .parallel () le changera positivement, mais pas radicalement



10
votes

JDK 12 aura Collectors.teeing ( webrev et CSR ), qui recueille à deux collectionneurs différents et puis fusionne les deux résultats partiels en un résultat final.

Vous pouvez l'utiliser ici pour collecter deux IntSummaryStatistics pour la coordonnée x et le y code> coordonnée:

public static <T, A1, A2, R1, R2, R> Collector<T, ?, R> teeing(
        Collector<? super T, A1, R1> downstream1,
        Collector<? super T, A2, R2> downstream2,
        BiFunction<? super R1, ? super R2, R> merger) {

    class Acc {
        A1 acc1 = downstream1.supplier().get();
        A2 acc2 = downstream2.supplier().get();

        void accumulate(T t) {
            downstream1.accumulator().accept(acc1, t);
            downstream2.accumulator().accept(acc2, t);
        }

        Acc combine(Acc other) {
            acc1 = downstream1.combiner().apply(acc1, other.acc1);
            acc2 = downstream2.combiner().apply(acc2, other.acc2);
            return this;
        }

        R applyMerger() {
            R1 r1 = downstream1.finisher().apply(acc1);
            R2 r2 = downstream2.finisher().apply(acc2);
            return merger.apply(r1, r2);
        }
    }

    return Collector.of(Acc::new, Acc::accumulate, Acc::combine, Acc::applyMerger);
}

Ici, le premier collecteur rassemble les statistiques pour x et la seconde pour y . Ensuite, les statistiques pour x et y sont fusionnées dans une List au moyen du JDK 9 List.of méthode d'usine qui accepte deux éléments.

Une alternative à List :: of pour la fusion serait:

(xStats, yStats) -> Arrays.asList(xStats, yStats)

Si vous ne pas avoir JDK 12 installé sur votre machine, voici une version générique simplifiée de la méthode teeing que vous pouvez utiliser en toute sécurité comme méthode utilitaire:

List<IntSummaryStatistics> stats = points.stream()
    .collect(Collectors.teeing(
             Collectors.mapping(p -> p.x, Collectors.summarizingInt()),
             Collectors.mapping(p -> p.y, Collectors.summarizingInt()),
             List::of));

int minX = stats.get(0).getMin();
int maxX = stats.get(0).getMax();
int minY = stats.get(1).getMin();
int maxY = stats.get(1).getMax();

Veuillez noter que je ne considère pas les caractéristiques des collecteurs en aval lors de la création du collecteur retourné.


0 commentaires

2
votes

Merci à tous pour toutes les suggestions et réponses. C'est très utile et j'ai beaucoup appris!

J'ai décidé d'essayer la plupart de vos solutions (à l'exception de la solution JDK12). Pour certains d'entre eux, vous me fournissez déjà le code. De plus, j'ai créé mon propre Collector .

class extremesCollector implements Collector<Point, Map<String, Integer>, Map<String , Integer>> {

    @Override
    public Supplier<Map<String, Integer>> supplier() {
        Map<String, Integer> map = new HashMap<>();
        map.put("xMin", Integer.MAX_VALUE);
        map.put("yMin", Integer.MAX_VALUE);
        map.put("xMax", Integer.MIN_VALUE);
        map.put("yMax", Integer.MIN_VALUE);
        return () -> map;
    }

    @Override
    public BiConsumer<Map<String, Integer>, Point> accumulator() {
        return (a, b) -> {
            a.put("xMin", Math.min(a.get("xMin"), b.x));
            a.put("yMin", Math.min(a.get("yMin"), b.y));
            a.put("xMax", Math.max(a.get("xMax"), b.x));
            a.put("yMax", Math.max(a.get("yMax"), b.y));
        };
    }

    @Override
    public Function<Map<String, Integer>, Map<String, Integer>> finisher() {
        return Function.identity();
    }

    @Override
    public BinaryOperator<Map<String, Integer>> combiner() {
        return (a, b) -> {
            a.put("xMin", Math.min(a.get("xMin"), b.get("xMin")));
            a.put("yMin", Math.min(a.get("yMin"), b.get("yMin")));
            a.put("xMax", Math.max(a.get("xMax"), b.get("xMax")));
            a.put("yMax", Math.max(a.get("yMax"), b.get("yMax")));
            return a;
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        Set<Characteristics> characteristics = new HashSet<>();
        characteristics.add(Characteristics.UNORDERED);
        characteristics.add(Characteristics.CONCURRENT);
        characteristics.add(Characteristics.IDENTITY_FINISH);
        return characteristics;
    }
}

Concernant la vitesse, voici le classement:

  1. boucles for
  2. quatre flux individuels
  3. stream avec un collecteur personnalisé
  4. flux parallèle avec un collecteur personnalisé
  5. approche statistique fournie par Andrew Tobilko

Les numéros 2 et 3 sont en fait très proches en termes de vitesse. La version parallèle est probablement plus lente car mon ensemble de données est trop petit.


2 commentaires

Désolé, j'ai fait le chemin rapide et sale. J'ai mesuré le temps (System.nanotime) avant et après l'exécution du fragment de code 50 fois et j'ai pris la moyenne. Je l'ai fait pour chaque approche.


Si la vitesse est votre préoccupation, je préfère utiliser un tableau de 4 éléments ou un objet personnalisé au lieu d'un HashMap.