¿Qué hay detrás de las lambdas de Java 8?

En este post trato de recoger, en español y de forma más resumida, buena parte del contenido del artículo: Java 8 Lambdas - A Peek Under the Hood.

¿Qué es una expresión lambda?

 Las expresiones lambdas y el API Streams son dos de los añadidos estrella que nos trajo Java 8. Podemos explicar de forma muy sencilla qué es una expresión lambda: se trata de una función anónima, no ligada a un identificador concreto.

¿Para qué nos sirve una función anónima en Java? Pues, entre otros casos, para la infinidad de ocasiones en que hemos querido parametrizar comportamiento, pasando como parámetro a un método la lógica de una función... y en lugar de eso nos ha tocado crearnos una clase (o clase anónima) con un método conteniendo el comportamiento y pasar esa clase como argumento.
Por ejemplo,  muchas APIs de Java tienen como parámetro la interfaz Comparator. Esta interfaz tiene un único método, compare, que recibe dos objetos a comparar y devuelve un entero. Si quisiéramos ordenar una lista de clientes por su edad, tendríamos que crear una clase que implemente el interfaz comparator para realizar esta tarea. Con expresiones lamba, podemos simplemente incluir la lógica de la comparación que necesitamos haciendo esto:
Si el argumento del método sort es un Comparator, ¿por qué puedo utilizar una expresión lambda en lugar de tener que pasar una instancia de un objeto que implemente esa interfaz? Comparator es una interfaz funcional, es decir: es una interfaz con un solo método abstracto (compare), también conocidas como interfaces SAM (single abstract method). Y Java 8 permite utilizar expresiones lambda allí donde se puede emplear una interfaz funcional.

Esto nos lleva a pensar que quizá allí donde usamos una expresión lambda, el compilador Java crea una inner class anónima que implementa la intefaz funcional correspondiente.

¿Son las expresiones lambda azúcar sintáctico de clases anónimas?

Bueno, esta podría haber sido perfectamente una posible implementación para las lambdas, pero nos habríamos encontrado con varios problemas que afectarían al rendimiento de las aplicaciones:
  • Cada expresión daría lugar a una nueva clase anónima y por tanto a un nuevo fichero class que la máquina virtual tiene que cargar y verificar, impactando en el arranque de la apliación.
  • En ejecución, estas clases ocuparían espacio en el meta-space de la JVM y el código máquina generado se almacenaría en caché, dando lugar a un mayor consumo de memoria por parte de las aplicaciones.
  • Por último, implementar lamdbas como clases anónimas habría impedido evolucionar en el futuro a una mejor implementación, pues se rompería la compatibilidad.
Como consecuencia de todo esto, los ingenieros buscaron una alternativa diferente que permitiera evolucionar a diferentes estrategias de implementación en el futuro.

 

Uso de invokedynamic para implementar expresiones lambda

 La instrucción de bytecode invokedynamic incluida en Java 7 permite seleccionar la estrategia de traducción en tiempo de ejecución, de modo que fue el camino elegido por los ingenieros. La generación de bytecode a partir de lambdas se realiza en dos pasos:

1) Generar un punto de llamada a invokedynamic

 Este paso consiste en generar el punto de invocación a invokedynamic, que devolverá una instancia de la interfaz funcional a la que se va a convertir la expresión lamda.

Por ejemplo, supongamos que tenemos una clase sencilla que contiene una expresión lambda:
Esto se traduciría en el siguiente bytecode:

 

2) Convertir el cuerpo de la expresión lambda en un método

Este segundo paso consiste en crear el método que será invocado mediante la instrucción invokedynamic. Cómo se realiza exactamente, depende de si la expresión lambda es capturing (accede a variables definidos fuera de su cuerpo) o non-capturing.

Las lambdas non-capturing son las más sencillas. Simplemente se genera un método con la misma signatura que la expresión lambda y este método se declara en la misma clase en que se utiliza la expresión. Por ejemplo, la lambda de la clase que hemos definido más arriba daría lugar a este método (observa que el nombre del método se generaría automáticamente a partir del nombre de la clase y añadiendo el símbolo $ seguido de un número):

Si se trata de lambdas de tipo capturing, entonces es necesario pasar las variables capturadas como argumentos del método generado. La estrategia de traducción normalmente consiste en añadir esas variables como parámetros al comienzo de la signatura del método. Por ejemplo, si tuviéramos:
El método generado podría ser así:

Conclusiones

Espero que en este artículo nos hayamos quedado con la idea de que la implementación actual de las lambdas proporciona buenos números en cuanto a rendimiento y consumo de memoria (frente al uso de clases anónimas). De hecho, Scala generaba inner classes para las expresiones lambda y a partir de Scala 2.12 utilizan también invokedynamic.

Adicionalmente, el uso de invokedynamic, permite explorar futuras optimizaciones para mejorar aún más el comportamiento en ciertas situaciones. Recibiremos con los brazos abiertos cualquier optimización que se nos presente :-)

Comentarios

Entradas populares de este blog

Creando un proyecto con Spring Initializr: diferencias entre un empaquetado jar y un war

Revisando la jerarquía de dependencias de Spring Cloud

Cómo funcionan los reintentos en los clientes del SDK de Amazon Web Services