JavaRush /Blog Java /Random-FR /Populaire sur les expressions lambda en Java. Avec des ex...
Стас Пасинков
Niveau 26
Киев

Populaire sur les expressions lambda en Java. Avec des exemples et des tâches. Partie 1

Publié dans le groupe Random-FR
À qui s’adresse cet article ?
  • Pour ceux qui pensent déjà bien connaître Java Core, mais n’ont aucune idée des expressions lambda en Java. Ou peut-être avez-vous déjà entendu parler des lambdas, mais sans détails.
  • pour ceux qui ont une certaine compréhension des expressions lambda, mais qui ont toujours peur et sont inhabituels de les utiliser.
Si vous n’appartenez pas à l’une de ces catégories, vous pourriez très bien trouver cet article ennuyeux, incorrect et généralement « pas cool ». Dans ce cas, soit n'hésitez pas à passer par ici, soit, si vous maîtrisez bien le sujet, suggérez dans les commentaires comment je pourrais améliorer ou compléter l'article. Le matériel ne revendique aucune valeur académique, encore moins de nouveauté. Au contraire, j'essaierai de décrire le plus simplement possible des choses complexes (pour certains). J'ai été inspiré pour écrire par une demande d'explication de l'API de flux. J'y ai réfléchi et j'ai décidé que sans comprendre les expressions lambda, certains de mes exemples sur les « flux » seraient incompréhensibles. Alors commençons par les lambdas. Populaire sur les expressions lambda en Java.  Avec des exemples et des tâches.  Partie 1 - 1Quelles connaissances sont nécessaires pour comprendre cet article :
  1. Compréhension de la programmation orientée objet (ci-après dénommée POO), à savoir :
    • connaissance de ce que sont les classes et les objets, quelle est la différence entre eux ;
    • connaissance de ce que sont les interfaces, en quoi elles diffèrent des classes, quel est le lien entre elles (interfaces et classes) ;
    • connaissance de ce qu'est une méthode, comment l'appeler, ce qu'est une méthode abstraite (ou une méthode sans implémentation), quels sont les paramètres/arguments d'une méthode, comment les y transmettre ;
    • modificateurs d'accès, méthodes/variables statiques, méthodes/variables finales ;
    • héritage (classes, interfaces, héritage multiple d'interfaces).
  2. Connaissance de Java Core : génériques, collections (listes), threads.
Eh bien, commençons.

Un peu d'histoire

Les expressions lambda sont venues à Java de la programmation fonctionnelle, et là des mathématiques. Au milieu du XXe siècle, en Amérique, travaillait à l'Université de Princeton un certain Alonzo Church, qui aimait beaucoup les mathématiques et toutes sortes d'abstractions. C'est Alonzo Church qui a inventé le calcul lambda, qui était au départ un ensemble d'idées abstraites et n'avait rien à voir avec la programmation. Parallèlement, des mathématiciens comme Alan Turing et John von Neumann travaillaient à la même université de Princeton. Tout s'est réuni : Church a inventé le système de calcul lambda, Turing a développé sa machine informatique abstraite, maintenant connue sous le nom de « machine de Turing ». Eh bien, von Neumann a proposé un schéma de l'architecture des ordinateurs, qui constitue la base des ordinateurs modernes (et est maintenant appelé « architecture de von Neumann »). À cette époque, les idées d'Alonzo Church n'avaient pas autant de renommée que les travaux de ses collègues (à l'exception du domaine des mathématiques « pures »). Cependant, un peu plus tard, un certain John McCarthy (également diplômé de l'Université de Princeton, au moment de l'histoire - employé du Massachusetts Institute of Technology) s'est intéressé aux idées de Church. Sur cette base, il crée en 1958 le premier langage de programmation fonctionnel, Lisp. Et 58 ans plus tard, les idées de programmation fonctionnelle ont été introduites dans Java sous le numéro 8. Pas même 70 ans ne se sont écoulés... En fait, ce n'est pas la période la plus longue pour appliquer une idée mathématique dans la pratique.

L'essence

Une expression lambda est une telle fonction. Vous pouvez considérer cela comme une méthode standard en Java, la seule différence est qu'elle peut être transmise à d'autres méthodes comme argument. Oui, il est devenu possible de transmettre non seulement des nombres, des chaînes et des chats aux méthodes, mais aussi d'autres méthodes ! Quand pourrions-nous en avoir besoin ? Par exemple, si nous voulons passer un rappel. Nous avons besoin de la méthode que nous appelons pour pouvoir appeler une autre méthode que nous lui transmettons. Autrement dit, nous avons la possibilité de transmettre un rappel dans certains cas et un autre dans d'autres. Et notre méthode, qui accepterait nos rappels, les appellerait. Un exemple simple est le tri. Disons que nous écrivons une sorte de tri délicat qui ressemble à ceci :
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
Où, ifon appelle la méthode compare(), on y passe deux objets que l'on compare, et on veut savoir lequel de ces objets est « le plus grand ». Nous mettrons celui qui est « plus » avant celui qui est « plus petit ». J'ai écrit « plus » entre guillemets car nous écrivons une méthode universelle qui sera capable de trier non seulement par ordre croissant mais aussi par ordre décroissant (dans ce cas, « plus » sera l'objet qui est essentiellement plus petit, et vice versa) . Pour définir la règle sur la manière exacte dont nous voulons trier, nous devons la transmettre d'une manière ou d'une autre à notre fichier mySuperSort(). Dans ce cas, nous pourrons en quelque sorte « contrôler » notre méthode pendant son appel. Bien entendu, vous pouvez écrire deux méthodes distinctes mySuperSortAsc()pour mySuperSortDesc()trier par ordre croissant et décroissant. Ou transmettez un paramètre à l'intérieur de la méthode (par exemple, booleanif true, triez par ordre croissant et if falsepar ordre décroissant). Mais que se passe-t-il si nous voulons trier non pas une structure simple, mais, par exemple, une liste de tableaux de chaînes ? Comment notre méthode mySuperSort()saura-t-elle trier ces tableaux de chaînes ? À la taille ? Par longueur totale des mots ? Peut-être par ordre alphabétique, en fonction de la première ligne du tableau ? Mais que se passe-t-il si, dans certains cas, nous devons trier une liste de tableaux par taille du tableau, et dans un autre cas, par la longueur totale des mots du tableau ? Je pense que vous avez déjà entendu parler des comparateurs et que dans de tels cas, nous transmettons simplement un objet comparateur à notre méthode de tri, dans lequel nous décrivons les règles selon lesquelles nous voulons trier. Puisque la méthode standard sort()est implémentée sur le même principe que , mySuperSort()dans les exemples j'utiliserai la méthode standard sort().
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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[]> sortByWordsLength = 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:
  1. maman a lavé le cadre
  2. la paix, le travail peut
  3. J'aime vraiment Java
Ici, les tableaux sont triés par le nombre de mots dans chaque tableau. Un tableau contenant moins de mots est considéré comme « plus petit ». C'est pourquoi cela vient au début. Celui où il y a plus de mots est considéré comme « plus » et se retrouve à la fin. Si sort()nous passons un autre comparateur à la méthode (sortByWordsLength), alors le résultat sera différent :
  1. la paix, le travail peut
  2. maman a lavé le cadre
  3. J'aime vraiment Java
Désormais, les tableaux sont triés selon le nombre total de lettres contenues dans les mots d'un tel tableau. Dans le premier cas, il y a 10 lettres, dans le deuxième 12 et dans le troisième 15. Si nous n'utilisons qu'un seul comparateur, nous ne pouvons pas créer de variable distincte pour celui-ci, mais simplement créer un objet d'une classe anonyme directement au moment de l'appel de la méthode sort(). Comme ça:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

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;
    }
});
Le résultat sera le même que dans le premier cas. Tache 1 . Réécrivez cet exemple pour qu'il trie les tableaux non pas par ordre croissant du nombre de mots dans le tableau, mais par ordre décroissant. Nous savons déjà tout cela. Nous savons comment passer des objets aux méthodes, nous pouvons passer tel ou tel objet à une méthode en fonction de ce dont nous avons besoin du moment, et à l'intérieur de la méthode où nous passons un tel objet, la méthode pour laquelle nous avons écrit l'implémentation sera appelée . La question se pose : qu’est-ce que les expressions lambda ont à voir là-dedans ? Étant donné qu'un lambda est un objet qui contient exactement une méthode. C'est comme un objet méthode. Une méthode enveloppée dans un objet. Ils ont juste une syntaxe légèrement inhabituelle (mais nous y reviendrons plus tard). Jetons un autre regard sur cette entrée
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Ici, nous prenons notre liste arrayset appelons sa méthode sort(), où nous passons un objet comparateur avec une seule méthode compare()(peu importe son nom, car c'est le seul dans cet objet, nous ne le manquerons pas). Cette méthode prend deux paramètres, avec lesquels nous travaillons ensuite. Si vous travaillez dans IntelliJ IDEA , vous avez probablement vu comment il vous propose de raccourcir considérablement ce code :
arrays.sort((o1, o2) -> o1.length - o2.length);
C'est ainsi que six lignes se sont transformées en une seule courte. 6 lignes ont été réécrites en une seule courte. Quelque chose a disparu, mais je vous garantis que rien d'important n'a disparu, et ce code fonctionnera exactement de la même manière qu'avec une classe anonyme. Tâche 2 . Découvrez comment réécrire la solution au problème 1 à l'aide de lambdas (en dernier recours, demandez à IntelliJ IDEA de transformer votre classe anonyme en lambda).

Parlons des interfaces

Fondamentalement, une interface n’est qu’une liste de méthodes abstraites. Lorsque nous créons une classe et disons qu'elle implémentera une sorte d'interface, nous devons écrire dans notre classe une implémentation des méthodes répertoriées dans l'interface (ou, en dernier recours, ne pas l'écrire, mais rendre la classe abstraite ). Il existe des interfaces avec de nombreuses méthodes différentes (par exemple List), il existe des interfaces avec une seule méthode (par exemple, le même Comparator ou Runnable). Il existe des interfaces sans méthode unique (appelées interfaces de marqueur, par exemple Serialisable). Les interfaces qui n'ont qu'une seule méthode sont également appelées interfaces fonctionnelles . Dans Java 8, ils sont même marqués d'une annotation spéciale @FunctionalInterface . Ce sont des interfaces avec une seule méthode qui peuvent être utilisées par les expressions lambda. Comme je l'ai dit ci-dessus, une expression lambda est une méthode enveloppée dans un objet. Et lorsque nous transmettons un tel objet quelque part, nous transmettons en fait cette seule méthode. Il s’avère que le nom de cette méthode n’a pas d’importance pour nous. Tout ce qui est important pour nous, ce sont les paramètres pris par cette méthode et, en fait, le code de la méthode lui-même. Une expression lambda est essentiellement. mise en place d'une interface fonctionnelle. Là où nous voyons une interface avec une méthode, cela signifie que nous pouvons réécrire une telle classe anonyme en utilisant un lambda. Si l'interface a plus/moins d'une méthode, alors l'expression lambda ne nous conviendra pas, et nous utiliserons une classe anonyme, voire normale. Il est temps de fouiller dans les lambdas. :)

Syntaxe

La syntaxe générale ressemble à ceci :
(параметры) -> {тело метода}
C'est-à-dire des parenthèses, à l'intérieur se trouvent les paramètres de la méthode, une « flèche » (ce sont deux caractères d'affilée : moins et plus), après quoi le corps de la méthode est entre accolades, comme toujours. Les paramètres correspondent à ceux précisés dans l'interface lors de la description de la méthode. Si le type des variables peut être clairement défini par le compilateur (dans notre cas, on sait avec certitude que nous travaillons avec des tableaux de chaînes, car il Listest saisi précisément par des tableaux de chaînes), alors le type des variables String[]n'a pas besoin etre ecrit.
Si vous n'êtes pas sûr, spécifiez le type et IDEA le surlignera en gris s'il n'est pas nécessaire.
Vous pouvez en savoir plus dans le didacticiel Oracle , par exemple. C'est ce qu'on appelle le « typage cible » . Vous pouvez donner n'importe quel nom aux variables, pas nécessairement ceux spécifiés dans l'interface. S'il n'y a pas de paramètres, juste des parenthèses. S'il n'y a qu'un seul paramètre, juste le nom de la variable sans parenthèses. Nous avons réglé les paramètres, concernant maintenant le corps de l'expression lambda elle-même. À l’intérieur des accolades, écrivez le code comme pour une méthode normale. Si l’intégralité de votre code ne comporte qu’une seule ligne, vous n’avez pas du tout besoin d’écrire des accolades (comme pour les ifs et les boucles). Si votre lambda renvoie quelque chose, mais que son corps est constitué d'une seule ligne, il returnn'est pas du tout nécessaire de l'écrire. Mais si vous avez des accolades, alors, comme dans la méthode habituelle, vous devez écrire explicitement return.

Exemples

Exemple 1.
() -> {}
L'option la plus simple. Et le plus dénué de sens :) Parce que ça ne fait rien. Exemple 2.
() -> ""
C'est aussi une option intéressante. Il n'accepte rien et renvoie une chaîne vide ( returnomise car inutile). Le même, mais avec return:
() -> {
    return "";
}
Exemple 3. Bonjour tout le monde utilisant lambdas
() -> System.out.println("Hello world!")
Ne reçoit rien, ne renvoie rien (on ne peut pas mettre returnavant l'appel System.out.println(), puisque le type return dans la méthode println() — void), affiche simplement une inscription à l'écran. Idéal pour implémenter une interface Runnable. Le même exemple 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 qu'objet de type Runnable, puis la transmettre au constructeurthread’а :
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 de la sauvegarde d'une expression lambda dans une variable. L'interface Runnablenous dit que ses objets doivent avoir une méthode public void run(). Selon l'interface, la méthode run n'accepte rien comme paramètres. Et ça ne renvoie rien (void). Par conséquent, lors de l’écriture de cette façon, un objet sera créé avec une méthode qui n’accepte ni ne renvoie rien. Ce qui est tout à fait cohérent avec la méthode run()de l'interface Runnable. C'est pourquoi nous avons pu mettre cette expression lambda dans une variable telle que Runnable. Exemple 4
() -> 42
Encore une fois, il n'accepte rien, mais renvoie le nombre 42. Cette expression lambda peut être placée dans une variable de type Callable, car cette interface ne définit qu'une seule méthode, qui ressemble à ceci :
V call(),
Vest le type de la valeur de retour (dans notre cas int). En conséquence, nous pouvons stocker une telle expression lambda comme suit :
Callable<Integer> c = () -> 42;
Exemple 5. Lambda en 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 ni son type de retour void(puisqu'il n'y en a pas return). Exemple 6
x -> x
Ici, nous prenons quelque chose dans une variable хet le renvoyons. Veuillez noter que si un seul paramètre est accepté, il n'est pas nécessaire d'écrire les parenthèses qui l'entourent. Le même, mais avec parenthèses :
(x) -> x
Et voici l'option avec une option explicite return:
x -> {
    return x;
}
Ou comme ça, avec des parenthèses et return:
(x) -> {
    return x;
}
Ou avec une indication explicite du type (et, par conséquent, avec des parenthèses) :
(int x) -> x
Exemple 7
x -> ++x
Nous l'acceptons хet le rendons, mais pour 1plus. Vous pouvez également le réécrire comme ceci :
x -> x + 1
Dans les deux cas, nous n'indiquons pas de parenthèses autour du paramètre, du corps de la méthode et du mot return, car cela n'est pas nécessaire. Les options avec parenthèses et retour sont décrites dans l'exemple 6. Exemple 8
(x, y) -> x % y
Nous en acceptons une partie хet уrendons le reste de la division xpar y. Les parenthèses autour des paramètres sont déjà obligatoires ici. Ils sont facultatifs uniquement lorsqu'il n'y a qu'un seul paramètre. Comme ceci avec 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 acceptons un objet Cat, une chaîne avec un nom et un âge entier. Dans la méthode elle-même, nous définissons le nom et l'âge transmis au chat. catPuisque notre variable est un type référence, l'objet Cat en dehors de l'expression lambda changera (il recevra le nom et l'âge transmis à l'intérieur). 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 print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

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

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

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

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

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

class Cat implements WithNameAndAge {
    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='Murzik', age=3} Comme vous pouvez le voir, au début l'objet Cat avait un état, mais après avoir utilisé l'expression lambda, l'état a changé . Les expressions lambda fonctionnent bien avec les génériques. Et si nous devons créer une classe Dog, par exemple, qui implémentera également WithNameAndAge, alors dans la méthode, main()nous pouvons effectuer les mêmes opérations avec Dog, sans changer du tout l'expression lambda elle-même. Tâche 3 . Écrivez 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 si le nombre transmis est divisible par 13 sanstrue reste . Écrivez une interface fonctionnelle avec une méthode qui prend deux chaînes et renvoie la même chaîne. Écrivez une implémentation d'une telle interface sous la forme d'un lambda qui renvoie la chaîne la plus longue. Tâche 5 . Écrivez une interface fonctionnelle avec une méthode qui accepte trois nombres fractionnaires : , et renvoie le même nombre fractionnaire. Écrivez une implémentation d'une telle interface sous la forme d'une expression lambda qui renvoie un discriminant. Qui a oublié, 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 l'opération . Populaire sur les expressions lambda en Java. Avec des exemples et des tâches. Partie 2.abca * b^c
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION