CodeGym/Blog Java/Random-FR/Une explication des expressions lambda en Java. Avec des ...
John Squirrels
Niveau 41
San Francisco

Une explication des expressions lambda en Java. Avec des exemples et des tâches. Partie 1

Publié dans le groupe Random-FR
membres
A qui s'adresse 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.
Une explication des expressions lambda en Java.  Avec des exemples et des tâches.  Partie 1 - 1Si vous ne correspondez pas à l'une de ces catégories, vous pourriez trouver cet article ennuyeux, imparfait ou généralement pas votre tasse de thé. Dans ce cas, n'hésitez pas à passer à autre chose ou, si vous connaissez bien le sujet, n'hésitez pas à faire des suggestions dans les commentaires sur la façon dont je pourrais améliorer ou compléter l'article. Le matériel ne prétend pas avoir de valeur académique, encore moins de nouveauté. Bien au contraire : je vais essayer de décrire le plus simplement possible des choses complexes (pour certaines personnes). Une demande d'explication de l'API Stream m'a inspiré pour écrire ceci. J'y ai réfléchi et j'ai décidé que certains de mes exemples de flux seraient incompréhensibles sans une compréhension des expressions lambda. Nous allons donc commencer par les expressions lambda. Que faut-il savoir pour comprendre cet article ?
  1. Vous devez comprendre la programmation orientée objet (POO), à savoir :

    • les classes, les objets et la différence entre eux ;
    • interfaces, comment elles diffèrent des classes et relation entre les interfaces et les classes ;
    • les méthodes, comment les appeler, les méthodes abstraites (c'est-à-dire les méthodes sans implémentation), les paramètres de méthode, les arguments de méthode et comment les passer ;
    • modificateurs d'accès, méthodes/variables statiques, méthodes/variables finales ;
    • héritage de classes et d'interfaces, héritage multiple d'interfaces.
  2. Connaissance de Java Core : types génériques (génériques), collections (listes), threads.
Eh bien, allons-y.

Un peu d'histoire

Les expressions lambda sont venues à Java de la programmation fonctionnelle, et là des mathématiques. Aux États-Unis au milieu du XXe siècle, Alonzo Church, qui aimait beaucoup les mathématiques et toutes sortes d'abstractions, travaillait à l'université de Princeton. C'est Alonzo Church qui a inventé le calcul lambda, qui était initialement un ensemble d'idées abstraites sans aucun rapport avec la programmation. Des mathématiciens comme Alan Turing et John von Neumann travaillaient à la même époque à l'Université de Princeton. Tout s'est mis en place : Church a inventé le calcul lambda. Turing a développé sa machine informatique abstraite, maintenant connue sous le nom de "machine de Turing". Et von Neumann a proposé une architecture informatique qui a formé la base des ordinateurs modernes (maintenant appelée « architecture von Neumann »). A cette époque, l'église d'Alonzo' les idées de s ne sont pas devenues aussi connues que les travaux de ses collègues (à l'exception du domaine des mathématiques pures). Cependant, un peu plus tard, John McCarthy (également diplômé de l'Université de Princeton et, à l'époque de notre histoire, employé du Massachusetts Institute of Technology) s'est intéressé aux idées de Church. En 1958, il crée le premier langage de programmation fonctionnel, LISP, basé sur ces idées. Et 58 ans plus tard, les idées de la programmation fonctionnelle se sont infiltrées dans Java 8. Même pas 70 ans se sont écoulés... Honnêtement, ce n'est pas le temps qu'il a fallu pour qu'une idée mathématique soit appliquée en pratique. un employé du Massachusetts Institute of Technology) s'est intéressé aux idées de Church. En 1958, il crée le premier langage de programmation fonctionnel, LISP, basé sur ces idées. Et 58 ans plus tard, les idées de la programmation fonctionnelle se sont infiltrées dans Java 8. Même pas 70 ans se sont écoulés... Honnêtement, ce n'est pas le temps qu'il a fallu pour qu'une idée mathématique soit appliquée en pratique. un employé du Massachusetts Institute of Technology) s'est intéressé aux idées de Church. En 1958, il crée le premier langage de programmation fonctionnel, LISP, basé sur ces idées. Et 58 ans plus tard, les idées de la programmation fonctionnelle se sont infiltrées dans Java 8. Même pas 70 ans se sont écoulés... Honnêtement, ce n'est pas le temps qu'il a fallu pour qu'une idée mathématique soit appliquée en pratique.

Le cœur du problème

Une expression lambda est une sorte de fonction. Vous pouvez la considérer comme une méthode Java ordinaire, mais avec la capacité distinctive d'être transmise à d'autres méthodes en tant qu'argument. C'est exact. Il est devenu possible de passer non seulement des nombres, des chaînes et des chats à des méthodes, mais également à d'autres méthodes ! Quand pourrions-nous en avoir besoin ? Ce serait utile, par exemple, si nous voulons passer une méthode de rappel. Autrement dit, si nous avons besoin que la méthode que nous appelons ait la possibilité d'appeler une autre méthode que nous lui transmettons. En d'autres termes, nous avons donc la possibilité de passer un rappel dans certaines circonstances et un rappel différent dans d'autres. Et pour que notre méthode qui reçoit nos rappels les appelle. Le tri est un exemple simple. Supposons que nous écrivions un algorithme de tri intelligent qui ressemble à ceci :
public void mySuperSort() {
    // We do something here
    if(compare(obj1, obj2) > 0)
    // And then we do something here
}
Dans l' ifénoncé, on appelle la compare()méthode, en passant deux objets à comparer, et on veut savoir lequel de ces objets est "plus grand". Nous supposons que le "plus grand" vient avant le "moins". J'ai mis "plus grand" entre guillemets, car nous écrivons une méthode universelle qui saura trier non seulement par ordre croissant, mais aussi par ordre décroissant (dans ce cas, l'objet "plus grand" sera en fait l'objet "petit" , et vice versa). Pour définir l'algorithme spécifique pour notre tri, nous avons besoin d'un mécanisme pour le transmettre à notre mySuperSort()méthode. De cette façon, nous pourrons "contrôler" notre méthode lorsqu'elle sera appelée. Bien sûr, nous pourrions écrire deux méthodes distinctes - mySuperSortAscend()etmySuperSortDescend()— pour trier par ordre croissant et décroissant. Ou nous pourrions passer un argument à la méthode (par exemple, une variable booléenne ; si vrai, alors trier dans l'ordre croissant, et si faux, alors dans l'ordre décroissant). Mais que se passe-t-il si nous voulons trier quelque chose de compliqué comme une liste de tableaux de chaînes ? Comment notre mySuperSort()méthode saura-t-elle comment trier ces tableaux de chaînes ? Par taille? Par la longueur cumulée de tous les mots ? Peut-être par ordre alphabétique en fonction de la première chaîne du tableau ? Et que se passe-t-il si nous devons trier la liste des tableaux par taille de tableau dans certains cas, et par la longueur cumulée de tous les mots de chaque tableau dans d'autres cas ? Je suppose que vous avez déjà entendu parler des comparateurs et que dans ce cas, nous passerions simplement à notre méthode de tri un objet comparateur qui décrit l'algorithme de tri souhaité. Parce que la normesort()est implémentée sur le même principe que mySuperSort(), que j'utiliserai sort()dans mes exemples.
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<;String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() {

    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }

        for (String s : o2) {
            length2 += s.length();
        }

        return length1 - length2;
    }
};

arrays.sort(sortByLength);
Résultat:
Dota GTA5 Halo
if then else
I really love Java
Ici, les tableaux sont triés par le nombre de mots dans chaque tableau. Un tableau avec moins de mots est considéré comme "inférieur". C'est pourquoi il vient en premier. Un tableau avec plus de mots est considéré comme "plus grand" et est placé à la fin. Si nous passons un comparateur différent à la sort()méthode, tel que sortByCumulativeWordLength, nous obtiendrons un résultat différent :
if then else
Dota GTA5 Halo
I really love Java
Maintenant, les tableaux sont triés par le nombre total de lettres dans les mots du tableau. Dans le premier tableau, il y a 10 lettres, dans le second — 12, et dans le troisième — 15. Si nous n'avons qu'un seul comparateur, nous n'avons pas à déclarer une variable distincte pour celui-ci. Au lieu de cela, nous pouvons simplement créer une classe anonyme juste au moment de l'appel à la sort()méthode. Quelque chose comme ça:
String[] array1 = {"Dota", "GTA5", "Halo"};
String[] array2 = {"I", "really", "love", "Java"};
String[] array3 = {"if", "then", "else"};

List<String[]> arrays = new ArrayList<>();

arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Nous obtiendrons le même résultat que dans le premier cas. Tâche 1. Réécrivez cet exemple afin qu'il trie les tableaux non pas dans l'ordre croissant du nombre de mots dans chaque tableau, mais dans l'ordre décroissant. Nous savons déjà tout cela. Nous savons comment passer des objets aux méthodes. En fonction de ce dont nous avons besoin sur le moment, nous pouvons passer différents objets à une méthode, qui invoquera ensuite la méthode que nous avons implémentée. Cela soulève la question : pourquoi diable avons-nous besoin d'une expression lambda ici ?  Parce qu'une expression lambda est un objet qui a exactement une méthode. Comme un "objet méthode". Une méthode empaquetée dans un objet. Il a juste une syntaxe légèrement inconnue (mais plus à ce sujet plus tard). Reprenons ce code :
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Ici, nous prenons notre liste de tableaux et appelons sa sort()méthode, à laquelle nous passons un objet comparateur avec une seule compare()méthode (son nom n'a pas d'importance pour nous — après tout, c'est la seule méthode de cet objet, donc nous ne pouvons pas nous tromper). Cette méthode a deux paramètres avec lesquels nous allons travailler. Si vous travaillez dans IntelliJ IDEA, vous l'avez probablement vu proposer de condenser considérablement le code comme suit :
arrays.sort((o1, o2) -> o1.length - o2.length);
Cela réduit six lignes à une seule courte. 6 lignes sont réécrites en une courte. Quelque chose a disparu, mais je vous garantis que ce n'était rien d'important. Ce code fonctionnera exactement de la même manière qu'avec une classe anonyme. Tâche 2. Tentez de réécrire la solution de la tâche 1 à l'aide d'une expression lambda (à tout le moins, demandez à IntelliJ IDEA de convertir votre classe anonyme en une expression lambda).

Parlons des interfaces

En principe, une interface est simplement une liste de méthodes abstraites. Lorsque nous créons une classe qui implémente une interface, notre classe doit implémenter les méthodes incluses dans l'interface (ou nous devons rendre la classe abstraite). Il existe des interfaces avec de nombreuses méthodes différentes (par exemple,  List) et d'autres avec une seule méthode (par exemple, Comparatorou Runnable). Il existe des interfaces qui n'ont pas de méthode unique (appelées interfaces de marqueur telles que Serializable). Les interfaces qui n'ont qu'une seule méthode sont également appelées interfaces fonctionnelles . En Java 8, ils sont même marqués d'une annotation spéciale :@FunctionalInterface. Ce sont ces interfaces à méthode unique qui conviennent comme types cibles pour les expressions lambda. Comme je l'ai dit plus haut, une expression lambda est une méthode enveloppée dans un objet. Et lorsque nous passons un tel objet, nous passons essentiellement cette seule méthode. Il s'avère que nous ne nous soucions pas du nom de la méthode. Les seules choses qui comptent pour nous sont les paramètres de la méthode et, bien sûr, le corps de la méthode. Essentiellement, une expression lambda est l'implémentation d'une interface fonctionnelle. Partout où nous voyons une interface avec une seule méthode, une classe anonyme peut être réécrite en tant que lambda. Si l'interface a plus ou moins d'une méthode, alors une expression lambda ne fonctionnera pas et nous utiliserons à la place une classe anonyme ou même une instance d'une classe ordinaire. Il est maintenant temps de creuser un peu les lambdas. :)

Syntaxe

La syntaxe générale ressemble à ceci :
(parameters) -> {method body}
C'est-à-dire des parenthèses entourant les paramètres de la méthode, une "flèche" (formée par un trait d'union et un signe supérieur à), puis le corps de la méthode entre accolades, comme toujours. Les paramètres correspondent à ceux spécifiés dans la méthode d'interface. Si les types de variables peuvent être déterminés sans ambiguïté par le compilateur (dans notre cas, il sait que nous travaillons avec des tableaux de chaînes, car notre Listobjet est typé à l'aide de String[]), alors vous n'avez pas à indiquer leurs types.
S'ils sont ambigus, indiquez le type. IDEA le coloriera en gris s'il n'est pas nécessaire.
Vous pouvez en savoir plus dans ce tutoriel Oracle et ailleurs. C'est ce qu'on appelle le « typage cible ». Vous pouvez nommer les variables comme vous le souhaitez — vous n'êtes pas obligé d'utiliser les mêmes noms spécifiés dans l'interface. S'il n'y a pas de paramètres, indiquez simplement des parenthèses vides. S'il n'y a qu'un seul paramètre, indiquez simplement le nom de la variable sans parenthèses. Maintenant que nous comprenons les paramètres, il est temps de discuter du corps de l'expression lambda. À l'intérieur des accolades, vous écrivez du code comme vous le feriez pour une méthode ordinaire. Si votre code se compose d'une seule ligne, vous pouvez omettre entièrement les accolades (similaire aux instructions if et aux boucles for). Si votre lambda à une seule ligne renvoie quelque chose, vous n'avez pas besoin d'inclure unreturndéclaration. Mais si vous utilisez des accolades, vous devez inclure explicitement une returninstruction, comme vous le feriez dans une méthode ordinaire.

Exemples

Exemple 1.
() -> {}
L'exemple le plus simple. Et le plus inutile :), puisqu'il ne fait rien. Exemple 2.
() -> ""
Un autre exemple intéressant. Il ne prend rien et renvoie une chaîne vide ( returnest omis, car inutile). Voici la même chose, mais avec return:
() -> {
    return "";
}
Exemple 3. "Hello, World!" en utilisant des lambdas
() -> System.out.println("Hello, World!")
Il ne prend rien et ne renvoie rien (nous ne pouvons pas mettre returnavant l'appel à System.out.println(), car le println()type de retour de la méthode est void). Il affiche simplement le message d'accueil. C'est idéal pour une implémentation de l' Runnableinterface. L'exemple suivant est plus complet :
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello, World!")).start();
    }
}
Ou comme ceci :
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello, World!"));
        t.start();
    }
}
Ou nous pouvons même enregistrer l'expression lambda en tant Runnablequ'objet, puis la transmettre au Threadconstructeur :
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello, World!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Examinons de plus près le moment où une expression lambda est enregistrée dans une variable. L' Runnableinterface nous dit que ses objets doivent avoir une public void run()méthode. Selon l'interface, la runméthode ne prend aucun paramètre. Et il ne renvoie rien, c'est-à-dire que son type de retour est void. En conséquence, ce code créera un objet avec une méthode qui ne prend ni ne renvoie rien. Cela correspond parfaitement à la méthode Runnablede l'interface run(). C'est pourquoi nous avons pu mettre cette expression lambda dans une Runnablevariable.  Exemple 4.
() -> 42
Encore une fois, cela ne prend rien, mais cela renvoie le nombre 42. Une telle expression lambda peut être placée dans une Callablevariable, car cette interface n'a qu'une seule méthode qui ressemble à ceci :
V call(),
où  V est le type de retour (dans notre cas,  int). En conséquence, nous pouvons enregistrer une expression lambda comme suit :
Callable<Integer> c = () -> 42;
Exemple 5. Une expression lambda impliquant plusieurs lignes
() -> {
    String[] helloWorld = {"Hello", "World!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Encore une fois, il s'agit d'une expression lambda sans paramètres et avec un voidtype de retour (car il n'y a pas returnd'instruction).  Exemple 6
x -> x
Ici, nous prenons une xvariable et la renvoyons. Veuillez noter que s'il n'y a qu'un seul paramètre, vous pouvez omettre les parenthèses qui l'entourent. Voici la même chose, mais avec des parenthèses :
(x) -> x
Et voici un exemple avec une instruction return explicite :
x -> {
    return x;
}
Ou comme ceci avec des parenthèses et une déclaration de retour :
(x) -> {
    return x;
}
Soit avec une indication explicite du type (et donc entre parenthèses) :
(int x) -> x
Exemple 7
x -> ++x
Nous xle prenons et le retournons, mais seulement après avoir ajouté 1. Vous pouvez réécrire ce lambda comme ceci :
x -> x + 1
Dans les deux cas, nous omettons les parenthèses autour du corps du paramètre et de la méthode, ainsi que l' returninstruction, car elles sont facultatives. Les versions entre parenthèses et une instruction de retour sont données dans l'exemple 6. Exemple 8
(x, y) -> x % y
Nous prenons xet yet renvoyons le reste de la division de xpar y. Les parenthèses autour des paramètres sont obligatoires ici. Ils ne sont facultatifs que lorsqu'il n'y a qu'un seul paramètre. Le voici avec une indication explicite des types :
(double x, int y) -> x % y
Exemple 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Nous prenons un Catobjet, un Stringnom et un int age. Dans la méthode elle-même, nous utilisons le nom et l'âge passés pour définir des variables sur le chat. Parce que notre catobjet est un type de référence, il sera modifié en dehors de l'expression lambda (il obtiendra le nom et l'âge passés). Voici une version légèrement plus compliquée qui utilise un lambda similaire :
public class Main {

    public static void main(String[] args) {
        // Create a cat and display it to confirm that it is "empty"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // Create a lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);

        };

        // Call a method to which we pass the cat and lambda
        changeEntity(myCat, s);

        // Display the cat on the screen and see that its state has changed (it has a name and age)
        System.out.println(myCat);

    }

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Smokey", 3);
    }
}

interface HasNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends HasNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements HasNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Résultat:
Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Comme vous pouvez le voir, l' Catobjet avait un état, puis l'état a changé après avoir utilisé l'expression lambda. Les expressions lambda se combinent parfaitement avec les génériques. Et si nous devons créer une Dogclasse qui implémente également HasNameAndAge, nous pouvons effectuer les mêmes opérations dans Dogla main() méthode sans modifier l'expression lambda. Tâche 3. Écrire une interface fonctionnelle avec une méthode qui prend un nombre et renvoie une valeur booléenne. Écrivez une implémentation d'une telle interface sous la forme d'une expression lambda qui renvoie vrai si le nombre passé est divisible par 13. Tâche 4.Écrivez une interface fonctionnelle avec une méthode qui prend deux chaînes et renvoie également une chaîne. Écrivez une implémentation d'une telle interface sous la forme d'une expression lambda qui renvoie la chaîne la plus longue. Tâche 5. Écrire une interface fonctionnelle avec une méthode qui prend trois nombres à virgule flottante : a, b et c et renvoie également un nombre à virgule flottante. Écrivez une implémentation d'une telle interface sous la forme d'une expression lambda qui renvoie le discriminant. Au cas où vous l'auriez oublié, c'est D = b^2 — 4ac. Tâche 6. À l'aide de l'interface fonctionnelle de la tâche 5, écrivez une expression lambda qui renvoie le résultat de a * b^c. Une explication des expressions lambda en Java. Avec des exemples et des tâches. Partie 2
Commentaires
  • Populaires
  • Nouveau
  • Anciennes
Tu dois être connecté(e) pour laisser un commentaire
Cette page ne comporte pas encore de commentaires