web-dev-qa-db-fra.com

Java 8 flux - collecter ou réduire

Quand utiliseriez-vous collect() vs reduce()? Quelqu'un a-t-il de bons exemples concrets de la meilleure façon d'aller d'une manière ou d'une 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 la création d'une nouvelle structure de données à restituer après chaque étape de la réduction.

Les déclarations ci-dessus sont cependant des conjectures et j'aimerais avoir un expert qui sonne ici.

121
jimhooker2002

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 second argument est l'élément de flux actuel.

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

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

Si nous voulions prendre un flux de chaînes et les concaténer en une seule chaîne longue, 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 ne serions peut-être pas heureux de la performance! Une telle implémentation ferait beaucoup de copie de chaîne et le temps d'exécution serait de O (n ^ 2) en nombre de caractères. Une approche plus performante consisterait à accumuler les résultats dans un StringBuilder, qui est un conteneur modifiable pour accumuler des chaînes. Nous pouvons utiliser la même technique pour paralléliser la réduction mutable que nous le faisons avec la réduction ordinaire.

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

99
Boris the Spider

La raison est simplement que:

  • collect() ne peut fonctionner qu'avec des objets de résultat mutables .
  • reduce() est conçu pour fonctionner avec des objets de résultat immuables .

Exemple "reduce() with immutable"

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(Integer.valueOf(6), sum);
}

Exemple "collect() with mutable"

Par exemple. si vous souhaitez calculer manuellement une somme à l'aide de collect(), il ne peut pas fonctionner avec BigDecimal mais uniquement avec MutableInt 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);
}

Cela fonctionne car accumulateurcontainer.add(employee.getSalary().intValue()); n'est pas censé renvoyer un nouvel objet avec le résultat, mais modifier l'état du mutable container de type MutableInt.

Si vous souhaitez utiliser BigDecimal à la place de container, vous ne pouvez pas utiliser la méthode collect() car container.add(employee.getSalary()); ne changera pas le container car BigDecimal ne le changera pas. (En dehors de cela, BigDecimal::new ne fonctionnerait pas car BigDecimal n'a pas de constructeur vide)

33
Sandro

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

Pour illustrer le problème, supposons que vous souhaitiez obtenir Collectors.toList() en utilisant une réduction simple 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(). Cependant, dans ce cas, vous modifiez le List<Integer>. Comme nous le savons, ArrayList n’est pas thread-safe, ni ajouter/supprimer des valeurs lors de la répétition, vous obtiendrez donc une exception concurrente ou une exception arrayIndexOutBound ou toute autre exception (surtout lorsqu’elle est exécutée en parallèle) lorsque vous mettez à jour la liste ou le combinateur essaie de fusionner les listes parce que vous faites la mutation de la liste en y accumulant (en ajoutant) les entiers. Si vous souhaitez sécuriser ce thread, vous devez passer une nouvelle liste à chaque fois, ce qui nuirait aux performances.

En revanche, la Collectors.toList() fonctionne de manière similaire. Cependant, cela garantit la sécurité des threads lorsque vous accumulez les valeurs dans la liste. Dans la documentation de 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 que le flux ne soit pas ordonné ou que le collecteur ne soit pas ordonné, une réduction simultanée sera effectuée. Lors de l'exécution en parallèle, plusieurs résultats intermédiaires peuvent être instanciés, remplis et fusionnés de manière à maintenir l'isolation des structures de données modifiables. Par conséquent, même lorsqu'il est exécuté en parallèle avec des structures de données non sécurisées pour les threads (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 avez des valeurs immuables telles que ints, doubles, Strings, la réduction normale fonctionne parfaitement. Cependant, si vous devez reduce vos valeurs dans une List (structure de données modifiable), vous devez utiliser la réduction mutable avec la méthode collect.

23
george

Que le flux soit un <- b <- c <- d

En réduction,

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

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

En collection,

votre collecteur aura une sorte de structure de collecte K.

K consomme a. 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.

8
Yan Ng

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

Par exemple, si vous souhaitez lire certaines données d'un fichier, les traiter et les placer dans une base de données, vous pouvez vous retrouver avec un code de flux Java similaire à celui-ci:

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

Dans ce cas, nous utilisons collect() pour forcer Java à transmettre des données en continu et à le sauvegarder dans la base de données. Sans collect(), les données ne sont jamais lues ni 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 suffisante ou si la taille du segment de mémoire est suffisamment basse. 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, ce qui fait exploser le tas.

Cependant, si vous remplacez collect() par reduce(), le problème ne se posera plus car ce dernier réduira et supprimera toutes les données qui les ont traversées.

Dans l'exemple présenté, remplacez simplement collect() par quelque chose avec reduce:

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

Vous n'avez même pas besoin de faire en sorte que le calcul dépende de la result, car Java n'est pas un langage pur FP (programmation fonctionnelle) et ne peut pas optimiser les données n'est pas utilisé au bas du flux en raison des effets secondaires possibles.

2
averasko

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();

System.out.println (somme);

Voici le résultat de l'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 dans le flux, le second paramètre est la valeur de calcul actuelle dans le flux, il additionne la première valeur et la valeur actuelle en tant que première valeur dans la prochaine calcul.

1
JetQin

Selon la documentation

Les collecteurs de reduction () sont plus utiles lorsqu'ils sont utilisés dans une réduction à plusieurs niveaux, en aval de groupingBy ou de partitioningBy. Pour effectuer une réduction simple sur un flux, utilisez plutôt Stream.reduce (BinaryOperator).

Donc, fondamentalement, vous utiliseriez reducing() uniquement lorsque 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 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 pourrait 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'élément, ce qui est inefficace. Il serait plus efficace pour vous de mettre à jour une collection existante. Vous pouvez le faire avec la méthode Stream.collect, décrite dans la section suivante ...

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

0
rogerdpack