¿Las expresiones lambda tienen algún otro uso que no sea guardar líneas de código?

120

¿Las expresiones lambda tienen algún otro uso que no sea guardar líneas de código?

¿Existen características especiales proporcionadas por lambdas que resolvieron problemas que no eran fáciles de resolver? El uso típico que he visto es que en lugar de escribir esto:

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

Podemos usar una expresión lambda para acortar el código:

Comparator<Developer> byName =
(Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName());
Vikash
fuente
27
Tenga en cuenta que puede ser aún más corto: (o1, o2) -> o1.getName().compareTo(o2.getName())
Thorbjørn Ravn Andersen
88
o aún más corto:Comparator.comparing(Developer::getName)
Thomas Kläger
31
No subestimaría ese beneficio. Cuando las cosas son mucho más fáciles, es más probable que haga las cosas de la manera "correcta". A menudo existe cierta tensión entre escribir código que sea más fácil de mantener a largo plazo y más fácil de anotar a corto plazo. Las lambdas reducen esa tensión y, por lo tanto, lo ayudan a escribir un código más limpio.
yshavit
5
Las lambdas son cierres, lo que hace que el ahorro de espacio sea aún mayor, ya que ahora no tiene que agregar un constructor parametrizado, campos, código de asignación, etc. a su clase de reemplazo.
CodesInChaos

Respuestas:

116

Las expresiones lambda no cambian el conjunto de problemas que puede resolver con Java en general, pero definitivamente facilitan la resolución de ciertos problemas, por la misma razón que ya no estamos programando en lenguaje ensamblador. Eliminar las tareas redundantes del trabajo del programador facilita la vida y permite hacer cosas que ni siquiera tocarías de otra manera, solo por la cantidad de código que tendrías que producir (manualmente).

Pero las expresiones lambda no solo guardan líneas de código. Las expresiones lambda le permiten definir funciones , algo para lo que antes podía usar clases internas anónimas como solución alternativa, por eso puede reemplazar las clases internas anónimas en estos casos, pero no en general.

En particular, las expresiones lambda se definen independientemente de la interfaz funcional a la que se convertirán, por lo que no hay miembros heredados a los que puedan acceder, además, no pueden acceder a la instancia del tipo que implementa la interfaz funcional. Dentro de una expresión lambda, thisy supertienen el mismo significado que en el contexto circundante, consulte también esta respuesta . Además, no puede crear nuevas variables locales que sombreen las variables locales del contexto circundante. Para la tarea prevista de definir una función, esto elimina muchas fuentes de error, pero también implica que para otros casos de uso, puede haber clases internas anónimas que no se pueden convertir en una expresión lambda, incluso si se implementa una interfaz funcional.

Además, la construcción new Type() { … }garantiza producir una nueva instancia distinta (como newsiempre lo hace). Las instancias de clases internas anónimas siempre mantienen una referencia a su instancia externa si se crean en un staticcontexto no . Por el contrario, las expresiones lambda solo capturan una referencia thiscuando es necesario, es decir, si acceden thiso un no staticmiembro. Y producen instancias de una identidad no especificada intencionalmente, lo que permite que la implementación decida en tiempo de ejecución si reutiliza las instancias existentes (consulte también “ ¿Una expresión lambda crea un objeto en el montón cada vez que se ejecuta? ”).

Estas diferencias se aplican a su ejemplo. Su construcción de clase interna anónima siempre producirá una nueva instancia, también puede capturar una referencia a la instancia externa, mientras que su (Developer o1, Developer o2) -> o1.getName().compareTo(o2.getName())es una expresión lambda que no captura y que evaluará a un singleton en implementaciones típicas. Además, no .classgenera un archivo en su disco duro.

Dadas las diferencias en cuanto a semántica y rendimiento, las expresiones lambda pueden cambiar la forma en que los programadores resolverán ciertos problemas en el futuro, por supuesto, también debido a que las nuevas API adoptan ideas de programación funcional utilizando las nuevas características del lenguaje. Consulte también Expresión lambda de Java 8 y valores de primera clase .

Holger
fuente
1
Esencialmente, las expresiones lambda son un tipo de programación funcional. Dado que cualquier tarea se puede realizar con programación funcional o programación imperativa, no hay ninguna ventaja, excepto la legibilidad y la capacidad de mantenimiento , que pueden superar otras preocupaciones.
Draco18s ya no confía en SE
1
@Trilarion: podrías llenar libros enteros con la definición de función . También tendría que decidir si centrarse en el objetivo o en los aspectos compatibles con las expresiones lambda de Java. El último enlace de mi respuesta ( este ) ya discute algunos de estos aspectos en el contexto de Java. Pero para entender mi respuesta, ya es suficiente entender que las funciones son un concepto diferente a las clases. Para más detalles, ya existen recursos y preguntas y respuestas…
Holger
1
@Trilarion: ambas funciones no permiten resolver más problemas, a menos que considere inaceptable la cantidad de tamaño / complejidad del código o las soluciones que necesitan otras soluciones (como se dijo en el primer párrafo), y ya existía una forma de definir funciones , como se dijo, las clases internas podrían usarse como solución alternativa para la ausencia de una mejor manera de definir funciones (pero las clases internas no son funciones, ya que pueden servir para muchos más propósitos). Es lo mismo que con OOP: no permite hacer nada que no puedas hacer con el ensamblaje, pero aún así, ¿puedes crear una imagen de una aplicación como Eclipse escrita en ensamblador?
Holger
3
@EricDuminil: para mí, Turing Completeness es demasiado teórico. Por ejemplo, si Java es Turing completo o no, no puede escribir un controlador de dispositivo de Windows en Java. (O en PostScript, que también es Turing completo) Agregar funciones a Java que permitan hacer eso, cambiaría el conjunto de problemas que puede resolver con él, sin embargo, eso no sucederá. Por eso prefiero el punto de reunión; Dado que todo lo que puede hacer se implementa en instrucciones ejecutables de la CPU, no hay nada que no pueda hacer en Ensamblador que admita todas las instrucciones de la CPU (también más fácil de entender que el formalismo de la máquina de Turing).
Holger
2
@abelito es común a todas las construcciones de programación de nivel superior, para proporcionar menos "visibilidad de lo que realmente está pasando", ya que de eso se trata, menos detalles, más enfoque en el problema de programación real. Puede usarlo para mejorar o empeorar el código; es solo una herramienta. Como la función de votación; es una herramienta que puede utilizar de la manera prevista, para proporcionar un juicio fundamentado sobre la calidad real de una publicación. O lo usa para rechazar una respuesta elaborada y razonable, solo porque odia las lambdas.
Holger
52

Los lenguajes de programación no son para que las máquinas los ejecuten.

Son para que los programadores piensen en ellos.

Los lenguajes son una conversación con un compilador para convertir nuestros pensamientos en algo que una máquina pueda ejecutar. Una de las principales quejas sobre Java de las personas que acuden a ella desde otras lenguas (o dejan que para otros idiomas) que solía ser que obliga a un determinado modelo mental en el programador (es decir, todo lo que es una clase).

No voy a opinar si eso es bueno o malo: todo son compensaciones. Pero las lambdas de Java 8 permiten a los programadores pensar en términos de funciones , que es algo que antes no podía hacer en Java.

Es lo mismo que un programador procedimental que aprende a pensar en términos de clases cuando llega a Java: los ves pasar gradualmente de clases que son estructuras glorificadas y tienen clases 'auxiliares' con un montón de métodos estáticos y pasan a algo que se parece más a un diseño OO racional (mea culpa).

Si solo piensa en ellos como una forma más corta de expresar clases internas anónimas, entonces probablemente no los encontrará muy impresionantes de la misma manera que el programador de procedimientos anterior probablemente no pensó que las clases fueran una gran mejora.

Jared Smith
fuente
5
Sin embargo, ten cuidado con eso, solía pensar que OOP lo era todo, y luego me confundí horriblemente con las arquitecturas de sistema de componente de entidad (¿qué significa que no tienes una clase para cada tipo de objeto de juego?! Y cada objeto de juego ¿no es un objeto OOP ?!)
user253751
3
En otras palabras, pensar en programación orientada a objetos, en lugar de simplemente pensar en * o f * clases como otra herramienta, restringió mi pensamiento de una mala manera.
user253751
2
@immibis: hay muchas formas diferentes de "pensar en POO", así que además del error común de confundir "pensar en POO" con "pensar en clases", hay muchas formas de pensar de forma contraproducente. Desafortunadamente, eso sucede con demasiada frecuencia desde el principio, ya que incluso los libros y los tutoriales enseñan a los inferiores, muestran patrones en contra de los ejemplos y difunden ese pensamiento. La supuesta necesidad de "tener una clase para cada tipo de" lo que sea, es solo un ejemplo, que contradice el concepto de abstracción, de la misma manera, parece existir el mito de "POO significa escribir la mayor cantidad de repetición posible", etc.
Holger
@immibis, aunque en ese caso no te ayudó, esa anécdota en realidad apoya el punto: el lenguaje y el paradigma proporcionan un marco para estructurar tus pensamientos y convertirlos en un diseño. En su caso, chocó contra los bordes del marco, pero en otro contexto, hacer eso podría haberlo ayudado a no caer en una gran bola de barro y producir un diseño feo. Por lo tanto, supongo que "todo son compensaciones". Mantener el segundo beneficio y evitar el primer problema es una cuestión clave a la hora de decidir qué tan "grande" hacer un idioma.
Leushenko
@immibis Es por eso que es importante para aprender un montón de paradigma así : cada uno le llegue sobre las deficiencias de los demás.
Jared Smith
36

Guardar líneas de código puede verse como una nueva característica, si le permite escribir una parte sustancial de la lógica de una manera más corta y clara, lo que a otros les lleva menos tiempo leer y comprender.

Sin expresiones lambda (y / o referencias a métodos), las Streamcanalizaciones habrían sido mucho menos legibles.

Piense, por ejemplo, en cómo se Streamvería la siguiente canalización si reemplazara cada expresión lambda con una instancia de clase anónima.

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

Podría ser:

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());

Esto es mucho más difícil de escribir que la versión con expresiones lambda y es mucho más propensa a errores. También es más difícil de entender.

Y esta es una tubería relativamente corta.

Para que esto sea legible sin expresiones lambda y referencias a métodos, habría tenido que definir variables que contengan las diversas instancias de interfaz funcional que se utilizan aquí, lo que habría dividido la lógica de la canalización, lo que lo haría más difícil de entender.

Eran
fuente
17
Creo que esta es una idea clave. Se podría argumentar que algo es prácticamente imposible de hacer en un lenguaje de programación si la sobrecarga sintáctica y cognitiva es demasiado grande para ser útil, por lo que otros enfoques serán superiores. Por lo tanto, al reducir la sobrecarga sintáctica y cognitiva (como lo hacen las lambdas en comparación con las clases anónimas), las canalizaciones como las anteriores se vuelven posibles. Más irónico, si escribir un fragmento de código hace que te despidan de tu trabajo, se podría decir que no es posible.
Sebastian Redl
Nitpick: En realidad, no necesitas el Comparatorfor, sortedya que se compara en orden natural de todos modos. ¿Quizás cambiarlo para comparar la longitud de las cadenas o similar, para hacer el ejemplo aún mejor?
tobias_k
@tobias_k no hay problema. Lo cambié a compareToIgnoreCasepara que se comporte diferente a sorted().
Eran
1
compareToIgnoreCasesigue siendo un mal ejemplo, ya que simplemente podría utilizar el String.CASE_INSENSITIVE_ORDERcomparador existente . La sugerencia de @tobias_k de comparar por longitud (o cualquier otra propiedad que no tenga un comparador incorporado) encaja mejor. A juzgar por los ejemplos de código de SO Q&A, las lambdas causaron muchas implementaciones innecesarias de comparadores…
Holger
¿Soy el único que piensa que el segundo ejemplo es más fácil de entender?
Clark Battle
8

Iteración interna

Al iterar las colecciones de Java, la mayoría de los desarrolladores tienden a obtener un elemento y luego procesarlo . Es decir, saque ese elemento y luego úselo, o vuelva a insertarlo, etc. Con las versiones anteriores a 8 de Java, puede implementar una clase interna y hacer algo como:

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

Ahora con Java 8 puede hacerlo mejor y menos detallado con:

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

o mejor

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

Comportamientos como argumentos

Adivina el siguiente caso:

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

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

Con la interfaz de Java 8 Predicate puedes hacerlo mejor así:

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

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

Llamándolo como:

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

Fuente: DZone - Por qué necesitamos expresiones Lambda en Java

alseether
fuente
Probablemente el primer caso sea una forma de guardar algunas líneas de código, pero hace que el código sea más fácil de leer
además,
4
¿Cómo es la iteración interna una característica lambda? El simple hecho es que técnicamente las lambdas no le permiten hacer nada que no pudiera hacer antes de java-8. Es solo una forma más concisa de transmitir código.
Ousmane D.
@ Aominè En este caso, numbers.forEach((Integer value) -> System.out.println(value));la expresión lambda está formada por dos partes, la de la izquierda del símbolo de flecha (->) que enumera sus parámetros y la de la derecha que contiene su cuerpo . El compilador se da cuenta automáticamente de que la expresión lambda tiene la misma firma del único método no implementado de la interfaz del consumidor.
alseether
5
No pregunté qué es una expresión lambda, solo digo que la iteración interna no tiene nada que ver con las expresiones lambda.
Ousmane D.
1
@ Aominè Veo su punto, no tiene nada con lambdas per se , pero como usted señala, es más una forma más limpia de escribir. Creo que en términos de eficiencia es tan bueno como las versiones anteriores a 8
alseether
4

Hay muchos beneficios de usar lambdas en lugar de la clase interna que se muestra a continuación:

  • Haga que el código sea más compacto y expresivo sin introducir más semántica de sintaxis del lenguaje. ya dio un ejemplo en su pregunta.

  • Al usar lambdas, está feliz de programar con operaciones de estilo funcional en flujos de elementos, como transformaciones de reducción de mapas en colecciones. consulte la documentación de los paquetes java.util.function y java.util.stream .

  • El compilador no genera ningún archivo de clases físicas para lambdas. Por lo tanto, reduce las aplicaciones entregadas. ¿Cómo se asigna Memory a lambda?

  • El compilador optimizará la creación de lambda si el lambda no accede a las variables fuera de su alcance, lo que significa que la instancia de lambda solo se crea una vez por la JVM. para obtener más detalles, puede ver la respuesta de @ Holger a la pregunta ¿Es una buena idea el almacenamiento en caché de referencias de métodos en Java 8? .

  • Lambdas puede implementar interfaces de marcadores múltiples además de la interfaz funcional, pero las clases internas anónimas no pueden implementar más interfaces, por ejemplo:

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

Las lambdas son simplemente azúcar sintáctico para clases anónimas.

Antes de las lambdas, se pueden usar clases anónimas para lograr lo mismo. Cada expresión lambda se puede convertir en una clase anónima.

Si está utilizando IntelliJ IDEA, puede hacer la conversión por usted:

  • Pon el cursor en la lambda
  • Presione alt / opción + enter

ingrese la descripción de la imagen aquí

Barrendero
fuente
3
... Pensé que @StuartMarks ya había aclarado la dulzura sintáctica (¿sp? Gr?) De las lambdas? ( stackoverflow.com/a/15241404/3475600 , softwareengineering.stackexchange.com/a/181743 )
srborlongan
2

Para responder a su pregunta, el hecho es que las lambdas no le permiten hacer nada que no pudiera hacer antes de java-8, sino que le permite escribir un código más conciso . Los beneficios de esto es que su código será más claro y flexible.

Ousmane D.
fuente
Claramente @Holger ha disipado cualquier duda en términos de eficiencia lambda. No es solo una forma de hacer que el código sea más limpio, sino que si lambda no captura ningún valor, será una clase Singleton (reutilizable)
alseether
Si profundiza, cada característica del idioma tiene una diferencia. La pregunta en cuestión está en un nivel alto, por lo tanto, escribí mi respuesta en un nivel alto.
Ousmane D.
2

Una cosa que no veo mencionada todavía es que una lambda le permite definir la funcionalidad donde se usa .

Entonces, si tiene una función de selección simple, no necesita colocarla en un lugar separado con un montón de texto repetitivo, simplemente escriba una lambda que sea concisa y localmente relevante.

Carlos
fuente
1

Sí, hay muchas ventajas.

  • No es necesario definir toda la clase, podemos pasar la implementación de la función en sí misma como referencia.
    • La creación interna de la clase creará un archivo .class, mientras que si usa lambda, el compilador evita la creación de la clase porque en lambda está pasando la implementación de la función en lugar de la clase.
  • La reutilización del código es mayor que antes
  • Y como dijiste, el código es más corto que la implementación normal.
Sunil Kanzar
fuente