CodeGym /Blogue Java /Random-PT /Exemplos de reflexão
John Squirrels
Nível 41
San Francisco

Exemplos de reflexão

Publicado no grupo Random-PT
Talvez você tenha encontrado o conceito de "reflexão" na vida cotidiana. Esta palavra geralmente se refere ao processo de estudar a si mesmo. Na programação, tem um significado semelhante - é um mecanismo para analisar dados sobre um programa e até mesmo alterar a estrutura e o comportamento de um programa enquanto ele está sendo executado. Exemplos de reflexão - 1 O importante aqui é que estamos fazendo isso em tempo de execução, não em tempo de compilação. Mas por que examinar o código em tempo de execução? Afinal, você já pode ler o código :/ Há uma razão pela qual a ideia de reflexão pode não ser imediatamente clara: até este ponto, você sempre soube com quais classes estava trabalhando. Por exemplo, você poderia escrever uma Catclasse:

package learn.codegym;

public class Cat {

   private String name;
   private int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

@Override
public String toString() {
   return "Cat{" +
           "name='" + name + '\'' +
           ", age=" + age +
           '}';
}

}
Você sabe tudo sobre ele e pode ver os campos e métodos que ele possui. Suponha que você de repente precise introduzir outras classes de animais no programa. Você provavelmente poderia criar uma estrutura de herança de classe com uma Animalclasse pai por conveniência. Anteriormente, até criamos uma classe que representava uma clínica veterinária, para a qual podíamos passar um Animalobjeto (instância de uma classe pai), e o programa tratava o animal de maneira adequada, dependendo se era um cachorro ou um gato. Mesmo que essas não sejam as tarefas mais simples, o programa é capaz de aprender todas as informações necessárias sobre as classes em tempo de compilação. Assim, quando você passa um Catobjeto para os métodos da classe clínica veterinária nomain()método, o programa já sabe que é um gato, não um cachorro. Agora vamos imaginar que estamos diante de uma tarefa diferente. Nosso objetivo é escrever um analisador de código. Precisamos criar uma CodeAnalyzerclasse com um único método: void analyzeObject(Object o). Este método deve:
  • determinar a classe do objeto passado para ele e exibir o nome da classe no console;
  • determinar os nomes de todos os campos da classe passada, incluindo os privados, e exibi-los no console;
  • determine os nomes de todos os métodos da classe passada, incluindo os privados, e exiba-os no console.
Vai parecer algo assim:

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
      
       // Print the name of the class of object o
       // Print the names of all variables of this class
       // Print the names of all methods of this class
   }
  
}
Agora podemos ver claramente como esta tarefa difere de outras tarefas que você resolveu anteriormente. Com nosso objetivo atual, a dificuldade está no fato de que nem nós nem o programa sabemos exatamente o que será passado para oanalyzeClass()método. Se você escrever tal programa, outros programadores começarão a usá-lo, e eles podem passar qualquer coisa para este método — qualquer classe Java padrão ou qualquer outra classe que eles escreverem. A classe passada pode ter qualquer número de variáveis ​​e métodos. Em outras palavras, nós (e nosso programa) não temos ideia de quais classes iremos trabalhar. Mas ainda assim, precisamos concluir esta tarefa. E é aqui que a API Java Reflection padrão vem em nosso auxílio. A API de reflexão é uma ferramenta poderosa da linguagem. A documentação oficial da Oracle recomenda que esse mecanismo seja usado apenas por programadores experientes que saibam o que estão fazendo. Você logo entenderá por que estamos dando esse tipo de aviso com antecedência :) Aqui está uma lista de coisas que você pode fazer com a API de reflexão:
  1. Identificar/determinar a classe de um objeto.
  2. Obtenha informações sobre modificadores de classe, campos, métodos, constantes, construtores e superclasses.
  3. Descubra quais métodos pertencem a uma(s) interface(s) implementada(s).
  4. Crie uma instância de uma classe cujo nome de classe não seja conhecido até que o programa seja executado.
  5. Obtenha e defina o valor de um campo de instância por nome.
  6. Chame um método de instância pelo nome.
Lista impressionante, né? :) Observação:o mecanismo de reflexão pode fazer tudo isso "on the fly", independentemente do tipo de objeto que passamos para o nosso analisador de código! Vamos explorar os recursos da API de reflexão observando alguns exemplos.

Como identificar/determinar a classe de um objeto

Vamos começar com o básico. O ponto de entrada para o mecanismo de reflexão Java é a Classclasse. Sim, parece muito engraçado, mas reflexão é isso :) Usando a Classclasse, primeiro determinamos a classe de qualquer objeto passado para o nosso método. Vamos tentar fazer isso:

import learn.codegym.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Saída do console:

class learn.codegym.Cat
Preste atenção em duas coisas. Primeiro, colocamos deliberadamente a Catclasse em um learn.codegympacote separado. Agora você pode ver que o getClass()método retorna o nome completo da classe. Em segundo lugar, nomeamos nossa variável clazz. Isso parece um pouco estranho. Faria sentido chamá-lo de "classe", mas "classe" é uma palavra reservada em Java. O compilador não permitirá que as variáveis ​​sejam chamadas assim. Tínhamos que contornar isso de alguma forma :) Nada mal para começar! O que mais tínhamos nessa lista de capacidades?

Como obter informações sobre modificadores de classe, campos, métodos, constantes, construtores e superclasses.

Agora as coisas estão ficando mais interessantes! Na classe atual, não temos nenhuma constante ou uma classe pai. Vamos adicioná-los para criar uma imagem completa. Crie a Animalclasse pai mais simples:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
E faremos nossa Catclasse herdar Animale adicionar uma constante:

package learn.codegym;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Feline family";

   private String name;
   private int age;

   // ...the rest of the class
}
Agora temos a imagem completa! Vamos ver do que a reflexão é capaz :)

import learn.codegym.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Class name: " + clazz);
       System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Parent class: " + clazz.getSuperclass());
       System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Aqui está o que vemos no console:

Class name:  class learn.codegym.Cat 
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age] 
Parent class: class learn.codegym.Animal 
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()] 
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
Veja todas as informações detalhadas sobre as aulas que conseguimos! E não apenas informações públicas, mas também informações privadas! Observação: privatevariáveis ​​também são exibidas na lista. Nossa "análise" da aula pode ser considerada essencialmente completa: estamos usando o analyzeObject()método para aprender tudo o que pudermos. Mas isso não é tudo que podemos fazer com a reflexão. Não estamos limitados à simples observação — passaremos à ação! :)

Como criar uma instância de uma classe cujo nome de classe não é conhecido até que o programa seja executado.

Vamos começar com o construtor padrão. Nossa Catclasse ainda não tem uma, então vamos adicioná-la:

public Cat() {
  
}
Aqui está o código para criar um Catobjeto usando createCat()o método reflection ():

import learn.codegym.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
Entrada do console:

learn.codegym.Cat
Saída do console:

Cat{name='null', age=0}
Isso não é um erro: os valores de namee agesão exibidos no console porque escrevemos o código para exibi-los no toString()método da Catclasse. Aqui lemos o nome de uma classe cujo objeto iremos criar a partir do console. O programa reconhece o nome da classe cujo objeto será criado. Exemplos de reflexão - 3Por uma questão de brevidade, omitimos o código de tratamento de exceção adequado, que ocuparia mais espaço do que o próprio exemplo. Em um programa real, é claro, você deve lidar com situações que envolvam nomes inseridos incorretamente, etc. O construtor padrão é bem simples, então, como você pode ver, é fácil usá-lo para criar uma instância da classe :) Usando o newInstance()método , criamos um novo objeto desta classe. É outra questão se oCatconstrutor recebe argumentos como entrada. Vamos remover o construtor padrão da classe e tentar executar nosso código novamente.

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Algo deu errado! Ocorreu um erro porque chamamos um método para criar um objeto usando o construtor padrão. Mas não temos esse construtor agora. Assim, quando o newInstance()método é executado, o mecanismo de reflexão usa nosso antigo construtor com dois parâmetros:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Mas não fizemos nada com os parâmetros, como se tivéssemos esquecido completamente deles! Usar reflexão para passar argumentos para o construtor requer um pouco de "criatividade":

import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.codegym.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Saída do console:

Cat{name='Fluffy', age=6}
Vamos dar uma olhada no que está acontecendo em nosso programa. Criamos uma matriz de Classobjetos.

Class[] catClassParams = {String.class, int.class};
Correspondem aos parâmetros do nosso construtor (que só tem parâmetros Stringe int). Nós os passamos para o clazz.getConstructor()método e ganhamos acesso ao construtor desejado. Depois disso, tudo o que precisamos fazer é chamar o newInstance()método com os argumentos necessários e não esquecer de converter explicitamente o objeto para o tipo desejado: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Agora nosso objeto foi criado com sucesso! Saída do console:

Cat{name='Fluffy', age=6}
Seguindo em frente :)

Como obter e definir o valor de um campo de instância por nome.

Imagine que você está usando uma classe escrita por outro programador. Além disso, você não tem a capacidade de editá-lo. Por exemplo, uma biblioteca de classes pronta empacotada em um JAR. Você pode ler o código das classes, mas não pode alterá-lo. Suponha que o programador que criou uma das classes nesta biblioteca (que seja nossa Catclasse antiga), falhando em dormir o suficiente na noite anterior à finalização do projeto, removeu o getter e o setter do agecampo. Agora essa aula chegou até você. Atende a todas as suas necessidades, pois você só precisa Catde objetos em seu programa. Mas você precisa deles para ter um agecampo! Isso é um problema: não podemos chegar ao campo, porque tem oprivatemodificador, e o getter e o setter foram excluídos pelo desenvolvedor privado de sono que criou a classe :/ Bem, a reflexão pode nos ajudar nessa situação! Temos acesso ao código da Catclasse, então podemos pelo menos descobrir quais campos ela possui e como são chamados. Munidos dessas informações, podemos resolver nosso problema:

import learn.codegym.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.codegym.Cat");
           cat = (Cat) clazz.newInstance();

           // We got lucky with the name field, since it has a setter
           cat.setName("Fluffy");

           Field age = clazz.getDeclaredField("age");
          
           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Conforme declarado nos comentários, tudo com o namecampo é direto, pois os desenvolvedores da classe forneceram um setter. Você já sabe como criar objetos a partir de construtores padrão: temos o newInstance()para isso. Mas teremos que fazer alguns ajustes no segundo campo. Vamos descobrir o que está acontecendo aqui :)

Field age = clazz.getDeclaredField("age");
Aqui, usando nosso Class clazzobjeto , acessamos o agecampo através do getDeclaredField()método. Isso nos permite obter o campo de idade como um Field ageobjeto. Mas isso não é suficiente, porque não podemos simplesmente atribuir valores aos privatecampos. Para fazer isso, precisamos tornar o campo acessível usando o setAccessible()método:

age.setAccessible(true);
Depois de fazer isso em um campo, podemos atribuir um valor:

age.set(cat, 6);
Como você pode ver, nosso Field ageobjeto tem uma espécie de setter de dentro para fora para o qual passamos um valor int e o objeto cujo campo deve ser atribuído. Executamos nosso main()método e vemos:

Cat{name='Fluffy', age=6}
Excelente! Conseguimos! :) Vamos ver o que mais podemos fazer...

Como chamar um método de instância pelo nome.

Vamos mudar um pouco a situação no exemplo anterior. Digamos que o Catdesenvolvedor da classe não cometeu nenhum erro com os getters e setters. Está tudo bem a esse respeito. Agora o problema é diferente: existe um método que definitivamente precisamos, mas o desenvolvedor o tornou privado:

private void sayMeow() {

   System.out.println("Meow!");
}
Isso significa que, se criarmos Catobjetos em nosso programa, não poderemos chamar o sayMeow()método neles. Teremos gatos que não miam? Isso é estranho :/ Como consertaríamos isso? Mais uma vez, a API do Reflection nos ajuda! Sabemos o nome do método de que precisamos. Todo o resto é tecnicalidade:

import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Fluffy", 6);
          
           clazz = Class.forName(Cat.class.getName());
          
           Method sayMeow = clazz.getDeclaredMethod("sayMeow");
          
           sayMeow.setAccessible(true);
          
           sayMeow.invoke(cat);
          
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
Aqui fazemos praticamente a mesma coisa que fizemos ao acessar um campo privado. Primeiro, obtemos o método de que precisamos. É encapsulado em um Methodobjeto:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
O getDeclaredMethod()método nos permite chegar aos métodos privados. Em seguida, tornamos o método chamável:

sayMeow.setAccessible(true);
E por fim, chamamos o método no objeto desejado:

sayMeow.invoke(cat);
Aqui, nossa chamada de método se parece com um "callback": estamos acostumados a usar um ponto para apontar um objeto para o método desejado ( cat.sayMeow()), mas ao trabalhar com reflexão, passamos para o método o objeto que queremos chamar esse método. O que está em nosso console?

Meow!
Tudo funcionou! :) Agora você pode ver as vastas possibilidades que o mecanismo de reflexão do Java nos oferece. Em situações difíceis e inesperadas (como nossos exemplos com uma classe de uma biblioteca fechada), isso pode nos ajudar muito. Mas, como acontece com qualquer grande poder, traz consigo uma grande responsabilidade. As desvantagens da reflexão são descritas em uma seção especial no site da Oracle. Existem três desvantagens principais:
  1. O desempenho é pior. Os métodos chamados usando a reflexão têm desempenho pior do que os métodos chamados da maneira normal.

  2. Existem restrições de segurança. O mecanismo de reflexão nos permite alterar o comportamento de um programa em tempo de execução. Mas no seu local de trabalho, ao trabalhar em um projeto real, você pode enfrentar limitações que não permitem isso.

  3. Risco de exposição de informações internas. É importante entender que a reflexão é uma violação direta do princípio do encapsulamento: ela nos permite acessar campos privados, métodos etc. Acho que não preciso mencionar que uma violação direta e flagrante dos princípios da OOP deve ser utilizada para apenas nos casos mais extremos, quando não há outras maneiras de resolver um problema por motivos fora de seu controle.

Use a reflexão com sabedoria e apenas em situações em que ela não pode ser evitada e não se esqueça de suas deficiências. Com isso, nossa aula chegou ao fim. Acabou sendo muito longo, mas você aprendeu muito hoje :)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION