JavaRush /Blogue Java /Random-PT /Reflexão em Java - Exemplos de uso

Reflexão em Java - Exemplos de uso

Publicado no grupo Random-PT
Você deve ter se deparado com o conceito de “reflexão” na vida cotidiana. Geralmente esta palavra se refere ao processo de auto-estudo. Na programação, tem um significado semelhante - é um mecanismo para examinar dados sobre um programa, bem como alterar a estrutura e o comportamento do programa durante sua execução. O importante aqui é que isso seja feito em tempo de execução, não em tempo de compilação. Mas por que examinar o código em tempo de execução? Você já percebeu :/ Exemplos de uso de Reflexão - 1A ideia de reflexão pode não ficar imediatamente clara por um motivo: até esse momento, você sempre conhecia as turmas com as quais estava trabalhando. Bem, por exemplo, você poderia escrever uma classe Cat:
package learn.javarush;

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 isso, vê quais campos e métodos ele possui. Certamente você pode criar um sistema de herança com uma classe comum por conveniência Animal, se de repente o programa precisar de outras classes de animais. Anteriormente, criamos até uma classe de clínica veterinária na qual você poderia passar um objeto pai Animal, e o programa trataria o animal dependendo se era um cachorro ou um gato. Embora essas tarefas não sejam muito simples, o programa aprende todas as informações necessárias sobre as classes em tempo de compilação. Portanto, quando você main()passa um objeto de um método Catpara os métodos da classe clínica veterinária, o programa já sabe que se trata de um gato e não de um cachorro. Agora vamos imaginar que estamos diante de outra tarefa. Nosso objetivo é escrever um analisador de código. Precisamos criar uma classe CodeAnalyzercom um único método - void analyzeClass(Object o). Este método deve:
  • determine qual classe o objeto foi passado para ele e exiba o nome da classe no console;
  • determine os nomes de todos os campos desta classe, inclusive os privados, e exiba-os no console;
  • determine os nomes de todos os métodos desta classe, incluindo os privados, e exiba-os no console.
Vai parecer algo assim:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит an object o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
Agora a diferença entre este problema e os outros problemas que você resolveu antes é visível. Nesse caso, a dificuldade está no fato de nem você nem o programa saberem exatamente o que será passado para o método analyzeClass(). Você escreve um programa, outros programadores começarão a usá-lo, podendo passar qualquer coisa para esse método - qualquer classe Java padrão ou qualquer classe que eles tenham escrito. Esta classe pode ter qualquer número de variáveis ​​e métodos. Em outras palavras, neste caso nós (e nosso programa) não temos ideia com quais classes iremos trabalhar. E, no entanto, devemos resolver este problema. E aqui a biblioteca Java padrão vem em nosso auxílio - a API Java Reflection. A API Reflection é um recurso de linguagem poderoso. A documentação oficial da Oracle afirma que este mecanismo é recomendado para ser utilizado apenas por programadores experientes que entendem muito bem o que estão fazendo. Você logo entenderá por que recebemos tais avisos com antecedência :) Aqui está uma lista do que pode ser feito usando a API Reflection:
  1. Descubra/determine 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 à interface/interfaces implementadas.
  4. Crie uma instância de uma classe quando o nome da classe for desconhecido até que o programa seja executado.
  5. Obtenha e defina o valor de um campo de objeto por nome.
  6. Chame o método de um objeto pelo nome.
Lista impressionante, não? :) Prestar atenção:O mecanismo de reflexão é capaz de fazer tudo isso “dinamicamente”, independentemente de qual objeto de classe passamos para nosso analisador de código! Vejamos os recursos da API Reflection com exemplos.

Como descobrir/determinar a classe de um objeto

Vamos começar com o básico. O ponto de entrada para o mecanismo de reflexão do Java é o Class. Sim, parece muito engraçado, mas é para isso que serve a reflexão :) Usando uma classe Class, primeiro determinamos a classe de qualquer objeto passado para o nosso método. Vamos tentar isso:
import learn.javarush.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("Barsik", 6));
   }
}
Saída do console:

class learn.javarush.Cat
Preste atenção em duas coisas. Primeiramente, colocamos deliberadamente a classe Catem um pacote separado. learn.javarush;Agora você pode ver que ele getClass()retorna o nome completo da classe. Em segundo lugar, nomeamos nossa variável clazz. Parece um pouco estranho. Claro, deveria ser chamada de “classe”, mas “classe” é uma palavra reservada na linguagem Java, e o compilador não permitirá que variáveis ​​sejam chamadas dessa forma. Eu tive que sair dessa :) Bem, não é um mau começo! O que mais tínhamos na lista de possibilidades?

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

Isso já é mais interessante! Na classe atual não temos constantes nem classe pai. Vamos adicioná-los para completar. Vamos criar a classe pai mais simples Animal:
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
E vamos adicionar Catherança Animale uma constante à nossa classe:
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
Agora temos um conjunto completo! Vamos experimentar as possibilidades de reflexão :)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Name класса: " + clazz);
       System.out.println("Поля класса: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Родительский класс: " + clazz.getSuperclass());
       System.out.println("Методы класса: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструкторы класса: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Isto é o que obtemos no console:
Name класса: class learn.javarush.Cat
Поля класса: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Родительский класс: class learn.javarush.Animal
Методы класса: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструкторы класса: [public learn.javarush.Cat(java.lang.String,int)]
Recebemos muitas informações detalhadas sobre a aula! E não só sobre o público, mas também sobre o privado. Prestar atenção: private-variables também são exibidas na lista. Na verdade, a “análise” da aula pode ser considerada completa neste ponto: agora, utilizando o método, analyzeClass()aprenderemos tudo o que for possível. Mas estas não são todas as possibilidades que temos quando trabalhamos com reflexão. Não nos limitemos à simples observação e passemos à ação ativa! :)

Como criar uma instância de uma classe se o nome da classe for desconhecido antes da execução do programa

Vamos começar com o construtor padrão. Ainda não está em nossa classe Cat, então vamos adicioná-lo:
public Cat() {

}
Esta é a aparência do código para criar um objeto Catusando reflexão (método createCat()):
import learn.javarush.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());
   }
}
Entre no console:

learn.javarush.Cat
Saída do console:

Cat{name='null', age=0}
Isso não é um erro: os valores namee agesão exibidos no console porque programamos sua saída no método toString()de classe Cat. Aqui lemos o nome da classe cujo objeto criaremos no console. O programa em execução aprende o nome da classe cujo objeto irá criar. Exemplos de uso de Reflexão - 3Por uma questão de brevidade, omitimos o código para o tratamento adequado de exceções, para que não ocupe mais espaço do que o próprio exemplo. Em um programa real, é claro, definitivamente vale a pena lidar com situações em que nomes incorretos são inseridos, etc. O construtor padrão é algo bastante simples, então criar uma instância de uma classe usando-o, como você pode ver, não é difícil :) E usando o método, newInstance()criamos um novo objeto desta classe. Outra questão é se o construtor da classe Catrecebe parâmetros como entrada. Vamos remover o construtor padrão da classe e tentar executar nosso código novamente.

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
Algo deu errado! Recebemos um erro porque chamamos um método para criar um objeto por meio do construtor padrão. Mas agora não temos esse designer. Isso significa que quando o método funcionar, newInstance()o mecanismo de reflexão usará 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 os tivéssemos esquecido completamente! Para passá-los para o construtor usando reflexão, você terá que ajustá-lo um pouco:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 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='Barsik', age=6}
Vamos dar uma olhada mais de perto no que está acontecendo em nosso programa. Criamos uma matriz de objetos Class.
Class[] catClassParams = {String.class, int.class};
Eles correspondem aos parâmetros do nosso construtor (temos apenas os parâmetros Stringe int). Nós os passamos para o método clazz.getConstructor()e obtemos acesso ao construtor necessário. Depois disso, resta chamar o método newInstance()com os parâmetros necessários e não se esquecer de lançar explicitamente o objeto para a classe que precisamos - Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
Como resultado, nosso objeto será criado com sucesso! Saída do console:

Cat{name='Barsik', age=6}
Vamos continuar :)

Como obter e definir o valor de um campo de objeto por nome

Imagine que você está usando uma classe escrita por outro programador. No entanto, você não tem a oportunidade de editá-lo. Por exemplo, uma biblioteca de classes pronta, empacotada em um JAR. Você pode ler o código da classe, mas não pode alterá-lo. O programador que criou a classe nesta biblioteca (que seja nossa classe antiga Cat) não dormiu o suficiente antes do design final e removeu os getters e setters do campo age. Agora essa aula chegou até você. Atende plenamente às suas necessidades, pois você só precisa de objetos no programa Cat. Mas você precisa deles com o mesmo campo age! Isso é um problema: não conseguimos alcançar o campo, pois ele possui um modificador private, e os getters e setters foram removidos pelo aspirante a desenvolvedor desta classe :/ Bom, a reflexão pode nos ajudar nessa situação também! CatTemos acesso ao código da classe : podemos pelo menos descobrir quais campos ela possui e como são chamados. Munidos dessas informações, resolvemos nosso problema:
import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

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

           //с полем name нам повезло - для него в классе есть setter
           cat.setName("Barsik");

           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 no comentário, nametudo é simples com o campo: os desenvolvedores da classe forneceram um setter para ele. Você também já sabe como criar objetos a partir de construtores padrão: existe um método para isso newInstance(). Mas você terá que mexer no segundo campo. Vamos descobrir o que está acontecendo aqui :)
Field age = clazz.getDeclaredField("age");
Aqui nós, usando nosso objeto Class clazz, acessamos o campo ageusando o getDeclaredField(). Isso nos dá a capacidade de obter o campo age como um objeto Field age. Mas isso ainda não é suficiente, porque privateos campos não podem simplesmente receber valores. Para fazer isso, você precisa tornar o campo “disponível” usando o método setAccessible():
age.setAccessible(true);
Os campos para os quais isso é feito podem receber valores:
age.set(cat, 6);
Como você pode ver, temos uma espécie de setter virado de cabeça para baixo: atribuímos ao campo Field ageseu valor, e também passamos a ele o objeto ao qual esse campo deve ser atribuído. Vamos executar nosso método main()e ver:

Cat{name='Barsik', age=6}
Ótimo, fizemos tudo! :) Vamos ver que outras possibilidades temos...

Como chamar o método de um objeto pelo nome

Vamos mudar um pouco a situação do exemplo anterior. Digamos que o desenvolvedor da classe Catcometeu um erro com os campos - ambos estão disponíveis, existem getters e setters para eles, está tudo bem. O problema é diferente: ele tornou privado um método que definitivamente precisamos:
private void sayMeow() {

   System.out.println("Meow!");
}
Como resultado, criaremos objetos Catem nosso programa, mas não poderemos chamar seus métodos sayMeow(). Teremos gatos que não miam? Muito estranho:/Como posso consertar isso? Mais uma vez, a API Reflection vem em socorro! Sabemos o nome do método necessário. O resto é uma questão de técnica:
import learn.javarush.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("Barsik", 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 agimos da mesma forma que na situação de acesso a um domínio privado. Primeiro, obtemos o método que precisamos, que está encapsulado em um objeto de classe Method:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Com ajuda, getDeclaredMethod()você pode “alcançar” métodos privados. Em seguida, tornamos o método chamável:
sayMeow.setAccessible(true);
E finalmente, chamamos o método no objeto desejado:
sayMeow.invoke(cat);
Chamar um método também se parece com uma “chamada ao contrário”: estamos acostumados a apontar um objeto para o método desejado usando um ponto ( cat.sayMeow()), e ao trabalhar com reflexão, passamos para o método o objeto a partir do qual ele precisa ser chamado . O que temos no console?

Meow!
Deu tudo certo! :) Agora você vê as amplas possibilidades que o mecanismo de reflexão em Java nos oferece. Em situações difíceis e inesperadas (como nos exemplos de uma turma de uma biblioteca fechada), pode realmente nos ajudar muito. No entanto, como qualquer grande potência, também implica 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. A produtividade diminui. Os métodos chamados usando reflexão têm desempenho inferior aos métodos chamados normalmente.

  2. Existem restrições de segurança. O mecanismo de reflexão permite alterar o comportamento do programa durante a execução. Mas no seu ambiente de trabalho em um projeto real pode haver restrições que não permitem que você faça isso.

  3. Risco de divulgação de informações privilegiadas. É importante entender que usar reflexão viola diretamente o princípio do encapsulamento: ela nos permite acessar campos, métodos privados, etc. Acho que não há necessidade de explicar que a violação direta e grosseira dos princípios da OOP deve ser utilizada apenas nos casos mais extremos, quando não há outras maneiras de resolver o problema por motivos alheios ao seu controle.

Utilize o mecanismo de reflexão com sabedoria e apenas em situações em que não possa ser evitado, e não se esqueça das suas deficiências. Isso conclui nossa palestra! Acabou sendo bem grande, mas hoje você aprendeu muitas coisas novas :)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION