Utilisation de la portée lexicale et des fermetures
De nombreux développeurs ont ce malentendu, pensant que l'utilisation d'expressions lambda entraînera une redondance du code et réduira la qualité du code. Au contraire, quelle que soit la complexité du code, nous ne ferons aucun compromis sur la qualité du code par souci de simplicité, comme nous le verrons ci-dessous.
Nous avons pu réutiliser l'expression lambda dans l'exemple précédent cependant, si nous faisons correspondre une autre lettre, le problème de redondance du code revient rapidement. Analysons d'abord ce problème plus en détail, puis utilisons la portée lexicale et les fermetures pour le résoudre.
Redondance causée par les expressions lambda
Filtrons les lettres commençant par N ou B de nos amis. En reprenant l'exemple ci-dessus, le code que nous écrivons pourrait ressembler à ceci :
Copiez le code comme suit :
final Predicate<String> startWithN = nom -> nom.startsWith("N");
final Predicate<String> startWithB = nom -> nom.startsWith("B");
décompte long finalFriendsStartN =
amis.stream()
.filter(startsWithN).count();
décompte long finalFriendsStartB =
amis.stream()
.filter(startsWithB).count();
Le premier prédicat détermine si le nom commence par N et le second détermine si le nom commence par B. Nous transmettons respectivement ces deux instances à deux appels de méthode de filtrage. Cela semble raisonnable, mais les deux prédicats sont redondants, ce ne sont que des lettres différentes dans le chèque. Voyons comment éviter cette redondance.
Utilisez la portée lexicale pour éviter la redondance
Dans la première solution, on peut extraire les lettres comme paramètres de la fonction et passer cette fonction à la méthode filter. C'est une bonne méthode, mais le filtre n'est pas accepté par toutes les fonctions. Il n'accepte que les fonctions avec un seul paramètre. Ce paramètre correspond à l'élément de la collection et renvoie une valeur booléenne. Il espère que ce qui est transmis est un prédicat.
Nous espérons qu'il existe un endroit où cette lettre pourra être mise en cache jusqu'à ce que le paramètre soit passé (dans ce cas, le paramètre name). Créons une nouvelle fonction comme celle-ci.
Copiez le code comme suit :
public static Predicate<String> checkIfStartsWith(final String letter) {
return name -> name.startsWith(letter);
}
Nous avons défini une fonction statique checkIfStartsWith, qui reçoit un paramètre String et renvoie un objet Predicate, qui peut être transmis à la méthode filter pour une utilisation ultérieure. Contrairement aux fonctions d’ordre supérieur que nous avons vues précédemment, qui prennent des fonctions comme paramètres, cette méthode renvoie une fonction. Mais c’est aussi une fonction d’ordre supérieur, que nous avons déjà évoquée dans Evolution, not change, page 12.
L'objet Predicate renvoyé par la méthode checkIfStartsWith est quelque peu différent des autres expressions lambda. Dans l'instruction return name -> name.startsWith(letter), nous savons exactement ce qu'est le nom, c'est le paramètre passé dans l'expression lambda. Mais quelle est exactement la lettre variable ? C'est en dehors du domaine de la fonction anonyme. Java trouve le domaine où l'expression lambda est définie et découvre la lettre de la variable. C'est ce qu'on appelle la portée lexicale. La portée lexicale est une chose très utile, elle nous permet de mettre en cache une variable dans une portée pour une utilisation ultérieure dans un autre contexte. Étant donné que cette expression lambda utilise des variables dans sa portée, cette situation est également appelée fermeture. Concernant les restrictions d'accès de portée lexicale, pouvez-vous lire les restrictions de portée lexicale à la page 31 ?
Y a-t-il des restrictions sur la portée lexicale ?
Dans une expression lambda, nous ne pouvons accéder qu'aux types finaux dans sa portée ou aux variables réellement locales du type final.
L'expression lambda peut être appelée immédiatement, retardée ou à partir d'un autre thread. Afin d'éviter les conflits de race, les variables locales du domaine auquel nous accédons ne peuvent pas être modifiées une fois initialisées. Toute opération de modification entraînera une exception de compilation.
Le marquer comme final résout ce problème, mais Java ne nous oblige pas à le marquer de cette façon. En fait, Java examine deux choses. La première est que la variable accessible doit être initialisée dans la méthode dans laquelle elle est définie, et avant que l'expression lambda ne soit définie. Deuxièmement, les valeurs de ces variables ne peuvent pas être modifiées, c'est-à-dire qu'elles sont en fait de type final, bien qu'elles ne soient pas marquées comme telles.
Les expressions lambda sans état sont des constantes d'exécution, tandis que celles qui utilisent des variables locales ont une surcharge de calcul supplémentaire.
Lors de l'appel de la méthode filter, nous pouvons utiliser l'expression lambda renvoyée par la méthode checkIfStartsWith, comme ceci :
Copiez le code comme suit :
décompte long finalFriendsStartN =
amis.stream() .filter(checkIfStartsWith("N")).count();
décompte long finalFriendsStartB = friends.stream()
.filter(checkIfStartsWith("B")).count();
Avant d'appeler la méthode filter, nous avons d'abord appelé la méthode checkIfStartsWith() et transmis les lettres souhaitées. Cet appel renvoie rapidement une expression lambda, que nous transmettons ensuite à la méthode filter.
En créant une fonction d'ordre supérieur (checkIfStartsWith dans ce cas) et en utilisant la portée lexicale, nous avons réussi à supprimer la redondance du code. Nous n'avons plus besoin de déterminer à plusieurs reprises si le nom commence par une certaine lettre.
Refactoriser, réduire la portée
Dans l'exemple précédent, nous avons utilisé une méthode statique, mais nous ne voulons pas utiliser la méthode statique pour mettre en cache les variables, ce qui gâcherait notre code. Il est préférable de limiter la portée de cette fonction à l'endroit où elle est utilisée. Nous pouvons utiliser une interface Function pour y parvenir.
Copiez le code comme suit :
final Function<String, Predicate<String>> startWithLetter = (Lettre de chaîne) -> {
Predicate<String> checkStarts = (Nom de la chaîne) -> name.startsWith(letter);
retourner checkStarts ;
Cette expression lambda remplace la méthode statique d'origine. Elle peut être placée dans une fonction et définie avant d'être nécessaire. La variable startWithLetter fait référence à une fonction dont le paramètre d'entrée est String et dont le paramètre de sortie est Predicate.
Par rapport à l'utilisation de la méthode statique, cette version est beaucoup plus simple, mais nous pouvons continuer à la refactoriser pour la rendre plus concise. D'un point de vue pratique, cette fonction est la même que la méthode statique précédente ; elles reçoivent toutes deux une chaîne et renvoient un prédicat. Au lieu de déclarer explicitement un prédicat, nous le remplaçons entièrement par une expression lambda.
Copiez le code comme suit :
final Function<String, Predicate<String>> startupsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);
Nous nous sommes débarrassés du désordre, mais nous pouvons également supprimer la déclaration de type pour la rendre plus concise, et le compilateur Java effectuera une déduction de type en fonction du contexte. Jetons un coup d'œil à la version améliorée.
Copiez le code comme suit :
final Function<String, Predicate<String>> startWithLetter =
lettre -> nom -> nom.startsWith(lettre);
Il faut un certain effort pour s'adapter à cette syntaxe concise. Si cela vous aveugle, cherchez d’abord ailleurs. Nous avons terminé la refactorisation du code et pouvons maintenant l'utiliser pour remplacer la méthode checkIfStartsWith() d'origine, comme ceci :
Copiez le code comme suit :
décompte long finalFriendsStartN = friends.stream()
.filter(startsWithLetter.apply("N")).count();
décompte long finalFriendsStartB = friends.stream()
.filter(startsWithLetter.apply("B")).count();
Dans cette section, nous utilisons des fonctions d’ordre supérieur. Nous avons vu comment créer des fonctions dans des fonctions si l'on passe une fonction à une autre fonction, et comment renvoyer une fonction à partir d'une fonction. Ces exemples démontrent tous la simplicité et la réutilisabilité apportées par les expressions lambda.
Dans cette section, nous avons pleinement utilisé les fonctions de Fonction et de Prédicat, mais examinons la différence entre elles. Predicate accepte un paramètre de type T et renvoie une valeur booléenne pour représenter le vrai ou le faux de sa condition de jugement correspondante. Lorsque nous devons porter des jugements conditionnels, nous pouvons utiliser Predicateg pour le compléter. Les méthodes telles que filter qui filtrent les éléments reçoivent Predicate en tant que paramètre. Funciton représente une fonction dont les paramètres d'entrée sont des variables de type T et renvoie un résultat de type R. C'est plus général que Predicate qui ne peut renvoyer que des booléens. Tant que l'entrée est convertie en sortie, nous pouvons utiliser Function, il est donc raisonnable que map utilise Function comme paramètre.
Comme vous pouvez le constater, la sélection d’éléments dans une collection est très simple. Ci-dessous, nous expliquerons comment sélectionner un seul élément de la collection.