
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) {
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;
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); }
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);
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) { 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"));
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),
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);
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),
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 {
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