El lado oscuro de la expresiones Lambda en Java 8

 ● 15th May 2015

13 min read

Este artículo no va a generarme nuevos amigos. Bueno, aunque tampoco es que yo fuera muy popular que digamos en la escuela. Pero bueno, vayamos al punto. La funcionalidad más importante incorporada en Java 8, en lo que se refiere al lenguaje, sin lugar a dudas es la de las expresiones Lambda. Ya han resultado ser el buque insignia entre las funcionalidades de otros lenguajes funcionales, tales como Scala y Clojure desde hace algunos años, y ahora, finalmente, Java se ha sumado a la flota.
La segunda funcionalidad en relevancia (depende de a quién le preguntes) es Nashorn: el nuevo motor de JavaScript de la JVM que está pensado para llevar a Java a la par de otros motores JS, tales como V8 y su contenedor, node.js.
Pero estas nuevas funcionalidades también tienen su lado oscuro.
Paso a explicarme. La plataforma Java está constituida por dos componentes esenciales: el JRE, que compila en tiempo de ejecución (JIT) y ejecuta el bytecode, y el JDK, que contiene herramientas de desarrollo y el compilador javac de código fuente. Estos dos componentes son bastante (aunque no totalmente) independientes, lo cual permite a los colegas escribir sus propios lenguajes de la JVM, con Scala habiendo alcanzado prominencia en los últimos años. Y allí es donde reside parte del problema.
La JVM fue pensada para ser agnóstica en términos de lenguaje, en el sentido de que pueda ejecutar código escrito en cualquiera de ellos, siempre y cuando el mismo pueda ser traducido a bytecode. La especificación del bytecode es, en sí misma, orientada a objetos, y fue diseñada para corresponderse adecuadamente con el lenguaje Java. Eso significa que el bytecode que haya sido compilado a partir de código fuente Java, va a tener una estructura muy parecida.
Pero cuanto más te alejas de Java, más crece la distancia. Si observas a Scala, que es un lenguaje funcional, vas a ver una distancia bastante importante entre el código fuente y el bytecode ejecutado. El compilador añade grandes cantidades de variables, métodos y clases sintéticas, para que la JVM ejecute los controles semánticos y de flujo requeridos por el lenguaje.
En los lenguajes completamente dinámicos, tales como JavaScript, esa distancia se multiplica.
Y ahora, con Java 8, lentamente, esto también se está contagiando a Java.

¿Y en qué me afecta eso a mí?

Ciertamente desearía que esta fuera una mera discusión teórica, interesante pero sin ninguna implicación en la práctica, en nuestro trabajo cotidiano. Pero, tristemente, sí que las tiene, y en gran manera. Con la tendencia de agregar nuevos elementos a Java, la distancia entre tu código y el tiempo de ejecución crece, lo cual significa que lo que estés escribiendo y lo que estés depurando serán dos cosas distintas.
Para explicar cómo ocurre eso, veamos el ejemplo de más abajo.

Java 6 y 7

Este es el método tradicional, en el cual iteraríamos a través de una lista de strings para analizar y correlacionar su longitud.
[java]
// simple check against empty strings
public static int check(String s) {
if (s.equals(“”)) {
throw new IllegalArgumentException();
}
return s.length();
}
//map names to lengths
List lengths = new ArrayList();
for (String name : Arrays.asList(args)) {
lengths.add(check(name));
}
[/java]
Esto arrojará una excepción si se pasa un string vacío. La traza de la pila (stack trace) va a ser algo así:
[java]
at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.main(LmbdaMain.java:34)
[/java]
Aquí vemos una relación de 1:1 entre la traza de la pila que vemos y el código que escribimos, lo cual hace que depurar esta pila de llamadas (call stack) sea una tarea más sencilla. Esto es a lo que los desarrolladores Java están acostumbrados.
Ahora, vamos a dar un vistazo a Scala y Java 8.


Desarrolladores Java / Scala: Sepan cuándo y por qué el código falla en producción. Leer más
rec1


Scala

Veamos el mismo código en Scala. Aquí nos encontramos con dos grandes diferencias. La primera es el uso de una expresión Lambda para analizar y correlacionar las longitudes, y la segunda radica en que la iteración es llevada a cabo por el framework (es decir, se trata de iteración interna).
[scala]
val lengths = names.map(name => check(name.length))
[/scala]
Aquí es donde realmente comenzamos a notar la diferencia entre cómo luce el código que tú escribiste, y cómo lo verá la JVM (y también tú mismo) en el tiempo de ejecución. Si se arrojara una excepción, la pila de llamadas sería una orden de magnitud más larga, y mucho más difícil de entender.
[scala]
at Main$.check(Main.scala:6)
at Main$$anonfun$1.apply(Main.scala:12)
at Main$$anonfun$1.apply(Main.scala:12)
at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
at scala.collection.immutable.List.foreach(List.scala:318)
at scala.collection.TraversableLike$class.map(TraversableLike.scala:244)
at scala.collection.AbstractTraversable.map(Traversable.scala:105)
at Main$delayedInit$body.apply(Main.scala:12)
at scala.Function0$class.apply$mcV$sp(Function0.scala:40)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.collection.immutable.List.foreach(List.scala:318)
at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32)
at scala.App$class.main(App.scala:71)
at Main$.main(Main.scala:1)
at Main.main(Main.scala)
[/scala]
* Recuerda que este ejemplo es muy simple. En la vida real, con Lambdas anidadas y estructuras más complejas te vas a enfrentar a pilas de llamadas sintéticas mucho más largas, a partir de las cuales necesitarás llegar a entender qué ocurrió.
Este ha sido un inconveniente de Scala desde hace mucho tiempo, y asimismo una de las razones por las que nosotros creamos el Stackifier de Scala.

Y ahora en Java 8

Hasta hoy, los desarrolladores Java eran bastante inmunes a todo esto. Esto va a cambiar desde que las expresiones Lambda se conviertan en una parte integral de Java. Veamos el código respectivo en Java 8, y la pila de llamadas resultante.

[java]
Stream lengths = names.stream().map(name -> check(name));
at LmbdaMain.check(LmbdaMain.java:19)
at LmbdaMain.lambda$0(LmbdaMain.java:37)
at LmbdaMain$$Lambda$1/821270929.apply(Unknown Source)
at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.util.stream.LongPipeline.reduce(LongPipeline.java:438)
at java.util.stream.LongPipeline.sum(LongPipeline.java:396)
at java.util.stream.ReferencePipeline.count(ReferencePipeline.java:526)
at LmbdaMain.main(LmbdaMain.java:39)
[/java]
Esto está volviéndose muy parecido a Scala. Estamos pagando el precio por obtener un código más breve y conciso con depuraciones más complejas y pilas de llamadas sintéticas más largas.
¿La razón? Mientras javac ha sido extendido para soportar funciones Lambda, la JVM todavía resulta ajena para ellos. Esta ha sido una decisión de diseño tomada por los amigos de Java, buscando que la JVM continúe operando a un bajo nivel, y sin introducir nuevos elementos en su especificación.
Y lo cierto es que, aunque lo beneficioso de esta decisión pueda ser debatible, ella implica que, como desarrolladores Java -nos guste o no- el costo de entender estas pilas de llamadas cuando recibimos un ticket, ahora reposa sobre nuestros hombros.

JavaScript en Java 8

Java 8 presenta un compilador JavaScript completamente nuevo. Ahora, por fin podemos integrar Java + JS de una manera simple y eficiente. De todos modos, en ningún lugar vamos a hallar una mayor disonancia que aquí entre el código que escribimos y el código que depuramos.
He aquí la misma función, ahora en Nashorn:
[java]
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName(“nashorn”);
String js = “var map = Array.prototype.map \n”;
js += “var a = map.call(names, function(name) { return Java.type(\”LmbdaMain\”).check(name) }) \n”;
js += “print(a)”;
engine.eval(js);
[/java]
En este caso, el código bytecode es generado de manera dinámica en el tiempo de ejecución, usando un árbol anidado de expresiones Lambda. Hay muy poca relación entre nuestro código fuente y el bytecode resultante, ejecutado por la JVM. La pila de llamadas ahora es dos órdenes de magnitud más larga. Haciéndonos eco de las angustiosas palabras de Mr. T, me compadezco de los tontos que necesitarán depurar la pila de llamadas que obtendremos aquí.
¿Preguntas, comentarios? (Asumiendo que puedas recorrer todo el camino hacia abajo a través de esta pila de llamadas). Realízalos en la sección de comentarios.

[java]
LmbdaMain [Java Application]
LmbdaMain at localhost:51287
Thread [main] (Suspended (breakpoint at line 16 in LmbdaMain))
LmbdaMain.wrap(String) line: 16
1525037790.invokeStatic_L_I(Object, Object) line: not available
1150538133.invokeSpecial_LL_I(Object, Object, Object) line: not available
538592647.invoke_LL_I(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
2150540.interpret_I(MethodHandle, Object, Object) line: not available
538592647.invoke_LL_I(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
92150540.interpret_I(MethodHandle, Object, Object) line: not available
38592647.invoke_LL_I(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
731260860.interpret_L(MethodHandle, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LL_L(MethodHandle, Object[]) line: 1108
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
2619171.interpret_L(MethodHandle, Object, Object, Object) line: not available
1597655940.invokeSpecial_LLLL_L(Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLL_L(MethodHandle, Object[]) line: 1118
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
2619171.interpret_L(MethodHandle, Object, Object, Object) line: not available
1353530305.linkToCallSite(Object, Object, Object, Object) line: not available
Script$\^eval\_._L3(ScriptFunction, Object, Object) line: 3
1596000437.invokeStatic_LLL_L(Object, Object, Object, Object) line: not available
1597655940.invokeSpecial_LLLL_L(Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLL_L(MethodHandle, Object[]) line: 1118
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
484673893.interpret_L(MethodHandle, Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLLL_L(MethodHandle, Object[]) line: 1123
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
282496973.interpret_L(MethodHandle, Object, Object, Object, long, Object) line: not available
93508253.invokeSpecial_LLLLJL_L(Object, Object, Object, Object, Object, long, Object) line: not available
1850777594.invoke_LLLLJL_L(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
282496973.interpret_L(MethodHandle, Object, Object, Object, long, Object) line: not available
293508253.invokeSpecial_LLLLJL_L(Object, Object, Object, Object, Object, long, Object) line: not available
1850777594.invoke_LLLLJL_L(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
1840903588.interpret_L(MethodHandle, Object, Object, Object, Object, long, Object) line: not available
2063763486.reinvoke(Object, Object, Object, Object, Object, long, Object) line: not available
850777594.invoke_LLLLJL_L(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
82496973.interpret_L(MethodHandle, Object, Object, Object, long, Object) line: not available
220309324.invokeExact_MT(Object, Object, Object, Object, long, Object, Object) line: not available
NativeArray$10.forEach(Object, long) line: 1304
NativeArray$10(IteratorAction).apply() line: 124
NativeArray.map(Object, Object, Object) line: 1315
1596000437.invokeStatic_LLL_L(Object, Object, Object, Object) line: not available
504858437.invokeExact_MT(Object, Object, Object, Object, Object) line: not available
FinalScriptFunctionData(ScriptFunctionData).invoke(ScriptFunction, Object, Object…) line: 522
ScriptFunctionImpl(ScriptFunction).invoke(Object, Object…) line: 207
ScriptRuntime.apply(ScriptFunction, Object, Object…) line: 378
NativeFunction.call(Object, Object…) line: 161
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
1740189450.invokeSpecial_LLL_L(Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLL_L(MethodHandle, Object[]) line: 1113
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
2619171.interpret_L(MethodHandle, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLL_L(MethodHandle, Object[]) line: 1113
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
323326911.interpret_L(MethodHandle, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLL_L(MethodHandle, Object[]) line: 1118
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
323326911.interpret_L(MethodHandle, Object, Object, Object, Object) line: not available
263793464.invokeSpecial_LLLLL_L(Object, Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLLL_L(MethodHandle, Object[]) line: 1123
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
1484673893.interpret_L(MethodHandle, Object, Object, Object, Object, Object) line: not available
587003819.invokeSpecial_LLLLLL_L(Object, Object, Object, Object, Object, Object, Object) line: not available
811301908.invoke_LLLLLL_L(MethodHandle, Object[]) line: not available
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
484673893.interpret_L(MethodHandle, Object, Object, Object, Object, Object) line: not available
LambdaForm$NamedFunction.invoke_LLLLL_L(MethodHandle, Object[]) line: 1123
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
LambdaForm$NamedFunction.invokeWithArguments(Object…) line: 1147
LambdaForm.interpretName(LambdaForm$Name, Object[]) line: 625
LambdaForm.interpretWithArguments(Object…) line: 604
323326911.interpret_L(MethodHandle, Object, Object, Object, Object) line: not available
2129144075.linkToCallSite(Object, Object, Object, Object, Object) line: not available
Script$\^eval\_.runScript(ScriptFunction, Object) line: 3
1076496284.invokeStatic_LL_L(Object, Object, Object) line: not available
1709804316.invokeExact_MT(Object, Object, Object, Object) line: not available
FinalScriptFunctionData(ScriptFunctionData).invoke(ScriptFunction, Object, Object…) line: 498
ScriptFunctionImpl(ScriptFunction).invoke(Object, Object…) line: 207
ScriptRuntime.apply(ScriptFunction, Object, Object…) line: 378
NashornScriptEngine.evalImpl(ScriptFunction, ScriptContext, ScriptObject) line: 544
NashornScriptEngine.evalImpl(ScriptFunction, ScriptContext) line: 526
NashornScriptEngine.evalImpl(Source, ScriptContext) line: 522
NashornScriptEngine.eval(String, ScriptContext) line: 193
NashornScriptEngine(AbstractScriptEngine).eval(String) line: 264
LmbdaMain.main(String[]) line: 44
[/java]


Java Developers – Know when and why production code breaks – Start Your Free Trial

As a co-founder and CTO, Tal is responsible for overseeing OverOps' product and engineering strategy. Previously, Tal was co-founder and CEO at VisualTao, acquired by Autodesk Inc. (ADSK). Following that, Tal was the Director for the AutoCAD global Cloud and Mobile product line. Plays Jazz drums and Skypes, sometimes simultaneously.

Troubleshooting Apache Spark Applications with OverOps OverOps’ ability to detect precisely why something broke and to see variable state is invaluable in a distributed compute environment.
Troubleshooting Apache Spark Applications with OverOps

Next Article

The Fastest Way to Why.

Eliminate the detective work of searching logs for the Cause of critical issues. Resolve issues in minutes.
Learn More