- C'est pour les personnes qui ont lu la première partie de cet article ;
- C'est pour les personnes qui pensent déjà bien connaître Java Core, mais qui n'ont aucune idée des expressions lambda en Java. Ou peut-être ont-ils entendu parler des expressions lambda, mais les détails manquent.
- C'est pour les personnes qui ont une certaine compréhension des expressions lambda, mais qui sont toujours découragées par elles et peu habituées à les utiliser.
Accès aux variables externes
Ce code compile-t-il avec une classe anonyme ?
int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
Non. La counter
variable doit être final
. Ou sinon final
, alors au moins il ne peut pas changer sa valeur. Le même principe s'applique aux expressions lambda. Ils peuvent accéder à toutes les variables qu'ils peuvent "voir" depuis l'endroit où elles sont déclarées. Mais un lambda ne doit pas les modifier (leur attribuer une nouvelle valeur). Cependant, il existe un moyen de contourner cette restriction dans les classes anonymes. Créez simplement une variable de référence et modifiez l'état interne de l'objet. Ce faisant, la variable elle-même ne change pas (pointe vers le même objet) et peut être marquée en toute sécurité comme final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
Ici, notre counter
variable est une référence à un AtomicInteger
objet. Et la incrementAndGet()
méthode est utilisée pour changer l'état de cet objet. La valeur de la variable elle-même ne change pas pendant l'exécution du programme. Il pointe toujours sur le même objet, ce qui nous permet de déclarer la variable avec le mot clé final. Voici les mêmes exemples, mais avec des expressions lambda :
int counter = 0;
Runnable r = () -> counter++;
Cela ne compilera pas pour la même raison que la version avec une classe anonyme : counter
ne doit pas changer pendant l'exécution du programme. Mais tout va bien si nous le faisons comme ceci:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Cela s'applique également aux méthodes d'appel. Dans les expressions lambda, vous pouvez non seulement accéder à toutes les variables "visibles", mais également appeler toutes les méthodes accessibles.
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("I'm staticMethod(), and someone just called me!");
}
}
Bien qu'il staticMethod()
soit privé, il est accessible à l'intérieur de la main()
méthode, il peut donc également être appelé à l'intérieur d'un lambda créé dans la main
méthode.
Quand une expression lambda est-elle exécutée ?
Vous trouverez peut-être la question suivante trop simple, mais vous devriez quand même la poser : quand le code à l'intérieur de l'expression lambda sera-t-il exécuté ? Quand est-il créé ? Ou quand il s'appelle (ce qui n'est pas encore connu) ? Ceci est assez facile à vérifier.
System.out.println("Program start");
// All sorts of code here
// ...
System.out.println("Before lambda declaration");
Runnable runnable = () -> System.out.println("I'm a lambda!");
System.out.println("After lambda declaration");
// All sorts of other code here
// ...
System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
Sortie écran :
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Vous pouvez voir que l'expression lambda a été exécutée à la toute fin, après la création du thread et uniquement lorsque l'exécution du programme atteint la run()
méthode. Certainement pas lorsqu'il est déclaré. En déclarant une expression lambda, nous avons seulement créé un Runnable
objet et décrit le run()
comportement de sa méthode. La méthode elle-même est exécutée beaucoup plus tard.
Références de méthode ?
Les références de méthode ne sont pas directement liées aux lambdas, mais je pense qu'il est logique d'en dire quelques mots dans cet article. Supposons que nous ayons une expression lambda qui ne fait rien de spécial, mais appelle simplement une méthode.
x -> System.out.println(x)
Il reçoit quelques x
appels et seulement System.out.println()
, en passant x
. Dans ce cas, nous pouvons le remplacer par une référence à la méthode souhaitée. Comme ça:
System.out::println
C'est vrai - pas de parenthèses à la fin ! Voici un exemple plus complet :
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(x -> System.out.println(x));
Dans la dernière ligne, nous utilisons la forEach()
méthode, qui prend un objet qui implémente l' Consumer
interface. Encore une fois, il s'agit d'une interface fonctionnelle, n'ayant qu'une seule void accept(T t)
méthode. En conséquence, nous écrivons une expression lambda qui a un paramètre (parce qu'elle est typée dans l'interface elle-même, nous ne spécifions pas le type de paramètre ; nous indiquons seulement que nous l'appellerons x
). Dans le corps de l'expression lambda, nous écrivons le code qui sera exécuté lorsque la accept()
méthode sera appelée. Ici, nous affichons simplement ce qui s'est retrouvé dans la x
variable. Cette même forEach()
méthode parcourt tous les éléments de la collection et appelle la accept()
méthode sur l'implémentation de laConsumer
interface (notre lambda), en transmettant chaque élément de la collection. Comme je l'ai dit, nous pouvons remplacer une telle expression lambda (qui classe simplement une méthode différente) par une référence à la méthode souhaitée. Ensuite, notre code ressemblera à ceci :
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(System.out::println);
L'essentiel est que les paramètres des méthodes println()
et accept()
correspondent. Parce que la println()
méthode peut accepter n'importe quoi (elle est surchargée pour tous les types primitifs et tous les objets), au lieu d'expressions lambda, nous pouvons simplement passer une référence à la println()
méthode à forEach()
. Puis forEach()
prendra chaque élément de la collection et le passera directement à la println()
méthode. Pour toute personne rencontrant cela pour la première fois, veuillez noter que nous n'appelons pas System.out.println()
(avec des points entre les mots et avec des parenthèses à la fin). Au lieu de cela, nous passons une référence à cette méthode. Si nous écrivons ceci
strings.forEach(System.out.println());
nous aurons une erreur de compilation. Avant l'appel à forEach()
, Java voit qui System.out.println()
est appelé, il comprend donc que la valeur de retour est void
et essaiera de passer void
à forEach()
, qui attend à la place un Consumer
objet.
Syntaxe des références de méthode
C'est assez simple :-
Nous passons une référence à une méthode statique comme celle-ci :
ClassName::staticMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // Do something } }
-
Nous passons une référence à une méthode non statique en utilisant un objet existant, comme ceci :
objectName::instanceMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // Do something } }
-
Nous passons une référence à une méthode non statique en utilisant la classe qui l'implémente comme suit :
ClassName::methodName
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add (new User("John")); users.add(new User("Paul")); users.add(new User("George")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
-
Nous passons une référence à un constructeur comme celui-ci :
ClassName::new
Les références de méthode sont très pratiques lorsque vous disposez déjà d'une méthode qui fonctionnerait parfaitement comme rappel. Dans ce cas, au lieu d'écrire une expression lambda contenant le code de la méthode, ou d'écrire une expression lambda qui appelle simplement la méthode, nous lui passons simplement une référence. Et c'est tout.
Une distinction intéressante entre les classes anonymes et les expressions lambda
Dans une classe anonyme, lethis
mot clé pointe vers un objet de la classe anonyme. Mais si nous l'utilisons à l'intérieur d'un lambda, nous accédons à l'objet de la classe contenante. Celui où nous avons en fait écrit l'expression lambda. Cela se produit parce que les expressions lambda sont compilées dans une méthode privée de la classe dans laquelle elles sont écrites. Je ne recommanderais pas d'utiliser cette "fonctionnalité", car elle a un effet secondaire et contredit les principes de la programmation fonctionnelle. Cela dit, cette approche est tout à fait cohérente avec la POO. ;)
Où ai-je obtenu mes informations et que devriez-vous lire d'autre ?
- Tutoriel sur le site officiel d'Oracle. Beaucoup d'informations détaillées, y compris des exemples.
- Chapitre sur les références de méthodes dans le même tutoriel Oracle.
- Laissez-vous happer par Wikipédia si vous êtes vraiment curieux.
GO TO FULL VERSION