web-dev-qa-db-fra.com

Pourquoi les primitives Stream n'ont-elles pas collect (Collector)?

J'écris une bibliothèque pour les programmeurs débutants, alors j'essaie de garder l'API aussi propre que possible.

Une des tâches que ma bibliothèque doit effectuer est d’effectuer des calculs complexes sur une vaste collection d’entités ou de longues. Mes utilisateurs doivent calculer ces valeurs à partir de nombreux scénarios et objets métier. Je pensais donc que le meilleur moyen serait d'utiliser des flux pour permettre aux utilisateurs de mapper des objets métier sur IntStream ou LongStream, puis de calculer les calculs à l'intérieur d'un collecteur.

Cependant, IntStream et LongStream ont uniquement la méthode de collecte à 3 paramètres:

collect(Supplier<R> supplier, ObjIntConsumer<R> accumulator, BiConsumer<R,R> combiner)

Et n'a pas la méthode collect(Collector) plus simple que Stream<T> a.

Donc, au lieu de pouvoir faire

Collection<T> businessObjs = ...
MyResult result = businessObjs.stream()
                              .mapToInt( ... )
                              .collect( new MyComplexComputation(...));

Je dois fournir des fournisseurs, des accumulateurs et des combineurs comme ceci:

MyResult result = businessObjs.stream()
                              .mapToInt( ... )
                              .collect( 
                                  ()-> new MyComplexComputationBuilder(...),
                                  (builder, v)-> builder.add(v),
                                  (a,b)-> a.merge(b))
                              .build(); //prev collect returns Builder object

C'est beaucoup trop compliqué pour mes utilisateurs novices et très sujet aux erreurs.

Mon travail consiste à créer des méthodes statiques prenant IntStream ou LongStream en entrée et à masquer pour vous la création et l’exécution du collecteur.

public static MyResult compute(IntStream stream, ...){
       return .collect( 
                        ()-> new MyComplexComputationBuilder(...),
                        (builder, v)-> builder.add(v),
                        (a,b)-> a.merge(b))
               .build();
}

Mais cela ne suit pas les conventions habituelles de travail avec Streams:

IntStream tmpStream = businessObjs.stream()
                              .mapToInt( ... );

 MyResult result = MyUtil.compute(tmpStream, ...);

Parce que vous devez soit enregistrer une variable temporaire et la transmettre à la méthode statique, soit créer le Stream à l'intérieur de l'appel statique, ce qui peut prêter à confusion lorsqu'il est mélangé avec les autres paramètres de mon calcul.

Existe-t-il une méthode plus propre pour le faire tout en travaillant avec IntStream ou LongStream?

29
dkatzel

Nous avons en fait prototypé certaines spécialisations Collector.OfXxx. Ce que nous avons constaté - en plus des inconvénients évidents de types plus spécialisés - c’est que cela n’était pas vraiment utile sans un ensemble complet de collections spécialisées (comme Trove ou GS-Collections, mais que le JDK ne propose pas). ne pas avoir). Sans IntArrayList, par exemple, un Collector.OfInt ne fait que pousser la boxe ailleurs - du Collector au conteneur - ce qui n'est pas un gros gain et beaucoup plus de surface API. 

24
Brian Goetz

Peut-être que si les références aux méthodes sont utilisées à la place de lambdas, le code nécessaire à la collecte du flux primitif ne semblera pas aussi compliqué.

MyResult result = businessObjs.stream()
                              .mapToInt( ... )
                              .collect( 
                                  MyComplexComputationBuilder::new,
                                  MyComplexComputationBuilder::add,
                                  MyComplexComputationBuilder::merge)
                              .build(); //prev collect returns Builder object

Dans le réponse définitive à cette question de Brian, il mentionne deux autres frameworks de collection Java qui possèdent des collections primitives pouvant être utilisées avec la méthode collect sur des flux primitifs. J'ai pensé qu'il pourrait être utile d'illustrer quelques exemples d'utilisation des conteneurs primitifs dans ces cadres avec des flux primitifs. Le code ci-dessous fonctionnera également avec un flux parallèle. 

// Eclipse Collections
List<Integer> integers = Interval.oneTo(5).toList();

Assert.assertEquals(
        IntInterval.oneTo(5),
        integers.stream()
                .mapToInt(Integer::intValue)
                .collect(IntArrayList::new, IntArrayList::add, IntArrayList::addAll));

// Trove Collections

Assert.assertEquals(
        new TIntArrayList(IntStream.range(1, 6).toArray()),
        integers.stream()
                .mapToInt(Integer::intValue)
                .collect(TIntArrayList::new, TIntArrayList::add, TIntArrayList::addAll));

Remarque: Je suis l'auteur de Eclipse Collections .

6
Donald Raab

J'ai implémenté les collecteurs de primitives dans ma bibliothèque StreamEx (à partir de la version 0.3.0). Il existe des interfaces IntCollector , LongCollector et DoubleCollector qui étendent l'interface Collector et sont spécialisées dans l'utilisation de primitives. Il existe une différence mineure supplémentaire dans la procédure de combinaison car des méthodes telles que IntStream.collect acceptent une BiConsumer au lieu de BinaryOperator.

Il existe de nombreuses méthodes de collecte prédéfinies pour joindre des nombres à une chaîne, stocker dans un tableau primitif, à BitSet, rechercher min, max, somme, calculer des statistiques récapitulatives, effectuer des opérations de regroupement et de partitionnement. Bien sûr, vous pouvez définir vos propres collectionneurs. Voici plusieurs exemples d'utilisation (en supposant que vous ayez un tableau int[] input avec des données d'entrée).

Joindre des nombres en tant que chaîne avec séparateur:

String nums = IntStreamEx.of(input).collect(IntCollector.joining(","));

Regroupement par dernier chiffre:

Map<Integer, int[]> groups = IntStreamEx.of(input)
      .collect(IntCollector.groupingBy(i -> i % 10));

Sommez les nombres positifs et négatifs séparément:

Map<Boolean, Integer> sums = IntStreamEx.of(input)
      .collect(IntCollector.partitioningBy(i -> i > 0, IntCollector.summing()));

Voici un simple benchmark qui compare ces collectionneurs et les collectionneurs d’objets habituels.

Notez que ma bibliothèque ne fournit pas (et ne fournira pas à l'avenir) de structures de données visibles par l'utilisateur telles que des cartes sur des primitives. Le regroupement est donc effectué dans la variable HashMap habituelle. Toutefois, si vous utilisez Trove/GS/HFTC/peu importe, il n’est pas si difficile d’écrire des collecteurs de primitives supplémentaires pour les structures de données définies dans ces bibliothèques afin d’améliorer les performances.

3
Tagir Valeev

Convertissez les flux primitifs en flux d’objets encadrés s’il existe des méthodes qui vous manquent.

MyResult result = businessObjs.stream()
                          .mapToInt( ... )
                          .boxed()
                          .collect( new MyComplexComputation(...));

Ou n'utilisez pas les flux primitifs en premier lieu et travaillez avec Integers tout le temps.

MyResult result = businessObjs.stream()
                          .map( ... )     // map to Integer not int
                          .collect( new MyComplexComputation(...));
3
John Kugelman

M. Geotz a fourni la réponse définitive à la raison pour laquelle la décision avait été prise de ne pas inclure les collectionneurs spécialisés , cependant, je souhaitais approfondir l’influence de cette décision sur les performances. 

Je pensais poster mes résultats comme réponse.

J'ai utilisé le cadre jmh microbenchmark pour calculer le temps nécessaire au calcul des calculs à l'aide des deux types de collecteurs sur des collections de tailles 1, 100, 1000, 100 000 et 1 million:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class MyBenchmark {

@Param({"1", "100", "1000", "100000", "1000000"})
public int size;

List<BusinessObj> seqs;

@Setup
public void setup(){
    seqs = new ArrayList<BusinessObj>(size);
    Random Rand = new Random();
    for(int i=0; i< size; i++){
        //these lengths are random but over 128 so no caching of Longs
        seqs.add(BusinessObjFactory.createOfRandomLength());
    }
}
@Benchmark
public double objectCollector() {       

    return seqs.stream()
                .map(BusinessObj::getLength)
                .collect(MyUtil.myCalcLongCollector())
                .getAsDouble();
}

@Benchmark
public double primitiveCollector() {

    LongStream stream= seqs.stream()
                                    .mapToLong(BusinessObj::getLength);
    return MyUtil.myCalc(stream)        
                        .getAsDouble();
}

public static void main(String[] args) throws RunnerException{
    Options opt = new OptionsBuilder()
                        .include(MyBenchmark.class.getSimpleName())
                        .build();

    new Runner(opt).run();
}

}

Voici les résultats:

# JMH 1.9.3 (released 4 days ago)
# VM invoker: /Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/bin/Java
# VM options: <none>
# Warmup: 20 iterations, 1 s each
# Measurement: 20 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: org.sample.MyBenchmark.objectCollector

# Run complete. Total time: 01:30:31

Benchmark                        (size)  Mode  Cnt          Score         Error  Units
MyBenchmark.objectCollector           1  avgt  200        140.803 ±       1.425  ns/op
MyBenchmark.objectCollector         100  avgt  200       5775.294 ±      67.871  ns/op
MyBenchmark.objectCollector        1000  avgt  200      70440.488 ±    1023.177  ns/op
MyBenchmark.objectCollector      100000  avgt  200   10292595.233 ±  101036.563  ns/op
MyBenchmark.objectCollector     1000000  avgt  200  100147057.376 ±  979662.707  ns/op
MyBenchmark.primitiveCollector        1  avgt  200        140.971 ±       1.382  ns/op
MyBenchmark.primitiveCollector      100  avgt  200       4654.527 ±      87.101  ns/op
MyBenchmark.primitiveCollector     1000  avgt  200      60929.398 ±    1127.517  ns/op
MyBenchmark.primitiveCollector   100000  avgt  200    9784655.013 ±  113339.448  ns/op
MyBenchmark.primitiveCollector  1000000  avgt  200   94822089.334 ± 1031475.051  ns/op

Comme vous pouvez le constater, la version primitive de Stream est légèrement plus rapide, mais même lorsque la collection contient 1 million d’éléments, elle n’est que de 0,05 secondes plus rapide (en moyenne).

Pour mon API, je préférerais rester fidèle aux conventions plus propres du flux d’objets et utiliser la version Boxed car c’est un inconvénient mineur en termes de performances.

Merci à tous ceux qui ont apporté un éclairage sur ce problème.

0
dkatzel