Java 8 Flux - recueillir vs réduire


Quand utiliseriez-vous collect() vs reduce()? Quelqu'un aurait-bon, d'exemples concrets, il est certainement préférable d'aller dans un sens ou l'autre?

Javadoc mentionne que collect () est une réduction mutable.

Étant donné qu'il s'agit d'une réduction mutable, je suppose qu'elle nécessite une synchronisation (interne) qui, à son tour, peut être préjudiciable aux performances. Vraisemblablement, reduce() est plus facilement parallélisable au prix de devoir créer une nouvelle structure de données pour le retour après chaque étape dans la réduction.

Les déclarations ci-dessus sont des suppositions cependant et j'aimerais qu'un expert sonne ici.

Author: Sergey Brunov, 2014-03-22

7 answers

Tout d'Abord, les valeurs de retour sont différentes:

<R,A> R collect(Collector<? super T,A,R> collector)

T reduce(T identity, BinaryOperator<T> accumulator)

, Donc collect retourne aucun R alors que reduce retourne T - le type de la Stream.

reduce est une opération" fold ", elle applique un opérateur binaire à chaque élément du flux où le premier argument de l'opérateur est la valeur de retour de l'application précédente et le deuxième argument est l'élément courant du flux.

collection est une opération d'agrégation où une "collection" est créé et chaque élément est "ajouté" à cette collection. Les collections dans différentes parties du flux sont ensuite additionnées.

Le document lié donne la raison d'avoir deux approches différentes:

Si nous voulions prendre un flux de chaînes et les concaténer en un une seule longue chaîne, nous pourrions y parvenir avec une réduction ordinaire:

 String concatenated = strings.reduce("", String::concat)  

Nous obtiendrions le résultat souhaité, et cela fonctionnerait même en parallèle. Cependant, nous pourrions ne pas être heureux à propos de la performance! Une telle l'implémentation ferait beaucoup de copie de chaîne, et l'exécution le temps serait O(n^2) dans le nombre de caractères. Un plus performant l'approche consisterait à accumuler les résultats dans un StringBuilder, qui est un conteneur mutable pour accumuler des chaînes. Nous pouvons utiliser la même technique pour paralléliser la réduction mutable que nous le faisons avec l'ordinaire réduction.

Donc le fait est que la parallélisation est la même dans les deux cas mais dans le reduce cas nous appliquons la fonction aux éléments de flux eux-mêmes. Dans le cas collect, nous appliquons la fonction à un conteneur mutable.

 73
Author: Boris the Spider, 2016-10-17 18:25:19

La raison en est simplement que:

  • collect() ne peut fonctionner que avec mutable les objets du résultat.
  • reduce() est conçu pour fonctionner avec immuable les objets du résultat.

"reduce() avec immuable " exemple

public class Employee {
  private Integer salary;
  public Employee(String aSalary){
    this.salary = new Integer(aSalary);
  }
  public Integer getSalary(){
    return this.salary;
  }
}

@Test
public void testReduceWithImmutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));
  list.add(new Employee("3"));

  Integer sum = list
  .stream()
  .map(Employee::getSalary)
  .reduce(0, (Integer a, Integer b) -> Integer.sum(a, b));

  assertEquals(new Integer(6), sum);
}

"collect() avec mutables" exemple

Par exemple, si vous souhaitez calculer manuellement une somme en utilisant collect(), cela ne peut pas fonctionner avec BigDecimal mais seulement avec MutableInt à partir de org.apache.commons.lang.mutable par exemple. Voir:

public class Employee {
  private MutableInt salary;
  public Employee(String aSalary){
    this.salary = new MutableInt(aSalary);
  }
  public MutableInt getSalary(){
    return this.salary;
  }
}

@Test
public void testCollectWithMutable(){
  List<Employee> list = new LinkedList<>();
  list.add(new Employee("1"));
  list.add(new Employee("2"));

  MutableInt sum = list.stream().collect(
    MutableInt::new, 
    (MutableInt container, Employee employee) -> 
      container.add(employee.getSalary().intValue())
    , 
    MutableInt::add);
  assertEquals(new MutableInt(3), sum);
}

Ceci fonctionne parce que les accumulateur container.add(employee.getSalary().intValue()); n'est pas censé renvoyer un nouvel objet avec le résultat, mais pour changer l'état de la mutable container de type MutableInt.

Si vous souhaitez utiliser BigDecimal au lieu de la container vous ne pourriez pas utiliser le collect() méthode container.add(employee.getSalary()); ne changerait pas la container, car BigDecimal, il est immuable. (En dehors de cela BigDecimal::new ne fonctionnerait pas car BigDecimal n'a pas de constructeur vide)

 26
Author: Sandro, 2017-10-06 13:58:24

La réduction normale est destinée à combiner deux valeurs immuables telles que int, double, etc. et en produire un nouveau; c'est une réduction immuable. En revanche, la méthode collect est conçue pour muter un conteneur pour accumuler le résultat qu'il est censé produire.

Pour illustrer le problème, supposons que vous souhaitiez réaliser Collectors.toList() en utilisant une simple réduction comme ci-dessous

    List<Integer> numbers = stream.reduce( new ArrayList<Integer>(), 
    (List<Integer> l, Integer e) -> {
     l.add(e); 
     return l; 
    },
     (List<Integer> l1, List<Integer> l2) -> { 
    l1.addAll(l2); return l1; });

C'est l'équivalent de Collectors.toList(). Toutefois, dans ce cas, vous muter le List<Integer>. Comme nous le savons, le ArrayList n'est pas sûr pour les threads, ni pour ajouter/supprimer des valeurs lors de l'itération, vous obtiendrez donc soit une exception concurrente, soit une exception arrayIndexOutBound ou tout type d'exception (en particulier lorsqu'il est exécuté en parallèle) lorsque vous mettez à jour la liste ou que le combineur essaie de fusionner les listes Si vous voulez rendre ce thread sûr, vous devez passer une nouvelle liste à chaque fois, ce qui nuirait performance.

En revanche, le Collectors.toList() fonctionne de la même manière. Cependant, il garantit la sécurité des threads lorsque vous accumulez les valeurs dans la liste. De la documentation pour la méthode collect:

Effectue une opération de réduction mutable sur les éléments de ce flux à l'aide d'un Collecteur. Si le flux est parallèle et que le collecteur est simultané, et le flux est non ordonnée ou le collecteur n'est pas ordonné, puis un réduction simultanée sera effectuée. Lorsqu'ils sont exécutés en parallèle, plusieurs résultats intermédiaires peuvent être instanciés, remplis et fusionnés de manière à maintenir l'isolement des structures de données mutables. Par conséquent, même lorsqu'il est exécuté en parallèle avec des structures de données non thread-safe (telles que ArrayList), aucune synchronisation supplémentaire n'est nécessaire pour une réduction parallèle. lien

Donc, pour répondre à votre question:

Quand utiliseriez-vous collect() vs reduce()?

Si vous avoir des valeurs immuables telles que ints, doubles, Strings ensuite, la réduction normale fonctionne très bien. Cependant, si vous devez reduce vos valeurs dans say a List (structure de données mutable), vous devez utiliser la réduction mutable avec la méthode collect.

 14
Author: george, 2017-11-17 20:01:48

Laissez le flux a

En réduction,

Vous aurez ((a # b) # c) # d

Où # est cette opération intéressante que vous aimeriez faire.

Dans la collecte, le

Votre collecteur aura une sorte de structure de collecte K.

K consomme. K consomme alors b. K consomme alors c. K consomme alors d.

À la fin, vous demandez à K quel est le résultat final.

K vous le donne ensuite.

 5
Author: Yan Ng, 2016-10-13 08:27:56

Ils sont très différents dans l'empreinte mémoire potentielle pendant l'exécution. Alors que collect()collecte et place toutes les données dans la collection, reduce() vous demande explicitement de spécifier comment réduire les données qui ont traversé le flux.

Par exemple, si vous voulez lire certaines données d'un fichier, les traiter et les mettre dans une base de données, vous pourriez vous retrouver avec du code java stream similaire à ceci:

streamDataFromFile(file)
            .map(data -> processData(data))
            .map(result -> database.save(result))
            .collect(Collectors.toList());

Dans ce cas, nous utilisons collect() pour forcer java à diffusez les données et enregistrez le résultat dans la base de données. Sans collect() les données ne sont jamais lues et jamais stockées.

Ce code génère heureusement une erreur d'exécution java.lang.OutOfMemoryError: Java heap space, si la taille du fichier est suffisamment grande ou si la taille du tas est suffisamment faible. La raison évidente est qu'il essaie d'empiler toutes les données qui ont traversé le flux (et, en fait, ont déjà été stockées dans la base de données) dans la collection résultante et cela explose le tas.

Cependant, si vous remplacez collect() avec reduce() it ce ne sera plus un problème car ce dernier réduira et rejettera toutes les données qui l'ont traversé.

Dans l'exemple présenté, il suffit de remplacer {[2] } par quelque chose avec reduce:

.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);

Vous n'avez même pas besoin de vous soucier de faire dépendre le calcul du result car Java n'est pas un langage FP (functional programming) pur et ne peut pas optimiser les données qui ne sont pas utilisées au bas du flux en raison des effets secondaires possibles.

 2
Author: averasko, 2016-07-21 20:17:42

Voici l'exemple de code

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().reduce((x,y) -> {
        System.out.println(String.format("x=%d,y=%d",x,y));
        return (x + y);
    }).get();

Système.hors.println(somme);

Voici le résultat d'exécution:

x=1,y=2
x=3,y=3
x=6,y=4
x=10,y=5
x=15,y=6
x=21,y=7
28

Réduire la fonction gérer deux paramètres, le premier paramètre est la valeur de retour précédente int le flux, le deuxième paramètre est le courant calculer la valeur dans le flux, il somme la première valeur et la valeur actuelle comme première valeur dans la prochaine caculation.

 1
Author: JetQin, 2018-03-15 15:20:16

Selon les documents

Les collecteurs reducing() sont les plus utiles lorsqu'ils sont utilisés dans une réduction à plusieurs niveaux, en aval de groupingBy ou partitioningBy. Pour effectuer une réduction simple sur un flux, utilisez Flux.réduire (BinaryOperator) à la place.

Donc, fondamentalement, vous utiliseriez reducing() uniquement lorsqu'il est forcé dans une collecte. Voici un autre exemple:

 For example, given a stream of Person, to calculate the longest last name 
 of residents in each city:

    Comparator<String> byLength = Comparator.comparing(String::length);
    Map<String, String> longestLastNameByCity
        = personList.stream().collect(groupingBy(Person::getCity,
            reducing("", Person::getLastName, BinaryOperator.maxBy(byLength))));

Selon ce tutoriel réduire l'est parfois moins efficace

L'opération de réduction renvoie toujours une nouvelle valeur. Cependant, la fonction accumulateur renvoie également une nouvelle valeur chaque fois qu'elle traite un élément d'un flux. Supposons que vous souhaitiez réduire les éléments d'un flux à un objet plus complexe, tel qu'une collection. Cela peut nuire aux performances de votre application. Si votre opération de réduction implique l'ajout d'éléments à une collection, chaque fois que votre fonction d'accumulateur traite un élément, elle crée une nouvelle collection qui inclut élément, qui est inefficace. Il serait plus efficace pour vous de mettre à jour une collection existante à la place. Vous pouvez le faire avec le flux.méthode de collecte, décrite dans la section suivante...

Donc l'identité est "réutilisée" dans un scénario de réduction, donc légèrement plus efficace pour aller avec .reduce si possible.

 0
Author: rogerdpack, 2018-03-13 02:27:28