Dans un langage de programmation, une fermeture ou clôture (en anglais : closure) est une fonction accompagnée de son environnement lexical. L’environnement lexical d’une fonction est l’ensemble des variables non locales qu’elle a capturé, soit par valeur (c’est-à-dire par copie des valeurs des variables), soit par référence (c’est-à-dire par copie des adresses mémoires des variables)[1]. Une fermeture est donc créée, entre autres, lorsqu’une fonction est définie dans le corps d’une autre fonction et utilise des paramètres ou des variables locales de cette dernière.
Java ne s’appuie pas sur les closures comme le font les langages de programmation fonctionnels et JavaScript, mais ce concept reste primordiale dans la mise en œuvre des lambda expressions en Java 8.
Supposons que nous voulions créer un thread simple qui n’imprime que quelque chose sur la console:
Et si on changeait la valeur answer pendant l’exécution du thread?
Dans cet article, je voudrais répondre à cette question, en discutant des limitations des expressions lambda Java et des conséquences qui en découlent.
La réponse courte est que Java implémente des closures, mais il y a des limites quand on les compare avec ce qui est possible dans d’autres langages. D’un autre côté, ces limitations peuvent être considérées comme négligeables à mon avis.
Pour soutenir cette affirmation, je vais montrer comment les closures jouent un rôle essentiel dans un langage célèbre comme JavaScript.
D’où proviennent les expressions lambda Java 8?
Dans le passé, une méthode compacte pour implémenter l’exemple ci-dessus était de créer une instance d’une nouvelle classe anonyme Runnable, comme suit:
Depuis Java 8, l’exemple précédent pourrait être écrit en utilisant une expression lambda.
Maintenant, nous savons tous que les expressions lambda Java 8 ne visent pas seulement à réduire la verbosité de votre code; ils ont aussi beaucoup d’autres nouvelles fonctionnalités. En outre, il existe des différences entre la mise en œuvre des classes anonymes et des expressions lambda.
Mais le point principal que je voudrais souligner ici est que, compte tenu de leur interaction avec la portée englobante, nous pouvons les considérer comme un moyen compact de créer des classes d’interfaces anonymes, comme Runnable, Callable, Function, Predicate, etc. . En fait, l’interaction entre une expression lambda et sa portée englobante reste sensiblement la même (parexemple, différences sur la sémantique de du mot-clé this).
Java 8 Limitations Lambda
Les expressions Lambda (ainsi que les classes anonymes) en Java ne peuvent accéder qu’aux variables finales (ou effectivement final) de la portée englobante.
Par exemple, considérez l’exemple suivant:
Cela ne compile pas puisque l’incrémentation de myVar l’empêche d’être effectivement final.
JavaScript et ses fonctions
Fonctions et expressions lambda en JavaScript utilisent le concept de closures:
«Une fermeture est un type particulier d’objet qui combine deux choses: une fonction et l’environnement dans lequel cette fonction a été créée. L’environnement est constitué de toutes les variables locales qui étaient présentes dans le champ d’application au moment de la création de la closure» – – MDN
En fait, l’exemple précédent fonctionne bien dans JavaScript.
La fonction lambda de cet exemple utilise la version modifiée de myVar.
En pratique, en JavaScript, une nouvelle fonction maintient un pointeur vers la portée englobante où elle a été définie. Ce mécanisme fondamental permet la création de closures qui enregistre l’emplacement de stockage des variables libres – celles-ci peuvent être modifiées par la fonction elle-même ainsi que par d’autres.
Est-ce que Java crée des closures?
Java n’enregistre que la valeur des variables libres pour les utiliser dans les expressions lambda. Même s’il y a un incrément de myVar, la fonction lambda renvoie toujours 42. Le compilateur évite la création de ces scénarios incohérents, en limitant le type de variables utilisables à l’intérieur des expressions lambda (et des classes anonymes) à des variables final.
Malgré cette limitation, nous pouvons affirmer que Java 8 implémente des closures. En fait, les closures, dans leur acception plus théorique, ne captent que la valeur des variables libres. Dans les langages fonctionnels purs, c’est la seule chose qui devrait être autorisée, en conservant la propriété de transparence référentielle.
Par la suite, quelques langages fonctionnels, ainsi que des langages tels que Javascript, ont permis de saisir les emplacements de stockage des variables libres. Cela permet la possibilité d’introduire des effets secondaires ou encore communément appelés effets de bord.
Ainsi, nous pourrions affirmer qu’avec les closures de JavaScript, nous pouvons faire plus. Mais, pour moi la question est: en quoi est ce que ces effets secondaires aident vraiment JavaScript? Sont – ils vraiment importants?
Effets secondaires et JavaScript
Pour mieux comprendre le concept des closures, considérez maintenant le code JavaScript suivant (Je sais qu’ en JavaScript, cela peut être fait de manière très compacte, mais je veux que le code ressemble à Java pour pouvoir effectuer une comparaison):
Chaque fois que createCounter est appelé, il crée une Map avec deux nouvelles fonctions lambda qui, respectivement, retournent et incrémentent la valeur de la variable qui a été définie dans la portée englobante.
En d’autres termes, la première fonction a des effets secondaires qui changent le résultat de l’autre.
Un fait important à remarquer ici est que la portée de createCounter existe toujours après sa terminaison, et est utilisée simultanément par les deux fonctions lambda.
Effets secondaires et Java
Maintenant essayons de faire la même chose en Java:
Ce code ne compile pas car la deuxième fonction lambda essaie de changer la variable count.
Java stocke des variables de fonction (par exemple, count) dans la pile; ceux-ci sont supprimés avec la fin de createCounter. Les lambdas créés utilisent des versions copiées de count. Si le compilateur autorisait la second lambda à changer sa version copiée de count, cela serait assez déroutant.
Pour prendre en charge ce type de closure, Java (JVM) doit enregistrer les portées englobantes dans le tas afin de leur permettre de survivre après la fin de l’exécution de la fonction.
Closures Java utilisant des objets mutables
Comme nous l’avons vu, la valeur d’une variable utilisée est copiée dans l’expression lambda (ou classe anonyme). Mais, et si nous utilisions des objets? Dans ce cas, seule la référence serait copiée, et nous pourrions examiner les choses un peu différemment.
Nous pourrions presque imiter le comportement des closures de JavaScript de la façon suivante:
En effet, ce n’est pas vraiment utile, et c’est vraiment pas du tout élégant.
Fermetures en tant que mécanisme de création d’objets
« Quand apprendras-tu? Les closures sont l’objet d’un pauvre. » — Anton
Les closures sont utilisées par JavaScript comme mécanisme fondamental pour créer des instances de classe: objets. C’est pourquoi, en JavaScript, une fonction comme MyCounter est appelée « fonction constructeur ».
Java lui a déjà des classes, et nous pouvons créer des objets de manière beaucoup plus élégante.
Dans l’exemple précédent, nous n’avons pas besoin d’une closures. Cette « fonction d’usine » est essentiellement un exemple étrange d’une définition de classe. En Java, nous pouvons simplement définir une classe comme celle-ci:
Modifier les variables libres est une mauvaise pratique
Les fonctions Lambda qui modifient des variables libres (c’est-à-dire tout objet qui a été défini en dehors de la fonction lambda) peuvent générer de la confusion. Les effets secondaires sur d’autres fonctions pourraient entraîner des erreurs indésirables.
Ceci est typique des développeurs de langages plus anciens qui ne comprennent pas pourquoi JavaScript produit des comportements aléatoires inexplicables. Dans les langages fonctionnelles, cette pratique est généralement interdite, et quand elle ne l’est pas, découragée.
Considérez que vous utilisez un paradigme parallèle, par exemple dans Spark:
Conclusions
Nous avons vu une très brève introduction des expressions lambda Java 8. Nous nous sommes concentrés sur les différences entre les classes anonymes et les expressions lambda. Après cela, nous avons mieux vu le concept de closures, en regardant comment elles sont implémentées en JavaScript. De plus, nous avons vu comment les closures de JavaScript ne peuvent pas être directement utilisées dans Java 8 et comment elles peuvent être simulées par des références d’objet.
Nous avons également découvert que Java a un support limité pour les closures lorsque nous les comparons avec des langages tels que JavaScript.
Néanmoins, nous avons vu comment ces limitations ne sont pas vraiment importantes. En fait, les closures sont utilisées en JavaScript comme mécanisme fondamental pour définir des classes et créer des objets, et nous savons tous que Java n’a pas de problèmes à ce niveau.