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?
5 Réponses :
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
.
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();
Alternativement, au lieu de créer un objet Function
, vous pouvez simplement extraire une méthode: public static IntSummaryStatistics getSummaryStatistics (Collection
@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.
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 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
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
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é.
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:
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.
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.
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 surInteger.MIN_VALUE
. Cela signifie que la conditionif (point.x> maxX)
deviendra définitivementtrue
(sauf si toutes les valeurs x sont égales àInteger.MIN_VALUE
, ce qui est hautement improbable).