IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

avec Java Discussion :

Java 8, streams et reduce à trois arguments


Sujet :

avec Java

Vue hybride

ptyxs Java 8, streams et reduce à... 13/06/2014, 16h15
adiGuba Salut, Déjà le code est... 13/06/2014, 18h03
professeur shadoko L'exemple me semble bizarre... 13/06/2014, 18h04
adiGuba Oui c'est tout à fait cela. ... 13/06/2014, 19h07
ptyxs Merci beaucoup pour vos... 14/06/2014, 09h01
geforce C'est une tres bonne question... 29/12/2017, 04h06
thelvin Je comprends pas trop c'est... 31/12/2017, 03h15
Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre très actif
    Profil pro
    professeur des universités à la retraite
    Inscrit en
    Août 2008
    Messages
    364
    Détails du profil
    Informations personnelles :
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : professeur des universités à la retraite

    Informations forums :
    Inscription : Août 2008
    Messages : 364
    Par défaut Java 8, streams et reduce à trois arguments
    Bonjour,
    j'essaie de comprendre quelque chose à la nouvelle programmation fonctionnelle à base de Stream et de lambdas introduite par Java 8.
    Je tente de comprendre le livre de Richard Warburton Java 8 lambdas.
    L'un des exemples donnés, chap 5, page 70 de la version française, exemple 15-19, est le suivant, il traite un stream issu d'une collection d'objets représentant des artistes, appelée... artists et il produit une chaîne contenant l'ensemble des noms de ces artistes.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     
    StringBuilder reduced = 
      artists.stream()
         .map(Artist::getName()
         .reduce(new StringBuilder(), (builder,name) -> {
             if(builder.length >0)
                builder.append(", ");
     
             builder.append(name);
             return builder;
         }, (left,right) -> left.append(right));
     
     reduced.insert(0,"[");
     reduced.append("]");
    String result = reduced.toString();
    Ma question est la suivante : à quoi diable sert donc le troisième argument de reduce, la lambda (left, right) -> left.append(right). Je ne vois pas du tout quel rôle
    elle joue vraiment.
    Je pensais que la documentation Java m'éclairerait mais j'y vois ceci, qui ne m'éclaire pas davantage :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <U> U reduce(U identity,
                 BiFunction<U,? super T,U> accumulator,
                 BinaryOperator<U> combiner)
     
    Performs a reduction on the elements of this stream, using the provided identity, accumulation and combining functions. This is equivalent to:
     
     
         U result = identity;
         for (T element : this stream)
             result = accumulator.apply(result, element)
         return result;
    Quelqu'un voit-il quel est le rôle de ce troisième argument combiner dans le processus conduisant au résultat final ?
    Merci d'avance.

  2. #2
    Expert éminent
    Avatar de adiGuba
    Homme Profil pro
    Développeur Java/Web
    Inscrit en
    Avril 2002
    Messages
    13 938
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Développeur Java/Web
    Secteur : Transports

    Informations forums :
    Inscription : Avril 2002
    Messages : 13 938
    Billets dans le blog
    1
    Par défaut
    Salut,


    Déjà le code est un peu faux ! Il ne faut pas utiliser reduce() pour cela.

    reduce() prend trois paramètres :
    • L'identity correspondant à la valeur initial (lorsque la liste est vide). Il devrait s'agir d'une constante qui ne sera pas modifié.
    • L'accumulator correspond à la méthode qui va accumuler les données.
      Cette méthode prend la valeur courante ainsi qu'une valeur du stream, et doit retourner la nouvelle valeur sous forme d'une nouvelle instance.
      C'est là que le code est faux (j'expliquerais cela plus bas).
    • Enfin le combiner sert à regrouper deux résultats différents... mais cela sert uniquement si le traitement est exécuté en parallèle...



    Là où le code est faux, c'est qu'il modifie l'identity, et que l'accumulator ne retourne pas une nouvelle valeur.
    En séquentiel cela ne pose pas vraiment de problème, mais en parallèle cela génèrera des erreurs...


    En séquentiel grosso-modo le traitement correspond à ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    StringBuilder result = identity;
    for(Artist artist : artists) {
        result = accumulator(result, artist);
    }
    Donc cela ne pose pas de problème.


    En parallèle c'est la même chose, mais la source de données "artists" est découpé en plusieurs sources, et le code ci-dessus est dispatché sur plusieurs threads.
    Le problème c'est que cela partage la même "identity" entre tous les threads, ce qui va provoquer des erreurs...

    Donc en fait déjà il faudrait que l'accumulator du reduce() retourne un nouvel objet à chaque fois :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    StringBuilder reduced = artists.stream()
            .map(Artist::getName)
            .reduce(new StringBuilder(), (builder, name) -> {
                StringBuilder newbuilder = new StringBuilder();
                if (builder.length() > 0) {
                    newbuilder.append(builder).append(", ");
                }
                newbuilder.append(name);
                return newbuilder;
            }, (left, right) -> left.append(right));
    C'est pour cela que le reduce() n'est pas trop adapté dans ce cas, car cela implique de retourner un nouveau StringBuilder à chaque fois, ce qui est lourd et gourmand en mémoire !
    En fait pour cela la méthode collect() est plus adaptée (j'y reviendrais plus bas)





    Mais d'abord on revient au sujet sur le troisième paramètre, le combiner.
    Celui-ci sert lorsqu'on est en parallèle. Imaginons qu'on ait une liste de 6 artistes ("Fred", "Paul", "Eric", "Michel", "Pascal", "Franck").
    En séquentiel pas de soucis on aura le résultat suivant : "Fred, Paul, Eric, Michel, Pascal, Franck".

    Maintenant si on fais cela en parallèle, avec deux threads on pourrait avoir deux résultats : "Fred, Paul, Eric" et "Michel, Pascal, Franck".
    Le combiner sert donc à réunir ces deux résultats en un seul, pour les combiner et obtenir le résultat final.
    D'ailleurs dans l'exemple le combiner est également faux puisqu'il ne prend pas en comte la virgule qu'il faut rajouter entre les deux...





    Mais en fait reduce() est plus adapté à la recherche d'un élément.
    Pour concaténer ou ajouter dans une collection, il est préférable d'utiliser la méthode collect() :




    C'est grosso-modo la même chose sauf que la valeur initiale est passé via un supplier (elle pourra donc être différente pour chaque thread), et que les méthodes accumulator et combiner travaille sur l'objet qu'elle reçoivent en paramètres (au lieu de retourner un nouvel objet).

    Dans ton cas cela donnerait ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    StringBuilder text = artists.stream()
            .map(Artist::getName)
            .collect(
               // supplier
               // utilisé au début du thread pour initialiser l'objet qui accumulera les données
               () -> new StringBuilder(),
               // accumulator
               // utilisé pour ajouter chaque élément dans l'objet de la collecte (sb ici)
               (sb, name) -> {
                   if (sb.length()>0)
                       sb.append(", ");
                   sb.append(name);
               },
               // combiner
               // utilisé pour regrouper deux résultats de thread différents
               (left, right) -> {
                   if (left.length()>0 && right.length()>0)
                       left.append(", ");
                   left.append(right);
               }
            );
    Si tu veux pousser plus loin tu peux regarder du coté de Collector.of() qui permet de pousser un peu plus loin dans le contrôle...




    Mais surtout tu as la classe Collectors qui fourni un bon nombre d'implémentation de base, et le code complet que tu as donné peut être remplacé par ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    String text = artists.stream()
            .map(Artist::getName)
            .collect(Collectors.joining(", ", "[", "]"));
    (et en plus il marchera en parallèle)



    a++

  3. #3
    Membre Expert
    Avatar de professeur shadoko
    Homme Profil pro
    retraité nostalgique Java SE
    Inscrit en
    Juillet 2006
    Messages
    1 257
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 76
    Localisation : France, Hauts de Seine (Île de France)

    Informations professionnelles :
    Activité : retraité nostalgique Java SE

    Informations forums :
    Inscription : Juillet 2006
    Messages : 1 257
    Par défaut
    L'exemple me semble bizarre mais voici ce que j'en comprends:
    - l'accumulateur remplit un StringBuilder
    - le combineur (3° argument) recombine de StringBuilders accumulés (éventuellement en parallèle)
    pas clair ....
    hyper-grilled

  4. #4
    Expert éminent
    Avatar de adiGuba
    Homme Profil pro
    Développeur Java/Web
    Inscrit en
    Avril 2002
    Messages
    13 938
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Développeur Java/Web
    Secteur : Transports

    Informations forums :
    Inscription : Avril 2002
    Messages : 13 938
    Billets dans le blog
    1
    Par défaut
    Citation Envoyé par professeur shadoko Voir le message
    L'exemple me semble bizarre mais voici ce que j'en comprends:
    - l'accumulateur remplit un StringBuilder
    - le combineur (3° argument) recombine de StringBuilders accumulés (éventuellement en parallèle)
    Oui c'est tout à fait cela.

    La doc de l'interface Stream peut sembler pas très complète, mais il ne faut pas oublier les liens vers la documentation du package bien plus complète : http://docs.oracle.com/javase/8/docs...e-summary.html

    On peut y lire que les méthodes du reduce() doivent bien renvoyer une nouvel objet (et non pas modifier leurs paramètres) :
    <U> U reduce(U identity,
    BiFunction<U, ? super T, U> accumulator,
    BinaryOperator<U> combiner);


    Here, the identity element is both an initial seed value for the reduction and a default result if there are no input elements. The accumulator function takes a partial result and the next element, and produces a new partial result. The combiner function combines two partial results to produce a new partial result. (The combiner is necessary in parallel reductions, where the input is partitioned, a partial accumulation computed for each partition, and then the partial results are combined to produce a final result.)

    a++

  5. #5
    Membre très actif
    Profil pro
    professeur des universités à la retraite
    Inscrit en
    Août 2008
    Messages
    364
    Détails du profil
    Informations personnelles :
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : professeur des universités à la retraite

    Informations forums :
    Inscription : Août 2008
    Messages : 364
    Par défaut
    Merci beaucoup pour vos réponses si détaillées et argumentées, je commence enfin à comprendre à quoi peut servir ce troisième argument...

    Je tiens à signaler - pour que vous ne pensiez pas de mal du bouquin - que l'exemple que j'ai donné n'est pas présenté - dans le livre de Warburtion - comme un modèle définitif de ce qu'il faudrait faire, mais comme une simple étape didactique qui conduit progressivement à un code plus satisfaisant par des refactorings successifs.
    il s'agit de montrer, dans le passage du livre concerné, comment on peut créer soi-même un collector et le code final utilise ce collector (appelé StringCollector) ainsi :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    String result = 
      artists.stream()
          .map(Artist::getname)
          .collect(new StringCollector(", ", "[", "]"));
    L'auteur signale lui-même que cela fait double emploi avec ce que fournit nativement Java 8 et qu'il s'agit d'un simple exercice, du reste la solution mentionnée par adiGuba utilisant Collectors.joining est celle qui est donnée pour commencer, en 5-12, p. 67.

    EDIT Mais à vrai dire même un exemple intermédiaire et purement pédagique aurait dû faire l'objet d'un caveat concernant la modification de l'élément identité et les problèmes que cela soulève en cas de parallélisme.

  6. #6
    Membre éprouvé
    Avatar de geforce
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Janvier 2010
    Messages
    1 055
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Janvier 2010
    Messages : 1 055
    Par défaut
    C'est une tres bonne question quand en utilise le parallelisme! Opps Multithread != Parallel
    ** Rappele le Parallelisme dans les Stream Java 8 est construit avec le concept du Multithreding.

    la somme ce fait en parallele
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
        List<Integer> list2 = Arrays.asList(2, 3, 4);
        //Here result will be 2*2 + 2*3 + 2*4 that is 18.
        int res = list2.parallelStream().reduce(2, (s1, s2) -> s1 * s2, (p, q) -> p + q);
        System.out.println(res);
    Source : https://www.concretepage.com/java/jd...le#accumulator

  7. #7
    Modérateur

    Profil pro
    Inscrit en
    Septembre 2004
    Messages
    12 582
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Septembre 2004
    Messages : 12 582
    Par défaut
    Je comprends pas trop c'est quoi ton histoire comme quoi parallélisme et multithreading c'est pas pareil (parce que bon, sur un ordinateur il y a pas 150 mécanismes pour faire plusieurs choses en parallèle...)

    Mais je voulais signaler qu'il est pourri cet exemple.

    Bon d'accord il illustre bien qu'en stream parallèle, on utilise bel et bien l'accumulator et le combinor. Certes. Et là du coup ça se voit, ok.

    Mais le principe de ces deux trucs est que justement ça doit toujours produire le même résultat qu'on se serve de l'un ou de l'autre, et donc ça ne doit pas se voir.
    L'identité, son nom est pourtant bien porté, ne doit pas être 2 (à moins qu'on me trouve une fonction de deux nombres dont 2 est l'identité.) Avec la multiplication l'identité c'est 1.
    ... Et avec l'addition l'identité c'est zéro. Deuxième problème, l'accumulator et le combinor doivent produire les mêmes résultats indépendamment du nombre de fois que le combinor intervient, zéro compris. Évidemment on ne peut pas en avoir un qui fait une addition et l'autre une multiplication.

    Ce truc est une sérieuse faute de programmation.
    N'oubliez pas de consulter les FAQ Java et les cours et tutoriels Java

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 3
    Dernier message: 14/03/2011, 17h12
  2. [Java][Info] argument de vente de java à un client
    Par billou77 dans le forum Général Java
    Réponses: 11
    Dernier message: 28/08/2005, 23h04
  3. [Apis]parser les arguments d'un programme Java
    Par sacofan dans le forum API standards et tierces
    Réponses: 4
    Dernier message: 06/08/2005, 14h32
  4. [Process]renvoyez un zcat en argument pour un prog java
    Par jdeboer dans le forum Eclipse Java
    Réponses: 5
    Dernier message: 01/07/2005, 15h48
  5. [Débutant][Main]Argument d'un progs java
    Par sanchou dans le forum Langage
    Réponses: 6
    Dernier message: 07/10/2004, 12h14

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo