Examinons la signification de "stream" et "collect" dans l'API Stream de Java.

introduction

Il y a déjà longtemps, Java a introduit une technique de programmation fonctionnelle appelée Stream API. La programmation fonctionnelle est déjà répandue et Java est assez tardif, mais elle est toujours introduite car elle a l'avantage de pouvoir écrire du code hautement lisible efficacement s'il est bien utilisé. penser. (Référence: "[Programmation fonctionnelle pour les programmeurs médiocres](https://anopara.net/2016/04/14/%E5%B9%B3%E5%87%A1%E3%81%AA%E3%" 83% 97% E3% 83% AD% E3% 82% B0% E3% 83% A9% E3% 83% 9E% E3% 81% AB% E3% 81% A8% E3% 81% A3% E3% 81% A6% E3% 81% AE% E9% 96% A2% E6% 95% B0% E5% 9E% 8B% E3% 83% 97% E3% 83% AD% E3% 82% B0% E3% 83% A9% E3% 83% 9F% E3% 83% B3 /) ")

Cependant, l'autre jour, dans une conférence, j'ai entendu dire que "l'opération de collecte de Java (Stream API) semble plus puissante que les langages de fonction purs tels que Haskell, et je ne veux pas l'utiliser." Plus précisément, pour les langages fonctionnels, `` list.map (...) '' suffit, mais pour Java, un par un.

list.stream().map(/* */).collect(Collectors.toList())


```comme```stream()```Je pensais que ce serait un style d'écriture redondant car il fallait le mettre entre les deux.

 Je préfère utiliser l'API Stream car c'est mieux que rien, mais je me suis demandé s'il y avait certains aspects que les gens qui aiment les langages fonctionnels n'accepteront pas.

 Alors pourquoi Java n'adopte pas un style d'écriture simple comme `` `` list.map (...) '', mais appelle `` `` stream () '' un par un et le convertit en un autre type appelé Stream. Ensuite, avez-vous essayé de convertir à nouveau avec `` `` collect '' ''? Java a une culture de conception de langage minutieuse, et il doit y avoir une raison pour les avantages et les inconvénients du résultat. Je pense qu'il y a deux raisons principales à cela.

 1. Évaluation des retards
 2. Contraintes orientées objet

 Ci-dessous, je donnerai mes réflexions en détail.

## Qu'est-ce que l'évaluation des retards?
 En règle générale, lors de l'exécution d'une opération de collecte, il est inutile de recréer la collection une par une dans le processus, et il y a des problèmes de performances.
 Pour éviter cela, même si le code peut sembler être un changement progressif dans la collection, il doit en fait être généré en masse à la fin. Cette méthode de calcul, dans laquelle la valeur n'est pas calculée tant qu'elle n'est pas nécessaire, est appelée ** évaluation du retard **.
 Par exemple

```java
List<String> list = Arrays.asList("foo", "bar", "hoge", "foo", "fuga");
list.stream()
  .filter(s -> s.startsWith("f"))
  .map(s -> s.toUpperCase())
  .collect(Collectors.toSet()); // ["FOO", "FUGA"]

Comme ça

--Extraire la liste des chaînes commençant par "f" --Convertir les chaînes en majuscules --Supprimer les doublons (convertir en Set)

Lors de l'exécution de l'opération de collecte, une nouvelle collection avec 3 éléments ne sera pas créée lorsque filtre '' est appelé. La collection réelle est générée lorsque collect (Collectors.toSet ()) '' est appelé en dernier.

Le synonyme d'évaluation différée est ** évaluation régulière **. C'est une méthode pour calculer à ce stade même si la valeur n'est pas nécessaire. Ceci est généralement plus courant.

Inconvénients de l'évaluation du retard

Bien qu'il existe des avantages, il existe des pièges inattendus si vous ne les utilisez pas avec prudence. Ce qui suit est un exemple (bien que ce ne soit pas très préférable) que l'évaluation du retard peut provoquer une différence entre l'apparence et la reconnaissance du résultat d'exécution réel.

//Définition des données d'entrée / sortie
List<String> input = Arrays.asList("foo", "bar", "hoge", "foo", "fuga");
List<String> copy1 = new ArrayList<>();
List<String> copy2 = new ArrayList<>();

//Lancer l'opération de collecte.Exécuter le filtre
Stream<String> stream = input.stream()
    .filter(s -> {
        copy1.add(s);
        return s.startsWith("f");
    });
System.out.println(copy1.size()); //À ce stade, l'opération de filtrage n'est pas réellement évaluée, donc copy1 est laissé vide et 0 est sorti.
System.out.println(copy2.size()); //Bien sûr, copy2 reste vide, donc 0 est affiché

//Puis exécutez la carte de l'opération de collecte
stream = stream
    .map(s -> {
        copy2.add(s);
        return s.toUpperCase();
    });
System.out.println(copy1.size());  //À ce stade, l'opération de filtrage n'a pas encore été évaluée, donc 0 est émis.
System.out.println(copy2.size()); //De même, l'opération de carte n'est pas évaluée, donc 0 est affiché.

stream.collect(Collectors.toList());
System.out.println(copy1.size()); // stream.5 est émis car le filtre est finalement évalué par collect
System.out.println(copy2.size()); //De même, l'opération de carte est évaluée, donc 3 est généré.

À première vue, le code ci-dessus semble augmenter la taille de copy1, copy2 lors de l'appel de filter, map, En fait, la taille de «copy1» et «copy2» augmente lorsque «stream.collect» est appelé. De cette manière, s'il y a un écart entre l'apparence et le moment réel de l'évaluation, il y a un risque qu'il soit difficile de déboguer et d'identifier la cause en cas de problème.

Comment équilibrer

Une évaluation retardée risque d'intégrer des bogues complexes si elle est mal utilisée. Cependant, si vous n'utilisez pas du tout l'évaluation différée, vous courez le risque de gaspiller les collections et de ralentir les performances.

Dans le cas de Java, étant donné que la possibilité de traiter une grande quantité de données sur le back-end est généralement envisagée, nous voulons éviter ce dernier risque, nous devons donc introduire une évaluation du retard. De plus, il serait préférable d'avoir une évaluation différée qui soit naturellement (?) Pour que l'évaluation régulière ne soit pas utilisée involontairement.

Cependant, il est risqué d'autoriser l'application des évaluations de retard à un large éventail de fonctionnalités standard Java. Par conséquent, je pense qu'il est raisonnable de limiter l'évaluation du retard à un type spécifique afin que l'évaluation du retard ne soit pas utilisée dans d'autres types.

Apparence de "Stream"

[Stream](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%A0_(%E3%) 83% 97% E3% 83% AD% E3% 82% B0% E3% 83% A9% E3% 83% 9F% E3% 83% B3% E3% 82% B0)))

Un flux est un type de données abstrait qui considère les données comme une «chose qui coule», entre les données qui entrent et gère les données qui s'écoulent comme une sortie.

Comme mentionné précédemment, le seul type qui peut effectuer une évaluation de retard est nommé "** Stream **". Et le nom de l'API d'opération de collecte est "Stream API" tel quel, le fait est que si vous voulez faire l'opération de collecte, vous pouvez utiliser stream () comme son nom l'indique.

Ce faisant, nous pensons que nous avons essayé de forcer les évaluations retardées et d'éviter le risque de dégradation des performances dû aux évaluations formelles.

Contraintes orientées objet

Une autre raison via stream est la contrainte orientée objet. (Strictement parlant, cela correspond à la contrainte de la manipulation des types plutôt qu'à celle orientée objet, mais comme les types fonctionnels et orientés objet sont souvent contrastés, le terme «orienté objet» est utilisé ici.) Supposons que vous ayez défini une méthode de carte par défaut pour le type List.

interface List<E> {
    default <R> List<R> map(Function<? super E, ? extends R> mapper) {
        List<R> result = new ArrayList<>();
        for (E elem : this) {
            result.add(mapper.apply(elem));
        }
        return result;
    }
}

Si vous faites cela, vous pouvez convertir le type de liste comme list.map (...) pour le moment. Si vous implémentez de la même manière des méthodes telles que le filtre et d'autres types de collection, vous pouvez convertir des collections de manière concise sans passer par le flux.

Cependant, cette méthode présente de sérieux inconvénients. Il s'agit d'un type de collection autre que la bibliothèque standard.

Par exemple, supposons qu'un développeur crée une MyList qui implémente l'interface List et ajoute une méthode unique, doSomething. Ici, si le type MyList est converti en mappage par la méthode ci-dessus, ce sera un autre type de liste après la conversion et doSomething ne peut pas être appelé.

MyList<> mylist = new MyList<>();
//Omission
mylist.doSomething(); //OK
myList.map(x -> new AnotherType(x)).doSomething(); //Erreur de compilation

Ce sera un défi lors de l'intégration de la programmation fonctionnelle dans des langages orientés objet. Cependant, je ne vois pas vraiment de tels cas, donc je n'ai pas à m'en soucier, mais c'est probablement inacceptable en raison de la nature du langage Java.

En ce qui concerne Scala, cette difficulté a été surmontée avec une résolution de type implicite. Il est décrit dans le livre présenté dans le Supplément A ci-dessous, veuillez donc y jeter un œil si vous êtes intéressé.

Apparition de "Collector"

Pour les raisons ci-dessus, lorsque vous démarrez l'opération de conversion d'une collection, vous devez régénérer quelque chose de différent de la collection d'origine, et il est de la responsabilité de l'appelant, et non de la bibliothèque, de spécifier la collection. "** Collector **" est responsable de cela, et Stream.collect spécifie le type de collection vers lequel l'appelant doit convertir. Le code source ci-dessous est l'implémentation de Collectors.toList.

public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

Pour le type de collection MyList créé par vous-même, si vous préparez une méthode pour générer une instance Collector de la même manière, il sera possible de convertir de MyList en MyList. Cela permet aux types de collection créés avant l'introduction de l'API Stream d'être utilisés sans aucune modification majeure sur l'API Stream.

À propos, la raison de ne pas créer de méthode toList pour le type Stream

Bien qu'il soit préférable de consolider les conversions de collection en type Collector, le type Liste etc. apparaît fréquemment dans la vie quotidienne. À tout le moins, je pense qu'il est correct d'écrire de manière aussi concise que «stream.toList ()» au lieu de «stream.collect (Collectors.toList ())». Je pense que la raison de ne pas faire cela est probablement une dépendance de type. Le fait est qu'il est essentiel de faire référence au type Collection au type Stream, mais je pense que la raison en est que référencer le type Collection à partir du type Stream n'est pas préférable en tant que conception de type car il s'agit d'une référence mutuelle.

Chantons la magie et utilisons-la en toute sécurité

Comme mentionné ci-dessus, le résultat de la prise en compte de divers équilibres et de la cohérence est list.stream (). Map (/ * * /). Collect (Collectors.toList ()) '', qui est une opération de collecte qui peut être considérée comme redondante. Je pense qu'il s'est installé sous la forme de.

Dans un sens, je pense que c'est une ** conclusion très proche de Java **.

Il semble qu'il existe un projet mystérieux dans le monde selon lequel vous ne devriez pas utiliser l'API Stream car c'est dangereux lorsque vous utilisez Java, mais comme il est conçu avec une telle sécurité à l'esprit, il est difficile de l'utiliser normalement. Vous n'êtes pas obligé d'y aller. Si vous chantez stream, collect selon la norme, aucun problème ne se produira sauf si quelque chose ne va pas.

en conclusion

Sauf pour ceux qui sont particuliers sur les langages purement fonctionnels, je pense que les descriptions redondantes sont bien tolérées. Si vous pouvez bien utiliser la programmation fonctionnelle, vous serez en mesure d'écrire efficacement du code hautement lisible. Si vous ne l'avez pas encore utilisé, essayez-le. (Référence: Introduction to Java Stream API)

Supplément A. Langues autres que Java

Je ne suis pas très familier avec cela, mais je décrirai ce que d'autres langues offrent pour référence.

C# LINQ en C # est une évaluation paresseuse comme Java. Contrairement à Java, il est beaucoup plus concis et pratique que Java car vous n'avez pas besoin d'appeler stream () pour le démarrer, et vous n'avez souvent besoin d'appeler ToList, par exemple, que lors de la collecte. (Référence: "[Notes diverses] LINQ et évaluation des délais") (Référence: "C # er sait naturellement!? Avantages et inconvénients de l'évaluation des délais LINQ")

Scala Scala est également un langage fonctionnel, et il est possible d'utiliser correctement l'évaluation des retards et l'évaluation régulière. Par exemple, list.map (...) '' peut être utilisé pour convertir en une autre collection par une évaluation régulière. Il est également possible de convertir en une autre collection par évaluation différée sous forme de vue, forcer comme `` list.view.map (...) .filter (...) .force```. (Référence: "Créer un générateur avec Scala et évaluer le délai")

En outre, il semble qu'il fut un temps où il était difficile de faire la distinction entre une évaluation régulière et une évaluation retardée et cela a causé de la confusion, mais à un moment donné, la frontière a été clarifiée.

Il semble que seuls ces deux types aient été organisés comme cibles pour l'évaluation des retards. Quant à Scala, le livre "Scala Scalable Programming" contient beaucoup de détails terrifiants, donc si vous êtes intéressé, n'hésitez pas à nous contacter. Jetez un œil à ceci.

JavaScript Il n'y a pas d'évaluation des délais dans la norme JavaScript. Array.prototype possède des API d'opérations de collecte standard telles que map et filter, mais elles sont toutes classées. Cela est probablement dû au fait que le JavaScript utilisé côté client ne gère pas de grandes quantités de données, de sorte que l'évaluation des délais n'est pas requise en tant qu'équipement standard.

Haskell Haskell est également un langage purement fonctionnel, et il semble que la couleur des cheveux soit différente de celles énumérées ci-dessus. Alors que les langues normales sont basées sur une évaluation régulière, Haskell est basée sur une évaluation différée. Par conséquent, il semble qu'il n'y ait rien à voir avec l'équilibre entre l'évaluation régulière et l'évaluation différée qui nous préoccupe dans cet article. (Référence: "Évaluation régulière et évaluation des délais (détails)")

Autre que ce qui précède (PHP, Ruby, Python, etc ...)

Je vais enquêter bientôt.

Supplément B.Option d'utiliser une autre bibliothèque

En plus du standard Java, il existe une bibliothèque de manipulation de collections appelée collections Eclipse. Cela vous permet de décrire clairement ce qui est redondant avec l'API Stream. (Référence: "J'ai touché aux collections Eclipse") (Référence: "Eclipse Collections Cheet Sheet")

En outre, ImmutableList, qui a une interface de liste immuable, est une bibliothèque avec une couleur plus profonde de la méthode fonctionnelle. Si vous souhaitez gérer des opérations de collecte plus fonctionnelles que l'API Stream, je pense que la mise en œuvre est une option.

Cependant, si vous souhaitez remplacer complètement l'API Stream par des collections Eclipse, vous devrez faire beaucoup de travail. Lors de son introduction, l'histoire du site où l'introduction a été effectivement réalisée "[Framework support for instilling Eclipse Collections in the field](https://speakerdeck.com/jflute/how-unext-took-in-eclipse- collections-in-fw) »sera utile.

Recommended Posts

Examinons la signification de "stream" et "collect" dans l'API Stream de Java.
Le concept de conception de l'API de date et d'heure de Java est intéressant
Essayez d'utiliser l'API Stream en Java
Remarques sur l'API Stream et SQL de Java
Vers la compréhension de la carte et de la flatmap dans Stream (1)
Résumé de la conversion mutuelle entre les méthodes Groovy par défaut de Groovy et l'API Stream de Java
[Pour les débutants] DI ~ Les bases de DI et DI au printemps ~
Utiliser des expressions Java lambda en dehors de l'API Stream
J'étais étrangement accro à l'utilisation de l'API Stream de Java avec Scala
Créer plus d'onglets et de fragments dans le fragment de BottomNavigationView
API Java Stream en 5 minutes
Memo of JSUG Study Group 2018 Partie 2-Efforts pour les spécifications de travail à l'ère du printemps et de l'API-
Implémentons la condition que la circonférence et l'intérieur de la forme Ougi soient inclus dans Java [Partie 2]
Implémentons la condition que la circonférence et l'intérieur de la forme Ougi soient inclus dans Java [Partie 1]
Explorons un peu plus l'API Stream, dont je comprends que c'est une réécriture.
[Java] Obtenez les dates des derniers lundi et dimanche dans l'ordre
La validation de printemps était importante dans l'ordre de Form et BindingResult
Créons une application TODO en Java 5 Changer l'affichage de TODO
Comportement détaillé de l'opération intermédiaire avec état de l'API Stream et de l'opération de terminaison de court-circuit
Tirez parti de l'un ou l'autre pour la gestion des exceptions individuelles dans l'API Java Stream
Prise en main des opérateurs logiques utilisant Doma tels que AND et OR dans la clause WHERE de l'API Criteria
Ceci et cela de JDK
Analyser l'analyse syntaxique de l'API COTOHA en Java
Ordre de traitement dans le programme
[Rails] Différence de comportement entre delegate et has_many-through dans le cas de one-to-one-to-many
L'histoire de l'oubli de fermer un fichier en Java et de l'échec
Définissez le nombre de secondes d'avance et de retour rapides dans ExoPlayer
Recevez la réponse de l'API au format xml et obtenez le DOM de la balise spécifiée
Confirmation et refactoring du flux de la requête au contrôleur dans [httpclient]
[Introduction à Ruby] À propos du rôle de true et break in the while statement
Comment convertir un tableau de chaînes en un tableau d'objets avec l'API Stream
(Déterminez en 1 minute) Comment utiliser vide?, Vide? Et présent?
Ceci et cela de la mise en œuvre du jugement en temps réel des dates en Java
Comment modifier le nombre maximum et maximum de données POST dans Spark
Calculez le pourcentage de "bon", "normal" et "mauvais" dans le questionnaire à l'aide de SQL
Trouvez le maximum et le minimum des cinq nombres saisis en Java
À vous à qui on a dit "Ne pas utiliser Stream API" dans le champ
Afficher la structure tridimensionnelle de l'ADN et des protéines dans Ruby-dans le cas de GR.rb