Les expressions lambda ont-elles une utilité autre que l'enregistrement de lignes de code?

120

Les expressions lambda ont-elles une utilité autre que l'enregistrement de lignes de code?

Existe-t-il des fonctionnalités spéciales fournies par lambdas qui résolvaient des problèmes qui n'étaient pas faciles à résoudre? L'utilisation typique que j'ai vue est qu'au lieu d'écrire ceci:

Comparator<Developer> byName = new Comparator<Developer>() {
  @Override
  public int compare(Developer o1, Developer o2) {
    return o1.getName().compareTo(o2.getName());
  }
};

Nous pouvons utiliser une expression lambda pour raccourcir le code:

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());
Vikash
la source
27
Notez qu'il peut être encore plus court: (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen
88
ou encore plus court:Comparator.comparing(Developer::getName)
Thomas Kläger
31
Je ne sous-estimerais pas cet avantage. Lorsque les choses sont beaucoup plus faciles, vous êtes plus susceptible de faire les choses de la «bonne» façon. Il y a souvent une certaine tension entre l'écriture de code plus facile à maintenir à long terme et plus facile à noter à court terme. Les lambdas réduisent cette tension et vous aident ainsi à écrire un code plus propre.
yshavit
5
Les lambdas sont des fermetures, ce qui rend les économies d'espace encore plus importantes, car vous n'avez plus besoin d'ajouter un constructeur paramétré, des champs, un code d'affectation, etc. à votre classe de remplacement.
CodesInChaos

Réponses:

116

Les expressions Lambda ne changent pas l'ensemble des problèmes que vous pouvez résoudre avec Java en général, mais facilitent certainement la résolution de certains problèmes, juste pour la même raison que nous ne programmons plus en langage assembleur. La suppression des tâches redondantes du travail du programmeur facilite la vie et permet de faire des choses que vous ne toucheriez même pas autrement, juste pour la quantité de code que vous auriez à produire (manuellement).

Mais les expressions lambda ne sont pas seulement de sauver des lignes de code. Les expressions Lambda vous permettent de définir des fonctions , ce pour quoi vous pouvez auparavant utiliser des classes internes anonymes comme solution de contournement, c'est pourquoi vous pouvez remplacer les classes internes anonymes dans ces cas, mais pas en général.

Plus particulièrement, les expressions lambda sont définies indépendamment de l'interface fonctionnelle vers laquelle elles seront converties, il n'y a donc pas de membres hérités auxquels ils pourraient accéder, de plus, elles ne peuvent pas accéder à l'instance du type implémentant l'interface fonctionnelle. Dans une expression lambda, thiset superont la même signification que dans le contexte environnant, voir aussi cette réponse . De plus, vous ne pouvez pas créer de nouvelles variables locales occultant les variables locales du contexte environnant. Pour la tâche prévue de définir une fonction, cela supprime un grand nombre de sources d'erreur, mais cela implique également que pour d'autres cas d'utilisation, il peut y avoir des classes internes anonymes qui ne peuvent pas être converties en une expression lambda, même si vous implémentez une interface fonctionnelle.

De plus, la construction new Type() { … }garantit de produire une nouvelle instance distincte (comme newtoujours). Les instances de classe interne anonymes conservent toujours une référence à leur instance externe si elles sont créées dans un non- staticcontexte. En revanche, les expressions lambda capturent une référence uniquement thislorsque cela est nécessaire, c'est-à-dire si elles y accèdent thisou si elles ne sont pas staticmembres. Et ils produisent des instances d'une identité intentionnellement non spécifiée, ce qui permet à l'implémentation de décider au moment de l'exécution s'il faut réutiliser les instances existantes (voir aussi « Une expression lambda crée-t-elle un objet sur le tas à chaque exécution? »).

Ces différences s'appliquent à votre exemple. Votre construction de classe interne anonyme produira toujours une nouvelle instance, elle peut également capturer une référence à l'instance externe, tandis que votre (Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())est une expression lambda non capturante qui évaluera à un singleton dans les implémentations typiques. De plus, il ne produit pas de .classfichier sur votre disque dur.

Compte tenu des différences concernant à la fois la sémantique et les performances, les expressions lambda peuvent changer la façon dont les programmeurs résoudront certains problèmes à l'avenir, bien sûr, également en raison des nouvelles API englobant des idées de programmation fonctionnelle utilisant les nouvelles fonctionnalités du langage. Voir aussi Expression lambda Java 8 et valeurs de première classe .

Holger
la source
1
Essentiellement, les expressions lambda sont un type de programmation fonctionnelle. Étant donné que n'importe quelle tâche peut être effectuée avec une programmation fonctionnelle ou une programmation impérative, il n'y a aucun avantage sauf en ce qui concerne la lisibilité et la maintenabilité , qui peuvent l'emporter sur d'autres préoccupations.
Draco18s ne fait plus confiance en SE
1
@Trilarion: vous pouvez remplir des livres entiers avec la définition de la fonction . Vous devrez également décider de vous concentrer sur l' objectif ou sur les aspects pris en charge par les expressions lambda de Java. Le dernier lien de ma réponse ( celui-ci ) traite déjà de certains de ces aspects dans le contexte de Java. Mais pour comprendre ma réponse, il suffit déjà de comprendre que les fonctions sont un concept différent des classes. Pour plus de détails, il existe déjà des ressources et des questions-réponses…
Holger
1
@Trilarion: les deux, les fonctions ne permettent pas de résoudre plus de problèmes, sauf si vous considérez la quantité de taille / complexité du code ou les solutions de contournement nécessaires par d'autres solutions comme inacceptables (comme dit dans le premier paragraphe), et il y avait déjà un moyen de définir les fonctions , comme dit, les classes internes pourraient être utilisées comme solution de contournement pour l'absence d'une meilleure façon de définir les fonctions (mais les classes internes ne sont pas des fonctions, car elles peuvent servir à bien d'autres fins). C'est la même chose qu'avec la POO - cela ne permet pas de faire quoi que ce soit que vous ne pourriez pas avec l'assembly, mais pouvez-vous quand même créer une image d'une application comme Eclipse écrite en assembly?
Holger
3
@EricDuminil: pour moi, la complétude de Turing est trop théorique. Par exemple, que Java soit terminé ou non, vous ne pouvez pas écrire un pilote de périphérique Windows en Java. (Ou dans PostScript, qui est également Turing complet) Ajouter des fonctionnalités à Java qui permettent de faire cela, changerait l'ensemble des problèmes que vous pouvez résoudre avec, cependant, cela ne se produira pas. Je préfère donc le point de l'Assemblée; puisque tout ce que vous pouvez faire est implémenté dans des instructions CPU exécutables, il n'y a rien que vous ne puissiez pas faire dans Assembly qui prend en charge toutes les instructions CPU (également plus facile à comprendre que le formalisme de la machine de Turing).
Holger
2
@abelito est commun à toutes les constructions de programmation de niveau supérieur, pour fournir moins de «visibilité sur ce qui se passe réellement», car c'est de cela qu'il s'agit, moins de détails, plus de concentration sur le problème de programmation réel. Vous pouvez l'utiliser pour rendre le code meilleur ou pire; c'est juste un outil. Comme la fonction de vote; c'est un outil que vous pouvez utiliser de la manière prévue, pour fournir un jugement fondé sur la qualité réelle d'un message. Ou vous l'utilisez pour voter contre une réponse élaborée et raisonnable, simplement parce que vous détestez les lambdas.
Holger
52

Les langages de programmation ne sont pas destinés aux machines à exécuter.

Ils sont pour les programmeurs de penser à.

Les langages sont une conversation avec un compilateur pour transformer nos pensées en quelque chose qu'une machine peut exécuter. L' une des principales plaintes au sujet de Java de personnes qui viennent des autres langues (ou quittent ce pour d' autres langues) utilisées pour être qu'il force un modèle mental sur le programmeur (c. -à- tout est une classe).

Je ne vais pas me demander si c'est bon ou mauvais: tout est compromis. Mais les lambdas Java 8 permettent aux programmeurs de penser en termes de fonctions , ce que vous ne pouviez pas faire auparavant en Java.

C'est la même chose qu'un programmeur procédural apprenant à penser en termes de classes quand ils arrivent à Java: vous les voyez passer progressivement de classes qui sont des structures glorifiées et ont des classes `` d'assistance '' avec un tas de méthodes statiques et passer à quelque chose qui ressemble plus à une conception rationnelle OO (mea culpa).

Si vous les considérez simplement comme un moyen plus court d'exprimer des classes internes anonymes, vous ne les trouverez probablement pas très impressionnants de la même manière que le programmeur procédural ci-dessus ne pensait probablement pas que les classes étaient une grande amélioration.

Jared Smith
la source
5
Soyez prudent avec cela cependant, je pensais que la POO était tout, puis je me suis terriblement confondu avec les architectures entité-composant-système (whaddaya signifie que vous n'avez pas de classe pour chaque type d'objet de jeu?! Et chaque objet de jeu n'est pas un objet POO?!)
user253751
3
En d'autres termes, penser en POO, plutôt que de penser simplement aux classes * o f * comme un autre outil, a limité ma façon de penser.
user253751
2
@immibis: il existe de nombreuses façons différentes de «penser en POO», donc en plus de l'erreur courante de confondre «penser en POO» avec «penser en classe», il existe de nombreuses façons de penser de manière contre-productive. Malheureusement, cela arrive trop souvent dès le début, car même les livres et les tutoriels enseignent aux inférieurs, montrent des modèles anti dans des exemples et diffusent cette réflexion. Le besoin supposé «d'avoir une classe pour chaque type de» quoi que ce soit, n'est qu'un exemple, contredisant le concept d'abstraction, de même, il semble y avoir le mythe «OOP signifie écrire autant de passe-partout que possible», etc.
Holger
@immibis bien que dans ce cas cela ne vous a pas aidé, cette anecdote soutient en fait le point: le langage et le paradigme fournissent un cadre pour structurer vos pensées et les transformer en design. Dans votre cas, vous vous êtes heurté aux bords du cadre, mais dans un autre contexte, cela vous aurait peut-être aidé à ne pas vous perdre dans une grosse boule de boue et à produire un design laid. Par conséquent, je suppose que "tout est compromis". Garder le dernier avantage tout en évitant le premier problème est une question clé pour décider de la "taille" d'une langue.
Leushenko
@immibis Voilà pourquoi il est important d'apprendre un tas de paradigme bien : chacun vous atteint sur les insuffisances des autres.
Jared Smith
36

L'enregistrement de lignes de code peut être considéré comme une nouvelle fonctionnalité, si cela vous permet d'écrire une partie substantielle de la logique d'une manière plus courte et plus claire, ce qui prend moins de temps pour que les autres le lisent et la comprennent.

Sans expressions lambda (et / ou références de méthode), les Streampipelines auraient été beaucoup moins lisibles.

Pensez, par exemple, à quoi Streamaurait ressemblé le pipeline suivant si vous remplaçiez chaque expression lambda par une instance de classe anonyme.

List<String> names =
    people.stream()
          .filter(p -> p.getAge() > 21)
          .map(p -> p.getName())
          .sorted((n1,n2) -> n1.compareToIgnoreCase(n2))
          .collect(Collectors.toList());

Ce serait:

List<String> names =
    people.stream()
          .filter(new Predicate<Person>() {
              @Override
              public boolean test(Person p) {
                  return p.getAge() > 21;
              }
          })
          .map(new Function<Person,String>() {
              @Override
              public String apply(Person p) {
                  return p.getName();
              }
          })
          .sorted(new Comparator<String>() {
              @Override
              public int compare(String n1, String n2) {
                  return n1.compareToIgnoreCase(n2);
              }
          })
          .collect(Collectors.toList());

C'est beaucoup plus difficile à écrire que la version avec des expressions lambda, et c'est beaucoup plus sujet aux erreurs. C'est aussi plus difficile à comprendre.

Et c'est un pipeline relativement court.

Pour rendre cela lisible sans expressions lambda et références de méthode, vous auriez dû définir des variables contenant les diverses instances d'interface fonctionnelle utilisées ici, ce qui aurait divisé la logique du pipeline, ce qui le rendrait plus difficile à comprendre.

Eran
la source
17
Je pense que c'est un élément clé. On pourrait soutenir que quelque chose est pratiquement impossible à faire dans un langage de programmation si la surcharge syntaxique et cognitive est trop importante pour être utile, de sorte que d'autres approches seront supérieures. Par conséquent, en réduisant la surcharge syntaxique et cognitive (comme le font les lambdas par rapport aux classes anonymes), des pipelines tels que ceux ci-dessus deviennent possibles. Plus ironique, si l'écriture d'un morceau de code vous fait virer de votre travail, on pourrait dire que ce n'est pas possible.
Sebastian Redl
Nitpick: Vous n'avez pas besoin du Comparatorfor sortedcar il compare de toute façon dans l'ordre naturel. Peut-être le changer pour comparer la longueur des chaînes ou similaire, pour rendre l'exemple encore meilleur?
tobias_k
@tobias_k pas de problème. Je l'ai changé en compareToIgnoreCasepour qu'il se comporte différemment de sorted().
Eran
1
compareToIgnoreCaseest encore un mauvais exemple, car vous pourriez simplement utiliser le String.CASE_INSENSITIVE_ORDERcomparateur existant . La suggestion de @ tobias_k de comparer par longueur (ou toute autre propriété n'ayant pas de comparateur intégré) convient mieux. À en juger par des exemples de code SO Q&A, les lambdas ont causé beaucoup d'implémentations de comparateurs inutiles…
Holger
Suis-je le seul à penser que le deuxième exemple est plus facile à comprendre?
Clark Battle le
8

Itération interne

Lors de l'itération des collections Java, la plupart des développeurs ont tendance à obtenir un élément puis à le traiter . C'est à dire, retirez cet élément puis utilisez-le, ou réinsérez-le, etc. Avec les versions antérieures à 8 de Java, vous pouvez implémenter une classe interne et faire quelque chose comme:

numbers.forEach(new Consumer<Integer>() {
    public void accept(Integer value) {
        System.out.println(value);
    }
});

Maintenant, avec Java 8, vous pouvez faire mieux et moins verbeux avec:

numbers.forEach((Integer value) -> System.out.println(value));

ou mieux

numbers.forEach(System.out::println);

Les comportements comme arguments

Devinez le cas suivant:

public int sumAllEven(List<Integer> numbers) {
    int total = 0;

    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    } 
    return total;
}

Avec l' interface Java 8 Predicate, vous pouvez faire mieux comme ceci:

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;

    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

En l'appelant comme:

sumAll(numbers, n -> n % 2 == 0);

Source: DZone - Pourquoi nous avons besoin d'expressions Lambda en Java

aussi
la source
Le premier cas est probablement un moyen de sauvegarder quelques lignes de code, mais cela rend le code plus facile à lire
également
4
En quoi l'itération interne est-elle une fonctionnalité lambda? Le fait est que techniquement, les lambdas ne vous permettent pas de faire quoi que ce soit que vous ne pouviez pas faire avant java-8. C'est juste un moyen plus concis de passer du code.
Ousmane D.
@ Aominè Dans ce cas, numbers.forEach((Integer value) -> System.out.println(value));l'expression lambda est composée de deux parties, l'une à gauche du symbole fléché (->) listant ses paramètres et celle de droite contenant son corps . Le compilateur détecte automatiquement que l'expression lambda a la même signature de la seule méthode non implémentée de l'interface Consumer.
également
5
Je n'ai pas demandé ce qu'est une expression lambda, je dis simplement que l'itération interne n'a rien à voir avec les expressions lambda.
Ousmane D.
1
@ Aominè Je vois votre point de vue, il n'a rien avec les lambdas en soi , mais comme vous le faites remarquer, cela ressemble plus à une manière plus propre d'écrire. Je pense qu'en termes d'efficacité, c'est aussi bon que les versions antérieures à 8
également
4

Il y a de nombreux avantages à utiliser des lambdas au lieu de la classe interne comme ci-dessous:

  • Rendre le code plus compact et expressif sans introduire plus de sémantique de syntaxe de langage. vous avez déjà donné un exemple dans votre question.

  • En utilisant des lambdas, vous êtes heureux de programmer avec des opérations de style fonctionnel sur des flux d'éléments, telles que des transformations de réduction de carte sur des collections. voir la documentation des packages java.util.function et java.util.stream .

  • Il n'y a pas de fichier de classes physiques généré pour les lambdas par le compilateur. Ainsi, cela rend vos applications livrées plus petites. Comment la mémoire affecte-t-elle lambda?

  • Le compilateur optimisera la création lambda si le lambda n'accède pas aux variables hors de sa portée, ce qui signifie que l'instance lambda ne sera créée qu'une seule fois par la JVM. pour plus de détails, vous pouvez voir la réponse de @ Holger à la question La mise en cache de référence de méthode est-elle une bonne idée dans Java 8? .

  • Lambdas peut implémenter plusieurs interfaces de marqueurs en plus de l'interface fonctionnelle, mais les classes internes anonymes ne peuvent pas implémenter plus d'interfaces, par exemple:

    //                 v--- create the lambda locally.
    Consumer<Integer> action = (Consumer<Integer> & Serializable) it -> {/*TODO*/};
holi-java
la source
2

Les lambdas ne sont que du sucre syntaxique pour les classes anonymes.

Avant les lambdas, les classes anonymes peuvent être utilisées pour réaliser la même chose. Chaque expression lambda peut être convertie en une classe anonyme.

Si vous utilisez IntelliJ IDEA, il peut effectuer la conversion pour vous:

  • Placez le curseur dans le lambda
  • Appuyez sur alt / option + entrée

entrez la description de l'image ici

Balayeuse
la source
3
... Je pensais que @StuartMarks avait déjà clarifié le sucre syntaxique (sp? Gr?) Des lambdas? ( stackoverflow.com/a/15241404/3475600 , softwareengineering.stackexchange.com/a/181743 )
srborlongan
2

Pour répondre à votre question, le fait est que les lambdas ne vous permettent pas de faire quoi que ce soit que vous ne pouviez pas faire avant java-8, mais plutôt d'écrire du code plus concis . L'avantage de cela, c'est que votre code sera plus clair et plus flexible.

Ousmane D.
la source
Clairement, @Holger a dissipé tout doute en termes d'efficacité lambda. Ce n'est pas seulement un moyen de rendre le code plus propre, mais si lambda ne capture aucune valeur, ce sera une classe Singleton (réutilisable)
également
Si vous creusez en profondeur, chaque fonctionnalité linguistique a une différence. La question à portée de main est à un niveau élevé, c'est pourquoi j'ai écrit ma réponse à un niveau élevé.
Ousmane D.
2

Une chose que je ne vois pas encore mentionnée est qu'un lambda vous permet de définir les fonctionnalités là où il est utilisé .

Donc, si vous avez une fonction de sélection simple, vous n'avez pas besoin de la mettre dans un endroit séparé avec un tas de passe-partout, vous écrivez simplement un lambda qui est concis et localement pertinent.

Carlos
la source
1

Oui, de nombreux avantages sont là.

  • Pas besoin de définir toute la classe, nous pouvons passer l'implémentation de la fonction elle-même comme référence.
    • La création interne de la classe créera un fichier .class tandis que si vous utilisez lambda, la création de classe est évitée par le compilateur car dans lambda vous transmettez l'implémentation de la fonction au lieu de la classe.
  • La réutilisation du code est plus élevée qu'avant
  • Et comme vous l'avez dit, le code est plus court que l'implémentation normale.
Sunil Kanzar
la source