Analisando Expressões Lambda em Java

imagem


De um tradutor: O LambdaMetafactory é provavelmente um dos mecanismos mais subestimados do Java 8. Nós o descobrimos recentemente, mas já apreciamos suas capacidades. A versão 7.0 da estrutura CUBA aprimora o desempenho, evitando chamadas reflexivas em favor da geração de expressões lambda. Uma das aplicações desse mecanismo em nossa estrutura é a ligação de manipuladores de eventos de aplicativos por anotações, uma tarefa comum, um análogo do EventListener da Spring. Acreditamos que o conhecimento dos princípios do LambdaFactory pode ser útil em muitos aplicativos Java, e nos apressamos em compartilhar esta tradução com você.


Neste artigo, mostraremos alguns truques pouco conhecidos ao trabalhar com expressões lambda no Java 8 e as limitações dessas expressões. O público-alvo do artigo são desenvolvedores seniores de Java, pesquisadores e desenvolvedores de kits de ferramentas. Somente a API Java pública será usada sem com.sun.* E outras classes internas, portanto, o código é portátil entre diferentes implementações da JVM.


Prefácio curto


As expressões Lambda apareceram no Java 8 como uma maneira de implementar métodos anônimos e,
em alguns casos, como uma alternativa às classes anônimas. No nível do bytecode, a expressão lambda é substituída pela invokedynamic . Esta instrução é usada para criar uma implementação de interface funcional e seu único método delega a chamada para o método real, que contém o código definido no corpo da expressão lambda.


Por exemplo, temos o seguinte código:


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

Este código será convertido pelo compilador Java em algo semelhante 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); } 

A instrução invokedynamic pode ser representada aproximadamente como esse 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 você pode ver, o LambdaMetafactory é usado para criar um CallSite que fornece um método de fábrica que retorna um manipulador para o método de destino. Este método retorna a implementação da interface funcional usando invokeExact . Se houver variáveis ​​capturadas na expressão lambda, invokeExact aceitará essas variáveis ​​como parâmetros reais.


No Oracle JRE 8, o metafactory gera dinamicamente uma classe Java usando o ObjectWeb Asm, que cria uma classe que implementa uma interface funcional. Campos adicionais podem ser adicionados à classe criada se a expressão lambda capturar variáveis ​​externas. Este se parece com classes anônimas Java, mas existem as seguintes diferenças:


  • Uma classe anônima é gerada pelo compilador Java.
  • A classe para implementar a expressão lambda é criada pela JVM em tempo de execução.



A implementação meta-fábrica depende do fornecedor e da versão da JVM




Obviamente, a invokedynamic não é usada apenas para expressões lambda em Java. É usado principalmente ao executar linguagens dinâmicas no ambiente da JVM. O mecanismo JavaScript Nashorn , que é incorporado ao Java, faz uso pesado dessas instruções.


A seguir, focaremos a classe LambdaMetafactory e seus recursos. Seguinte
A seção deste artigo pressupõe que você entenda muito bem como funcionam os métodos metafábricas e o que MethodHandle


Truques com expressões lambda


Nesta seção, mostraremos como criar lambdas dinâmicas para uso em tarefas diárias.


Exceções verificadas e lambdas


Não é segredo que todas as interfaces funcionais existentes em Java não suportam exceções verificadas. As vantagens das exceções verificadas em relação às regulares são um debate de longa data (e ainda quente).


Mas e se você precisar usar código com exceções verificadas nas expressões lambda em combinação com o Java Streams? Por exemplo, você precisa converter uma lista de cadeias de caracteres em uma lista de URLs como este:


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

Uma exceção lançável é declarada no construtor da URL (String) , portanto, não pode ser usada diretamente como referência de método na classe Functiion .


Você dirá: "Não, talvez se você usar esse truque aqui":


 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 é um truque sujo. E aqui está o porquê:


  • O bloco try-catch é usado.
  • A exceção é lançada novamente.
  • O uso sujo do tipo apagamento em Java.

O problema pode ser resolvido de maneira mais "legal", usando o conhecimento dos seguintes fatos:


  • As exceções verificadas são reconhecidas apenas no nível do compilador Java.
  • A seção throws são apenas metadados para um método sem um valor semântico no nível da JVM.
  • As exceções verificadas e normais são indistinguíveis no nível de bytecode na JVM.

A solução é Callable.call método Callable.call em um método sem uma seção de throws :


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

Esse código não é compilado porque o método Callable.call declarou exceções verificadas na seção throws . Mas podemos remover esta seção usando uma expressão lambda construída dinamicamente.


Primeiro, precisamos declarar uma interface funcional que não possui uma seção de throws .
mas quem poderá delegar a chamada para Callable.call :


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

A segunda etapa é criar uma implementação dessa interface usando o LambdaMetafactory e delegar a chamada do método SilentInvoker.invoke ao método Callable.call . Como mencionado anteriormente, a seção throws é ignorada no nível do bytecode, portanto, o método SilentInvoker.invoke pode chamar o método Callable.call sem declarar exceções:


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

Terceiro, escrevemos um método auxiliar que chama Callable.call sem declarar exceções:


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

Agora você pode reescrever o fluxo sem problemas com exceções verificadas:


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

Esse código é compilado sem problemas porque callUnchecked não declara exceções verificadas. Além disso, a chamada desse método pode ser incorporada usando o armazenamento em cache monomórfico , porque é apenas uma classe em toda a JVM que implementa a interface SilentOnvoker


Se a implementação de Callable.call lançar uma exceção em tempo de execução, ela será Callable.call pela função de chamada sem problemas:


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

Apesar das possibilidades desse método, lembre-se sempre da seguinte recomendação:




Ocultar exceções marcadas com callUnchecked apenas se você tiver certeza de que o código chamado não lançará nenhuma exceção




O exemplo a seguir mostra um exemplo dessa abordagem:


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

A implementação completa deste método está aqui , faz parte do projeto de código aberto SNAMP .


Trabalhando com getters e setters


Esta seção será útil para quem escreve serialização / desserialização para vários formatos de dados, como JSON, Thrift, etc. Além disso, pode ser bastante útil se o seu código depender muito da reflexão de Getters e Setters no JavaBeans.


Um getter declarado no JavaBean é um método chamado getXXX sem parâmetros e um tipo de dados de retorno diferente de void . Um setter declarado no JavaBean é um método chamado setXXX , com um parâmetro e retornando void . Essas duas notações podem ser representadas como interfaces funcionais:


  • Getter pode ser representado pela classe Function , na qual o argumento é o valor this .
  • Setter pode ser representado pela classe BiConsumer , na qual o primeiro argumento é this e o segundo é o valor que é passado para o Setter.

Agora, criaremos dois métodos que podem converter qualquer getter ou setter nesses
interfaces funcionais. E não importa que ambas as interfaces sejam genéricas. Após apagar os tipos
o tipo de dados real será Object . A conversão automática do tipo e argumentos de retorno pode ser feita usando o LambdaMetafactory . Além disso, a biblioteca Guava ajudará no cache de expressões lambda para os mesmos getters e setters.


Primeiro passo: crie um cache para getters e setters. A classe Method da API de reflexão representa um getter ou setter real e é usada como uma chave.
O valor do cache é uma interface funcional construída dinamicamente para um getter ou 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(); 

Em segundo lugar, criaremos métodos de fábrica que criam uma instância da interface funcional com base em referências ao getter ou 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); } } 

A conversão automática de tipo entre argumentos do tipo Object em interfaces funcionais (após apagamento do tipo) e tipos reais de argumentos e valor de retorno é alcançada usando a diferença entre samMethodType e instantiatedMethodType (o terceiro e o quinto argumentos do método metafactory, respectivamente). O tipo do método instanciado é a especialização do método que fornece a implementação da expressão lambda.


Em terceiro lugar, criaremos uma fachada para essas fábricas com suporte para armazenamento em cache:


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

As informações de método obtidas de uma instância da classe Method usando a API Java Reflection podem ser facilmente convertidas em MethodHandle . Lembre-se de que os métodos de instância de classe sempre têm um primeiro argumento oculto usado para passar this para esse método. Métodos estáticos não possuem esse parâmetro. Por exemplo, a assinatura real do método Integer.intValue() parece com int intValue(Integer this) . Esse truque é usado em nossa implementação de wrappers funcionais para getters e setters.


E agora é hora de testar o 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 

Essa abordagem com getters e setters em cache pode ser efetivamente usada em bibliotecas de serialização / desserialização (como Jackson) que usam getters e setters durante a serialização e desserialização.




Chamar interfaces funcionais com implementações geradas dinamicamente usando LambdaMetaFactory significativamente mais rápido do que chamar pela API Java Reflection




A versão completa do código pode ser encontrada aqui , faz parte da biblioteca SNAMP .


Limitações e bugs


Nesta seção, examinaremos alguns erros e limitações associados às expressões lambda no compilador Java e na JVM. Todas essas limitações podem ser reproduzidas no OpenJDK e Oracle JDK com o javac versão 1.8.0_131 para Windows e Linux.


Criando expressões lambda a partir de manipuladores de métodos


Como você sabe, uma expressão lambda pode ser construída dinamicamente usando o LambdaMetaFactory . Para fazer isso, você precisa definir um manipulador - a classe MethodHandle , que indica a implementação do único método definido na interface funcional. Vamos dar uma olhada neste exemplo simples:


 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 é equivalente a:


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

Mas e se substituirmos o manipulador de método que aponta para getValue pelo manipulador que os campos getter representam:


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

Esse código deve, como esperado, funcionar porque findGetter retorna um manipulador que aponta para os campos getter e tem a assinatura correta. Mas, se você executar esse código, verá a seguinte exceção:


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

Curiosamente, o getter para o campo funciona bem se usarmos MethodHandleProxies :


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

Deve-se observar que MethodHandleProxies não é uma boa maneira de criar expressões lambda dinamicamente, porque essa classe simplesmente MethodHandle em uma classe proxy e delega invocationHandler.invoke para MethodHandle.invokeWithArguments . Essa abordagem usa o Java Reflection e é muito lenta.


Como mostrado anteriormente, nem todos os manipuladores de métodos podem ser usados ​​para criar expressões lambda em tempo de execução.




Apenas alguns tipos de manipuladores de métodos podem ser usados ​​para criar dinamicamente expressões lambda.




Aqui estão elas:


  • REF_invokeInterface: pode ser criado usando Lookup.findVirtual para métodos de interface
  • REF_invokeVirtual: pode ser criado usando Lookup.findVirtual para métodos virtuais de classe
  • REF_invokeStatic: criado usando Lookup.findStatic para métodos estáticos
  • REF_newInvokeSpecial: pode ser criado usando Lookup.findConstructor para construtores
  • REF_invokeSpecial: pode ser criado usando Lookup.findSpecial
    para métodos particulares e ligação antecipada com métodos virtuais de classe

Outros tipos de manipuladores LambdaConversionException erro LambdaConversionException .


Exceções genéricas


Esse bug está relacionado ao compilador Java e à capacidade de declarar exceções genéricas na seção throws . O exemplo de código a seguir demonstra esse comportamento:


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

Esse código deve ser compilado porque o construtor da classe URL lança uma MalformedURLException . Mas não compila. A seguinte mensagem de erro é exibida:


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

Mas, se substituirmos a expressão lambda por uma classe anônima, o 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(); 

Segue-se disso:




Inferência de tipo para exceções genéricas não funciona corretamente em combinação com expressões lambda




Limitações do tipo de parametrização


Você pode construir um objeto genérico com várias restrições de tipo usando o sinal & : <T extends A & B & C & ... Z> .
Esse método de determinação de parâmetros genéricos raramente é usado, mas de certa maneira afeta expressões lambda em Java devido a algumas restrições:


  • Cada restrição de tipo, exceto a primeira, deve ser uma interface.
  • Uma versão pura de uma classe com esse genérico leva em consideração apenas a primeira restrição de tipo da lista.

A segunda limitação leva a diferentes comportamentos do código no tempo de compilação e no tempo de execução, quando ocorre a ligação à expressão lambda. Essa diferença pode ser demonstrada usando o seguinte 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 está absolutamente correto e compila com sucesso. A classe MutableInteger atende às restrições do tipo genérico T:


  • MutableInteger herda de Number .
  • MutableInteger implementa IntSupplier .

Mas o código falhará com uma exceção no tempo de execução:


 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) 

Isso acontece porque o pipeline JavaStream captura apenas um tipo puro, que, no nosso caso, é a classe Number e não implementa a interface IntSupplier . Esse problema pode ser corrigido declarando explicitamente o tipo de parâmetro em um método separado, usado como referência ao 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 exemplo demonstra inferência de tipo incorreta no compilador e no tempo de execução.




O tratamento de várias restrições de tipo de parâmetro genérico em conjunto com o uso de expressões lambda no tempo de compilação e no tempo de execução não é consistente



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


All Articles