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.
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.
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.
[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.
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é.
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.
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.
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.
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)
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)")
Je vais enquêter bientôt.
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