Analizando expresiones lambda en Java

imagen


De un traductor: LambdaMetafactory es quiz谩s uno de los mecanismos Java 8 m谩s subestimados. Lo descubrimos recientemente, pero ya apreciamos sus capacidades. La versi贸n 7.0 del marco CUBA mejora el rendimiento al evitar llamadas reflexivas a favor de generar expresiones lambda. Una de las aplicaciones de este mecanismo en nuestro marco es la vinculaci贸n de los controladores de eventos de la aplicaci贸n mediante anotaciones, una tarea com煤n, un an谩logo de EventListener de Spring. Creemos que el conocimiento de los principios de LambdaFactory puede ser 煤til en muchas aplicaciones Java, y nos apresuramos a compartir esta traducci贸n con usted.


En este art铆culo, mostraremos algunos trucos poco conocidos al trabajar con expresiones lambda en Java 8 y las limitaciones de estas expresiones. El p煤blico objetivo del art铆culo son los desarrolladores senior de Java, investigadores y desarrolladores de kits de herramientas. Solo se utilizar谩 la API Java p煤blica sin com.sun.* Y otras clases internas, por lo que el c贸digo es port谩til entre diferentes implementaciones de JVM.


Pr贸logo corto


Las expresiones Lambda aparecieron en Java 8 como una forma de implementar m茅todos an贸nimos y,
en algunos casos, como alternativa a las clases an贸nimas. En el nivel de bytecode, la expresi贸n lambda se reemplaza por la invokedynamic . Esta instrucci贸n se utiliza para crear una implementaci贸n de interfaz funcional y su 煤nico m茅todo delega la llamada al m茅todo real, que contiene el c贸digo definido en el cuerpo de la expresi贸n lambda.


Por ejemplo, tenemos el siguiente c贸digo:


 void printElements(List<String> strings){ strings.forEach(item -> System.out.println("Item = %s", item)); } 

El compilador de Java convertir谩 este c贸digo en algo similar a:


 private static void lambda_forEach(String item) { // Java  System.out.println("Item = %s", item); } private static CallSite bootstrapLambda(Lookup lookup, String name, MethodType type) { // //lookup =  VM //name = "lambda_forEach",  VM //type = String -> void MethodHandle lambdaImplementation = lookup.findStatic(lookup.lookupClass(), name, type); return LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(Consumer.class), //  - MethodType.methodType(void.class, Object.class), //  Consumer.accept    lambdaImplementation, //     - type); } void printElements(List<String> strings) { Consumer<String> lambda = invokedynamic# bootstrapLambda, #lambda_forEach strings.forEach(lambda); } 

La instrucci贸n invokedynamic se puede representar aproximadamente como dicho c贸digo Java:


 private static CallSite cs; void printElements(List<String> strings) { Consumer<String> lambda; //begin invokedynamic if (cs == null) cs = bootstrapLambda(MethodHandles.lookup(), "lambda_forEach", MethodType.methodType(void.class, String.class)); lambda = (Consumer<String>)cs.getTarget().invokeExact(); //end invokedynamic strings.forEach(lambda); } 

Como puede ver, LambdaMetafactory se utiliza para crear un CallSite que proporciona un m茅todo de f谩brica que devuelve un controlador para el m茅todo de destino. Este m茅todo devuelve la implementaci贸n de la interfaz funcional usando invokeExact . Si hay variables capturadas en la expresi贸n lambda, invokeExact acepta estas variables como par谩metros reales.


En Oracle JRE 8, metafactory genera din谩micamente una clase Java utilizando ObjectWeb Asm, que crea una clase que implementa una interfaz funcional. Se pueden agregar campos adicionales a la clase creada si la expresi贸n lambda captura variables externas. Este se parece a las clases an贸nimas de Java, pero existen las siguientes diferencias:


  • El compilador de Java genera una clase an贸nima.
  • La clase para implementar la expresi贸n lambda es creada por la JVM en tiempo de ejecuci贸n.



La implementaci贸n de metafactory depende del proveedor y la versi贸n de JVM




Por supuesto, la invokedynamic no solo se usa para expresiones lambda en Java. Se utiliza principalmente cuando se ejecutan lenguajes din谩micos en el entorno JVM. El motor de JavaScript de Nashorn , que est谩 integrado en Java, hace un uso intensivo de esta instrucci贸n.


A continuaci贸n, nos centraremos en la clase LambdaMetafactory y sus capacidades. Siguiente
La secci贸n de este art铆culo supone que comprende muy bien c贸mo funcionan los m茅todos metafactorios y qu茅 MethodHandle


Trucos con expresiones lambda


En esta secci贸n, mostraremos c贸mo construir lambdas din谩micas para su uso en tareas cotidianas.


Excepciones marcadas y lambdas


No es ning煤n secreto que todas las interfaces funcionales que existen en Java no admiten excepciones comprobadas. Las ventajas de las excepciones comprobadas sobre las regulares son un debate muy antiguo (y a煤n candente).


Pero, 驴qu茅 sucede si necesita usar c贸digo con excepciones marcadas dentro de expresiones lambda en combinaci贸n con Java Streams? Por ejemplo, debe convertir una lista de cadenas en una lista de URL como esta:


 Arrays.asList("http://localhost/", "https://github.com").stream() .map(URL::new) .collect(Collectors.toList()) 

Se declara una excepci贸n arrojable en el constructor de la URL (String) , por lo que no se puede usar directamente como referencia de m茅todo en la clase Functiion .


Dir谩s: "No, quiz谩s si usas este truco aqu铆":


 public static <T> T uncheckCall(Callable<T> callable) { try { return callable.call(); } catch (Exception e) { return sneakyThrow(e); } } private static <E extends Throwable, T> T sneakyThrow0(Throwable t) throws E { throw (E)t; } public static <T> T sneakyThrow(Throwable e) { return Util.<RuntimeException, T>sneakyThrow0(e); } //   //return s.filter(a -> uncheckCall(a::isActive)) // .map(Account::getNumber) // .collect(toSet()); 

Este es un truco sucio. Y aqu铆 est谩 el por qu茅:


  • Se utiliza el bloque try-catch.
  • La excepci贸n se lanza nuevamente.
  • El uso sucio del tipo borrado en Java.

El problema puede resolverse de una manera m谩s "legal", utilizando el conocimiento de los siguientes hechos:


  • Las excepciones marcadas se reconocen solo en el nivel del compilador Java.
  • La secci贸n de throws son solo metadatos para un m茅todo sin un valor sem谩ntico en el nivel JVM.
  • Las excepciones marcadas y normales no se pueden distinguir a nivel de bytecode en la JVM.

La soluci贸n es envolver el m茅todo Callable.call en un m茅todo sin una secci贸n de throws :


 static <V> V callUnchecked(Callable<V> callable){ return callable.call(); } 

Este c贸digo no se compila porque el m茅todo Callable.call declarado excepciones comprobadas en la secci贸n de throws . Pero podemos eliminar esta secci贸n usando una expresi贸n lambda construida din谩micamente.


Primero necesitamos declarar una interfaz funcional que no tenga una secci贸n de throws .
pero qui茅n podr谩 delegar la llamada a Callable.call :


 @FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);//  INVOKE <V> V invoke(final Callable<V> callable); } 

El segundo paso es crear una implementaci贸n de esta interfaz usando LambdaMetafactory y delegar la llamada del m茅todo Callable.call m茅todo Callable.call . Como se mencion贸 anteriormente, la secci贸n de throws se ignora en el nivel de bytecode, por lo que el m茅todo SilentInvoker.invoke puede llamar al m茅todo Callable.call sin declarar excepciones:


 private static final SilentInvoker SILENT_INVOKER; final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "invoke", MethodType.methodType(SilentInvoker.class), SilentInvoker.SIGNATURE, lookup.findVirtual(Callable.class, "call", MethodType.methodType(Object.class)), SilentInvoker.SIGNATURE); SILENT_INVOKER = (SilentInvoker) site.getTarget().invokeExact(); 

Tercero, escribimos un m茅todo auxiliar que llama a Callable.call sin declarar excepciones:


 public static <V> V callUnchecked(final Callable<V> callable) /*no throws*/ { return SILENT_INVOKER.invoke(callable); } 

Ahora puede reescribir la secuencia sin ning煤n problema con excepciones marcadas:


 Arrays.asList("http://localhost/", "https://dzone.com").stream() .map(url -> callUnchecked(() -> new URL(url))) .collect(Collectors.toList()); 

Este c贸digo se compila sin problemas porque callUnchecked no declara excepciones comprobadas. Adem谩s, llamar a este m茅todo se puede incorporar mediante el almacenamiento en cach茅 en l铆nea monom贸rfico , porque es solo una clase en toda la JVM que implementa la interfaz SilentOnvoker


Si la implementaci贸n de Callable.call arroja una excepci贸n en tiempo de ejecuci贸n, la funci贸n de llamada la detectar谩 sin ning煤n problema:


 try{ callUnchecked(() -> new URL("Invalid URL")); } catch (final Exception e){ System.out.println(e); } 

A pesar de las posibilidades de este m茅todo, siempre debe recordar la siguiente recomendaci贸n:




Oculte las excepciones marcadas con callUnchecked solo si est谩 seguro de que el c贸digo llamado no arrojar谩 ninguna excepci贸n




El siguiente ejemplo muestra un ejemplo de este enfoque:


 callUnchecked(() -> new URL("https://dzone.com")); // URL        MalformedURLException 

La implementaci贸n completa de este m茅todo est谩 aqu铆 , es parte del proyecto de c贸digo abierto SNAMP .


Trabajando con Getters y Setters


Esta secci贸n ser谩 煤til para quienes escriben serializaci贸n / deserializaci贸n para varios formatos de datos como JSON, Thrift, etc. Adem谩s, puede ser bastante 煤til si su c贸digo depende en gran medida de la reflexi贸n para Getters y Setters en JavaBeans.


Un getter declarado en JavaBean es un m茅todo llamado getXXX sin par谩metros y un tipo de datos de retorno que no sea void . Un setter declarado en JavaBean es un m茅todo llamado setXXX , con un par谩metro y regresando void . Estas dos notaciones se pueden representar como interfaces funcionales:


  • Getter puede ser representado por la clase Function , en la cual el argumento es el valor de this .
  • Setter puede ser representado por la clase BiConsumer , en la cual el primer argumento es this , y el segundo es el valor que se pasa a Setter.

Ahora crearemos dos m茅todos que pueden convertir cualquier getter o setter en estos
interfaces funcionales Y no importa que ambas interfaces sean gen茅ricas. Despu茅s de borrar tipos
El tipo de datos real ser谩 Object . La conversi贸n autom谩tica del tipo de retorno y los argumentos se pueden hacer usando LambdaMetafactory . Adem谩s, la biblioteca Guava ayudar谩 a almacenar en cach茅 las expresiones lambda para los mismos captadores y establecedores.


Primer paso: crear un cach茅 para captadores y establecedores. La clase Method de Reflection API representa un captador o setter real y se usa como clave.
El valor de cach茅 es una interfaz funcional construida din谩micamente para un getter o setter espec铆fico.


 private static final Cache<Method, Function> GETTERS = CacheBuilder.newBuilder().weakValues().build(); private static final Cache<Method, BiConsumer> SETTERS = CacheBuilder.newBuilder().weakValues().build(); 

En segundo lugar, crearemos m茅todos de f谩brica que creen una instancia de la interfaz funcional basada en referencias a getter o setter.


 private static Function createGetter(final MethodHandles.Lookup lookup, final MethodHandle getter) throws Exception{ final CallSite site = LambdaMetafactory.metafactory(lookup, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class), //signature of method Function.apply after type erasure getter, getter.type()); //actual signature of getter try { return (Function) site.getTarget().invokeExact(); } catch (final Exception e) { throw e; } catch (final Throwable e) { throw new Error(e); } } private static BiConsumer createSetter(final MethodHandles.Lookup lookup, final MethodHandle setter) throws Exception { final CallSite site = LambdaMetafactory.metafactory(lookup, "accept", MethodType.methodType(BiConsumer.class), MethodType.methodType(void.class, Object.class, Object.class), //signature of method BiConsumer.accept after type erasure setter, setter.type()); //actual signature of setter try { return (BiConsumer) site.getTarget().invokeExact(); } catch (final Exception e) { throw e; } catch (final Throwable e) { throw new Error(e); } } 

La conversi贸n autom谩tica de tipos entre argumentos de tipo Object en interfaces funcionales (despu茅s del borrado de tipo) y tipos reales de argumentos y valor de retorno se logra utilizando la diferencia entre samMethodType y instantiatedMethodType (el tercer y quinto argumento del m茅todo metafactorio, respectivamente). El tipo de la instancia creada del m茅todo: esta es la especializaci贸n del m茅todo que proporciona la implementaci贸n de la expresi贸n lambda.


En tercer lugar, crearemos una fachada para estas f谩bricas con soporte para el almacenamiento en cach茅:


 public static Function reflectGetter(final MethodHandles.Lookup lookup, final Method getter) throws ReflectiveOperationException { try { return GETTERS.get(getter, () -> createGetter(lookup, lookup.unreflect(getter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } } public static BiConsumer reflectSetter(final MethodHandles.Lookup lookup, final Method setter) throws ReflectiveOperationException { try { return SETTERS.get(setter, () -> createSetter(lookup, lookup.unreflect(setter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } } 

La informaci贸n del m茅todo obtenida de una instancia de la clase Method utiliza la API de Java Reflection se puede convertir f谩cilmente en un MethodHandle . Tenga en cuenta que los m茅todos de instancia de clase siempre tienen un primer argumento oculto utilizado para pasar this a este m茅todo. Los m茅todos est谩ticos no tienen dicho par谩metro. Por ejemplo, la firma real del m茅todo Integer.intValue() se ve como int intValue(Integer this) . Este truco se utiliza en nuestra implementaci贸n de envoltorios funcionales para captadores y establecedores.


Y ahora es el momento de probar el c贸digo:


 final Date d = new Date(); final BiConsumer<Date, Long> timeSetter = reflectSetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("setTime", long.class)); timeSetter.accept(d, 42L); //the same as d.setTime(42L); final Function<Date, Long> timeGetter = reflectGetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("getTime")); System.out.println(timeGetter.apply(d)); //the same as d.getTime() //output is 42 

Este enfoque con getters y setters almacenados en cach茅 se puede usar de manera efectiva en las bibliotecas de serializaci贸n / deserializaci贸n (como Jackson) que usan getters y setters durante la serializaci贸n y deserializaci贸n.




Llamar a interfaces funcionales con implementaciones generadas din谩micamente usando LambdaMetaFactory significativamente m谩s r谩pido que llamar a trav茅s de la API de Java Reflection




La versi贸n completa del c贸digo se puede encontrar aqu铆 , es parte de la biblioteca SNAMP .


Limitaciones y errores


En esta secci贸n, veremos algunos errores y limitaciones asociados con las expresiones lambda en el compilador de Java y JVM. Todas estas limitaciones se pueden reproducir en OpenJDK y Oracle JDK con javac versi贸n 1.8.0_131 para Windows y Linux.


Crear expresiones lambda a partir de manejadores de m茅todos


Como sabe, una expresi贸n lambda se puede construir din谩micamente usando LambdaMetaFactory . Para hacer esto, debe definir un controlador: la clase MethodHandle , que indica la implementaci贸n del 煤nico m茅todo definido en la interfaz funcional. Echemos un vistazo a este sencillo ejemplo:


 final class TestClass { String value = ""; public String getValue() { return value; } public void setValue(final String value) { this.value = value; } } final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findVirtual(TestClass.class, "getValue", MethodType.methodType(String.class)), MethodType.methodType(String.class)); final Supplier<String> getter = (Supplier<String>) site.getTarget().invokeExact(obj); System.out.println(getter.get()); 

Este c贸digo es equivalente a:


 final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final Supplier<String> elementGetter = () -> obj.getValue(); System.out.println(elementGetter.get()); 

Pero, 驴qu茅 sucede si reemplazamos el manejador de m茅todos que apunta a getValue con el manejador que representan los campos getter?


 final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findGetter(TestClass.class, "value", String.class), //field getter instead of method handle to getValue MethodType.methodType(String.class)); 

Este c贸digo deber铆a, como se esperaba, funcionar porque findGetter devuelve un controlador que apunta a los campos getter y tiene la firma correcta. Pero, si ejecuta este c贸digo, ver谩 la siguiente excepci贸n:


 java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField 

Curiosamente, el captador para el campo funciona bien si usamos MethodHandleProxies :


 final Supplier<String> getter = MethodHandleProxies .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class) .bindTo(obj)); 

Cabe se帽alar que MethodHandleProxies no es una buena manera de crear din谩micamente expresiones lambda, porque esta clase simplemente envuelve MethodHandle en una clase proxy y delega invocationHandler.invoke a MethodHandle.invokeWithArguments . Este enfoque utiliza Java Reflection y es muy lento.


Como se mostr贸 anteriormente, no todos los manejadores de m茅todos se pueden usar para crear expresiones lambda en tiempo de ejecuci贸n.




Solo se pueden usar unos pocos tipos de manejadores de m茅todos para crear din谩micamente expresiones lambda.




Aqu铆 est谩n:


  • REF_invokeInterface: se puede crear usando Lookup.findVirtual para m茅todos de interfaz
  • REF_invokeVirtual: se puede crear usando Lookup.findVirtual para m茅todos virtuales de clase
  • REF_invokeStatic: creado usando Lookup.findStatic para m茅todos est谩ticos
  • REF_newInvokeSpecial: se puede crear usando Lookup.findConstructor para constructores
  • REF_invokeSpecial: se puede crear usando Lookup.findSpecial
    para m茅todos privados y enlace temprano con m茅todos virtuales de clase

Otros tipos de controladores LambdaConversionException error LambdaConversionException .


Excepciones Gen茅ricas


Este error est谩 relacionado con el compilador de Java y la capacidad de declarar excepciones gen茅ricas en la secci贸n de throws . El siguiente ejemplo de c贸digo demuestra este comportamiento:


 interface ExtendedCallable<V, E extends Exception> extends Callable<V>{ @Override V call() throws E; } final ExtendedCallable<URL, MalformedURLException> urlFactory = () -> new URL("http://localhost"); urlFactory.call(); 

Este c贸digo debe compilarse porque el constructor de la clase URL arroja una MalformedURLException . Pero no se compila. Se muestra el siguiente mensaje de error:


 Error:(46, 73) java: call() in <anonymous Test$CODEgt; cannot implement call() in ExtendedCallable overridden method does not throw java.lang.Exception 

Pero, si reemplazamos la expresi贸n lambda con una clase an贸nima, entonces el c贸digo compila:


 final ExtendedCallable<URL, MalformedURLException> urlFactory = new ExtendedCallable<URL, MalformedURLException>() { @Override public URL call() throws MalformedURLException { return new URL("http://localhost"); } }; urlFactory.call(); 

De esto se desprende:




La inferencia de tipos para excepciones gen茅ricas no funciona correctamente en combinaci贸n con expresiones lambda




Limitaciones de tipo de parametrizaci贸n


Puede construir un objeto gen茅rico con varias restricciones de tipo utilizando el signo & : <T extends A & B & C & ... Z> .
Este m茅todo de determinaci贸n de par谩metros gen茅ricos rara vez se usa, pero de cierta manera afecta las expresiones lambda en Java debido a algunas restricciones:


  • Cada restricci贸n de tipo, excepto la primera, debe ser una interfaz.
  • Una versi贸n pura de una clase con tal gen茅rico solo tiene en cuenta la primera restricci贸n de tipo de la lista.

La segunda limitaci贸n conduce a diferentes comportamientos del c贸digo en tiempo de compilaci贸n y en tiempo de ejecuci贸n, cuando se produce el enlace a la expresi贸n lambda. Esta diferencia se puede demostrar usando el siguiente c贸digo:


 final class MutableInteger extends Number implements IntSupplier, IntConsumer { //mutable container of int value private int value; public MutableInteger(final int v) { value = v; } @Override public int intValue() { return value; } @Override public long longValue() { return value; } @Override public float floatValue() { return value; } @Override public double doubleValue() { return value; } @Override public int getAsInt() { return intValue(); } @Override public void accept(final int value) { this.value = value; } } static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection <T> values) { return values.stream().mapToInt(IntSupplier::getAsInt).min(); } final List <MutableInteger> values = Arrays.asList(new MutableInteger(10), new MutableInteger(20)); final int mv = findMinValue(values).orElse(Integer.MIN_VALUE); System.out.println(mv); 

Este c贸digo es absolutamente correcto y se compila con 茅xito. La clase MutableInteger satisface las restricciones del tipo gen茅rico T:


  • MutableInteger hereda de Number .
  • MutableInteger implementa IntSupplier .

Pero el c贸digo se bloquear谩 con una excepci贸n en tiempo de ejecuci贸n:


 java.lang.BootstrapMethodError: call site initialization exception at java.lang.invoke.CallSite.makeSite(CallSite.java:341) at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307) at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297) at Test.minValue(Test.java:77) Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class java.lang.Number; not a subtype of implementation type interface java.util.function.IntSupplier at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233) at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303) at java.lang.invoke.CallSite.makeSite(CallSite.java:302) 

Esto sucede porque la canalizaci贸n JavaStream captura solo un tipo puro, que, en nuestro caso, es la clase Number y no implementa la interfaz IntSupplier . Este problema se puede solucionar declarando expl铆citamente el tipo de par谩metro en un m茅todo separado, utilizado como referencia al m茅todo:


 private static int getInt(final IntSupplier i){ return i.getAsInt(); } private static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection<T> values){ return values.stream().mapToInt(UtilsTest::getInt).min(); } 

Este ejemplo demuestra una inferencia de tipo incorrecta en el compilador y el tiempo de ejecuci贸n.




El manejo de m煤ltiples restricciones de tipo de par谩metro gen茅rico junto con el uso de expresiones lambda en tiempo de compilaci贸n y en tiempo de ejecuci贸n no es consistente



Source: https://habr.com/ru/post/es432418/


All Articles