Manipuladores de método em Java
1. Introdução
Neste artigo, vamos explorar uma API importante que foi introduzida no Java 7 e aprimorada nas versões a seguir, ojava.lang.invoke.MethodHandles.
Em particular, aprenderemos o que são os manipuladores de método, como criá-los e como usá-los.
2. O que são identificadores de método?
Chegando à sua definição, conforme declarado na documentação da API:
Um identificador de método é uma referência digitada e diretamente executável a um método subjacente, construtor, campo ou operação semelhante de baixo nível, com transformações opcionais de argumentos ou valores de retorno.
De forma mais simples,method handles are a low-level mechanism for finding, adapting and invoking methods.
Os identificadores de método são imutáveis e não têm estado visível.
Para criar e usar umMethodHandle, são necessárias 4 etapas:
-
Criando a pesquisa
-
Criando o tipo de método
-
Localizando o identificador do método
-
Invocando o Identificador de Método
2.1. Alças de método vs reflexão
Os identificadores de método foram introduzidos a fim de trabalhar em conjunto com a APIjava.lang.reflect existente, pois eles têm finalidades diferentes e têm características diferentes.
Do ponto de vista de desempenho, oMethodHandles API can be much faster than the Reflection API since the access checks are made at creation time rather than at execution time. Essa diferença é ampliada se um gerente de segurança estiver presente, pois as pesquisas de membros e classes estão sujeitas a verificações adicionais.
No entanto, considerando que o desempenho não é a única medida de adequação para uma tarefa, devemos também considerar que a APIMethodHandles é mais difícil de usar devido à falta de mecanismos como enumeração de classe de membro, inspeção de sinalizadores de acessibilidade e muito mais .
Mesmo assim, a APIMethodHandles oferece a possibilidade de curry métodos, alterar os tipos de parâmetros e alterar sua ordem.
Tendo uma definição e objetivos claros da APIMethodHandles, podemos agora começar a trabalhar com eles, a partir da consulta.
3. Criando oLookup
A primeira coisa a fazer quando queremos criar um identificador de método é recuperar a pesquisa, o objeto de fábrica responsável por criar identificadores de métodos para métodos, construtores e campos visíveis para a classe de pesquisa.
Por meio da APIMethodHandles, é possível criar o objeto lookup, com diferentes modos de acesso.
Vamos criar a pesquisa que fornece acesso aos métodospublic:
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
Porém, caso desejemos ter acesso também aos métodosprivateeprotected, podemos usar, em vez disso, o métodolookup():
MethodHandles.Lookup lookup = MethodHandles.lookup();
4. Criando umMethodType
Para poder criar oMethodHandle, o objeto lookup requer uma definição de seu tipo e isso é feito por meio da classeMethodType.
Em particular,a MethodType represents the arguments and return type accepted and returned by a method handle or passed and expected by a method handle caller.
A estrutura de aMethodType é simples e é formada por um tipo de retorno junto com um número apropriado de tipos de parâmetro que devem ser correspondidos adequadamente entre um manipulador de método e todos os seus chamadores
Da mesma forma queMethodHandle, mesmo as instâncias de aMethodType são imutáveis.
Vamos ver como é possível definir umMethodType que especifica uma classejava.util.List como tipo de retorno e um arrayObject como tipo de entrada:
MethodType mt = MethodType.methodType(List.class, Object[].class);
Caso o método retorne um tipo primitivo ouvoid como seu tipo de retorno, usaremos a classe que representa esses tipos (void.class, int.class…).
Vamos definir umMethodType que retorna um valor int e aceita umObject:
MethodType mt = MethodType.methodType(int.class, Object.class);
Agora podemos prosseguir para criarMethodHandle.
5. Encontrando umMethodHandle
Depois de definir nosso tipo de método, para criar umMethodHandle,, temos que encontrá-lo por meio do objetolookup oupublicLookup, fornecendo também a classe de origem e o nome do método.
Em particular, a fábrica de pesquisa fornece umset of methods que nos permite encontrar o manipulador do método de forma apropriada, considerando o escopo do nosso método. Começando com o cenário mais simples, vamos explorar os principais.
5.1. Identificador de método para métodos
Usar o métodofindVirtual() nos permite criar um MethodHandle para um método de objeto. Vamos criar um, com base no métodoconcat() da classeString:
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);
5.2. Identificador de método para métodos estáticos
Quando queremos obter acesso a um método estático, podemos usar o métodofindStatic():
MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asListMH = publicLookup.findStatic(Arrays.class, "asList", mt);
Neste caso, criamos um método handle que converte um array deObjects emList deles.
5.3. Identificador de método para construtores
O acesso a um construtor pode ser feito usando o métodofindConstructor().
Vamos criar um manipulador de método que se comporta como o construtor da classeInteger, aceitando um atributoString:
MethodType mt = MethodType.methodType(void.class, String.class);
MethodHandle newIntegerMH = publicLookup.findConstructor(Integer.class, mt);
5.4. Identificador de método para campos
Usando um identificador de método, é possível obter acesso também aos campos.
Vamos começar a definir a classeBook:
public class Book {
String id;
String title;
// constructor
}
Tendo como pré-condição uma visibilidade de acesso direto entre o identificador do método e a propriedade declarada, podemos criar um identificador do método que se comporte como um getter:
MethodHandle getTitleMH = lookup.findGetter(Book.class, "title", String.class);
Para obter mais informações sobre como manipular variáveis / campos, dê uma olhada emJava 9 Variable Handles Demystified, onde discutimos a APIjava.lang.invoke.VarHandle, adicionada em Java 9.
5.5. Identificador de método para métodos privados
A criação de um identificador de método para um método privado pode ser feita, com a ajuda da APIjava.lang.reflect.
Vamos começar adicionando um métodoprivate à classeBook:
private String formatBook() {
return id + " > " + title;
}
Agora podemos criar um identificador de método que se comporta exatamente como o métodoformatBook():
Method formatBookMethod = Book.class.getDeclaredMethod("formatBook");
formatBookMethod.setAccessible(true);
MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);
6. Invocando um identificador de método
Depois de criar nossos identificadores de método, usá-los é a próxima etapa. Em particular, a classeMethodHandle fornece 3 maneiras diferentes de executar um manipulador de método:invoke(),invokeWithArugments()einvokeExact().
Vamos começar com a opçãoinvoke.
6.1. Invocando um identificador de método
Ao usar o métodoinvoke(), forçamos o número de argumentos (aridade) a ser corrigido, mas permitimos a execução de conversão e encaixe / desempacotamento dos argumentos e tipos de retorno.
Vamos ver como é possível usar oinvoke() com um argumento em caixa:
MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);
String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');
assertEquals("java", output);
Nesse caso, oreplaceMH requer argumentoschar, mas oinvoke() executa um unboxing no argumentoCharacter antes de sua execução.
6.2. Invocando com Argumentos
Chamar um identificador de método usando o métodoinvokeWithArguments é a menos restritiva das três opções.
De fato, ele permite uma invocação variável da aridade, além da conversão e encaixotamento / retirada da caixa dos argumentos e dos tipos de retorno.
Na prática, isso nos permite criar umList deInteger a partir de umarray dos valores deint:
MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);
List list = (List) asList.invokeWithArguments(1,2);
assertThat(Arrays.asList(1,2), is(list));
6.3. Invocando Exato
Caso desejemos ser mais restritivos na forma como executamos o manipulador de um método (número de argumentos e seu tipo), devemos usar o métodoinvokeExact().
Na verdade, ele não fornece nenhuma conversão para a classe fornecida e requer um número fixo de argumentos.
Vamos ver como podemossum dois valores deint usando um identificador de método:
MethodType mt = MethodType.methodType(int.class, int.class, int.class);
MethodHandle sumMH = lookup.findStatic(Integer.class, "sum", mt);
int sum = (int) sumMH.invokeExact(1, 11);
assertEquals(12, sum);
Se, neste caso, decidirmos passar para o métodoinvokeExact um número que não seja umint, a invocação levará aWrongMethodTypeException.
7. Trabalhando com Array
MethodHandles não se destina a funcionar apenas com campos ou objetos, mas também com matrizes. Na verdade, com a APIasSpreader(), é possível fazer um tratamento de método de propagação de matriz.
Nesse caso, o identificador de método aceita um argumento de matriz, espalhando seus elementos como argumentos posicionais e, opcionalmente, o comprimento da matriz.
Vamos ver como podemos espalhar um identificador de método para verificar se os elementos dentro de uma matriz são iguais:
MethodType mt = MethodType.methodType(boolean.class, Object.class);
MethodHandle equals = publicLookup.findVirtual(String.class, "equals", mt);
MethodHandle methodHandle = equals.asSpreader(Object[].class, 2);
assertTrue((boolean) methodHandle.invoke(new Object[] { "java", "java" }));
8. Aprimorando um identificador de método
Depois de definir um identificador de método, é possível aprimorá-lo vinculando o identificador de método a um argumento sem realmente invocá-lo.
Por exemplo, no Java 9, esse tipo de comportamento é usado para otimizar a concatenaçãoString.
Vamos ver como podemos realizar uma concatenação, vinculando um sufixo ao nossoconcatMH:
MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);
MethodHandle bindedConcatMH = concatMH.bindTo("Hello ");
assertEquals("Hello World!", bindedConcatMH.invoke("World!"));
9. Aprimoramentos do Java 9
Com o Java 9, poucos aprimoramentos foram feitos na APIMethodHandles com o objetivo de torná-la muito mais fácil de usar.
As melhorias afetaram três tópicos principais:
-
Lookup functions - permitindo pesquisas de classe em diferentes contextos e suporta métodos não abstratos em interfaces
-
Argument handling - melhorando as funcionalidades de dobrar, coletar e espalhar argumentos
-
Additional combinations - adição de loops (loop,whileLoop,doWhileLoop…) e um melhor suporte para tratamento de exceções com otryFinally
Essas alterações resultaram em alguns benefícios adicionais:
-
Otimizações aprimoradas do compilador JVM
-
Redução de instanciação
-
Precisão ativada no uso da APIMethodHandles
Detalhes das melhorias feitas estão disponíveis emMethodHandles API Javadoc.
10. Conclusão
Neste artigo, cobrimos a APIMethodHandles, o que são e como podemos usá-los.
Também discutimos como ela se relaciona com a API de reflexão e, como os manipuladores de método permitem operações de baixo nível, é melhor evitar usá-las, a menos que elas se encaixem perfeitamente no escopo do trabalho.
Como sempre, o código-fonte completo deste artigo está disponívelover on Github.