CodeGym /Blogue Java /Random-PT /API de reflexão: Reflexão. O lado obscuro de Java
John Squirrels
Nível 41
San Francisco

API de reflexão: Reflexão. O lado obscuro de Java

Publicado no grupo Random-PT
Saudações, jovem Padawan. Neste artigo, falarei sobre a Força, um poder que os programadores Java só usam em situações aparentemente impossíveis. O lado negro do Java é a API de reflexão. Em Java, a reflexão é implementada usando a API Java Reflection.

O que é reflexão Java?

Existe uma definição curta, precisa e popular na Internet. Reflexão ( do latim tardio reflexio - voltar atrás ) é um mecanismo para explorar dados sobre um programa enquanto ele está sendo executado. A reflexão permite explorar informações sobre campos, métodos e construtores de classe. A reflexão permite trabalhar com tipos que não estavam presentes no tempo de compilação, mas que se tornaram disponíveis durante o tempo de execução. A reflexão e um modelo logicamente consistente para emitir informações de erro possibilitam a criação de um código dinâmico correto. Em outras palavras, entender como a reflexão funciona em Java abrirá uma série de oportunidades incríveis para você. Você pode literalmente fazer malabarismos com classes e seus componentes. Aqui está uma lista básica do que a reflexão permite:
  • Aprender/determinar a classe de um objeto;
  • Obtenha informações sobre os modificadores, campos, métodos, constantes, construtores e superclasses de uma classe;
  • Descubra quais métodos pertencem à(s) interface(s) implementada(s);
  • Crie uma instância de uma classe cujo nome de classe seja desconhecido até o tempo de execução;
  • Obter e definir valores dos campos de um objeto por nome;
  • Chame o método de um objeto pelo nome.
A reflexão é usada em quase todas as tecnologias Java modernas. É difícil imaginar que o Java, como plataforma, pudesse ter alcançado uma adoção tão ampla sem reflexão. Muito provavelmente, não teria. Agora que você já está familiarizado com a reflexão como conceito teórico, vamos à sua aplicação prática! Não aprenderemos todos os métodos da API do Reflection, apenas aqueles que você realmente encontrará na prática. Como a reflexão envolve trabalhar com classes, começaremos com uma classe simples chamada MyClass:

public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Como você pode ver, esta é uma classe muito básica. O construtor com parâmetros é deliberadamente comentado. Voltaremos a isso mais tarde. Se você examinou cuidadosamente o conteúdo da classe, provavelmente notou a ausência de um getter para o campo de nome . O próprio campo de nome é marcado com o modificador de acesso privado : não podemos acessá-lo fora da própria classe, o que significa que não podemos recuperar seu valor. " Então qual é o problema ?" você diz. "Adicione um getter ou altere o modificador de acesso". E você estaria certo, a menos queMyClassestava em uma biblioteca AAR compilada ou em outro módulo privado sem capacidade de fazer alterações. Na prática, isso acontece o tempo todo. E algum programador descuidado simplesmente esqueceu de escrever um getter . Este é o momento exato de lembrar a reflexão! Vamos tentar chegar ao campo de nome privado da MyClassclasse:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; // No getter =(
   System.out.println(number + name); // Output: 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name); // Output: 0default
}
Vamos analisar o que acabou de acontecer. Em Java, existe uma classe maravilhosa chamada Class. Ele representa classes e interfaces em um aplicativo Java executável. Não abordaremos a relação entre Classe ClassLoader, pois esse não é o tópico deste artigo. Em seguida, para recuperar os campos desta classe, você precisa chamar o getFields()método. Este método retornará todos os campos acessíveis desta classe. Isso não funciona para nós, porque nosso campo é private , então usamos o getDeclaredFields()método. Esse método também retorna uma matriz de campos de classe, mas agora inclui campos privados e protegidos . Neste caso, sabemos o nome do campo que nos interessa, então podemos usar o getDeclaredField(String)método, ondeStringé o nome do campo desejado. Observação: getFields()e getDeclaredFields()não retorne os campos de uma classe pai! Ótimo. Temos um Fieldobjeto referenciando nosso nome . Como o campo não era público , devemos conceder acesso para trabalhar com ele. O setAccessible(true)método nos permite prosseguir. Agora o campo de nome está sob nosso controle total! Você pode recuperar seu valor chamando o método Fielddo objeto , onde é uma instância de nossa classe. Convertemos o tipo e atribuímos o valor à nossa variável de nome . Se não conseguirmos encontrar um configurador para definir um novo valor para o campo de nome, você pode usar o método set :get(Object)ObjectMyClassString

field.set(myClass, (String) "new value");
Parabéns! Você acabou de dominar os fundamentos da reflexão e acessou um campo privado ! Preste atenção ao try/catchbloco e aos tipos de exceções que estão sendo tratadas. O IDE dirá a você que a presença deles é necessária por conta própria, mas você pode dizer claramente por seus nomes por que eles estão aqui. Se movendo! Como você deve ter notado, nossa MyClassclasse já possui um método para exibir informações sobre os dados da classe:

private void printData(){
       System.out.println(number + name);
   }
Mas esse programador também deixou suas impressões digitais aqui. O método tem um modificador de acesso privado e temos que escrever nosso próprio código para exibir os dados todas as vezes. Que bagunça. Para onde foi nosso reflexo? Escreva a seguinte função:

public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
O procedimento aqui é quase o mesmo usado para recuperar um campo. Acessamos o método desejado pelo nome e concedemos acesso a ele. E no Methodobjeto chamamos de invoke(Object, Args)método, onde Objecttambém é uma instância da MyClassclasse. Argssão os argumentos do método, embora o nosso não tenha nenhum. Agora usamos a printDatafunção para exibir as informações:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// Output: 0new value
}
Viva! Agora temos acesso ao método privado da classe. Mas e se o método tiver argumentos e por que o construtor está comentado? Tudo no seu devido tempo. Está claro desde a definição no início que a reflexão permite criar instâncias de uma classe em tempo de execução (enquanto o programa está em execução)! Podemos criar um objeto usando o nome completo da classe. O nome completo da classe é o nome da classe, incluindo o caminho de seu pacote .
API de reflexão: Reflexão.  O lado negro do Java - 2
Na minha hierarquia de pacotes , o nome completo de MyClass seria "reflection.MyClass". Há também uma maneira simples de aprender o nome de uma classe (retorne o nome da classe como uma String):

MyClass.class.getName()
Vamos usar a reflexão Java para criar uma instância da classe:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass); // Output: created object reflection.MyClass@60e53b93
}
Quando um aplicativo Java é iniciado, nem todas as classes são carregadas na JVM. Se o seu código não fizer referência à MyClassclasse, o ClassLoader, que é responsável por carregar as classes na JVM, nunca carregará a classe. Isso significa que você tem que forçar ClassLoaderpara carregá-lo e obter uma descrição de classe na forma de uma Classvariável. É por isso que temos o forName(String)método, onde Stringé o nome da classe cuja descrição precisamos. Depois de obter o Сlassobjeto, chamar o método newInstance()retornará um Objectobjeto criado usando essa descrição. Tudo o que resta é fornecer este objeto ao nossoMyClassaula. Legal! Isso foi difícil, mas compreensível, espero. Agora podemos criar uma instância de uma classe literalmente em uma linha! Infelizmente, a abordagem descrita funcionará apenas com o construtor padrão (sem parâmetros). Como você chama métodos e construtores com parâmetros? É hora de descomentar nosso construtor. Como esperado, newInstance()não é possível encontrar o construtor padrão e não funciona mais. Vamos reescrever a instanciação da classe:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);// Output: created object reflection.MyClass@60e53b93
}
O getConstructors()método deve ser chamado na definição de classe para obter construtores de classe e, em seguida, getParameterTypes()deve ser chamado para obter os parâmetros de um construtor:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Isso nos dá todos os construtores e seus parâmetros. No meu exemplo, refiro-me a um construtor específico com parâmetros específicos previamente conhecidos. E para chamar esse construtor, usamos o newInstancemétodo, para o qual passamos os valores desses parâmetros. Será o mesmo ao usar invokemétodos de chamada. Isso levanta a questão: quando chamar construtores por meio de reflexão é útil? Como já mencionado no início, as modernas tecnologias Java não podem passar sem a API Java Reflection. Por exemplo, Dependency Injection (DI), que combina anotações com reflexão de métodos e construtores para formar o popular Darerbiblioteca para desenvolvimento Android. Depois de ler este artigo, você pode se considerar confiante nas formas da API Java Reflection. Eles não chamam a reflexão de lado negro de Java por nada. Isso quebra completamente o paradigma OOP. Em Java, o encapsulamento oculta e restringe o acesso de outros a determinados componentes do programa. Quando utilizamos o modificador private, pretendemos que aquele campo seja acessado somente de dentro da classe onde ele existe. E construímos a arquitetura subsequente do programa com base nesse princípio. Neste artigo, vimos como você pode usar a reflexão para forçar seu caminho em qualquer lugar. O padrão de design criacional Singletoné um bom exemplo disso como uma solução arquitetônica. A ideia básica é que uma classe implementando esse padrão terá apenas uma instância durante a execução de todo o programa. Isso é feito adicionando o modificador de acesso privado ao construtor padrão. E seria muito ruim se um programador usasse reflexão para criar mais instâncias dessas classes. A propósito, ouvi recentemente um colega de trabalho fazer uma pergunta muito interessante: uma classe que implementa o padrão Singleton pode ser herdada? Será que, neste caso, até a reflexão seria impotente? Deixe seu feedback sobre o artigo e sua resposta nos comentários abaixo e faça suas próprias perguntas lá!

Mais leitura:

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION