Saudações, jovem Padawan. Neste artigo vou falar sobre a Força, cujo poder os programadores Java usam apenas em uma situação aparentemente desesperadora. Então, o lado negro do Java é -
A reflexão em Java é feita usando a API Java Reflection. O que é essa reflexão? Existe uma definição curta e precisa que também é popular na Internet. Reflexão (do latim tardio reflexio - voltar atrás) é um mecanismo para estudar dados sobre um programa durante sua execução. O Reflection permite examinar informações sobre campos, métodos e construtores de classes. O próprio mecanismo de reflexão permite processar tipos que faltam durante a compilação, mas aparecem durante a execução do programa. A reflexão e a presença de um modelo logicamente coerente para reportar erros possibilitam a criação de código dinâmico correto. Em outras palavras, entender como a reflexão funciona em java abre 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:
Na minha hierarquia,
Reflection API
- Descubra/determine a classe de um objeto;
- Obtenha informações sobre modificadores de classe, campos, métodos, constantes, construtores e superclasses;
- Descubra quais métodos pertencem à interface/interfaces implementadas;
- Crie uma instância de uma classe e o nome da classe será desconhecido até que o programa seja executado;
- Obtenha e defina o valor de um campo de objeto por nome;
- Chame o método de um objeto pelo nome.
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 podemos ver, esta é a classe mais comum. O construtor com parâmetros está comentado por um motivo, voltaremos a isso mais tarde. Se você olhou atentamente o conteúdo da aula, provavelmente notou a ausência de getter
'a para o name
. O campo em si name
está marcado com um modificador de acesso private
; não poderemos acessá-lo fora da própria classe; =>
não poderemos obter seu valor. "Então qual é o problema? - você diz. “Adicione getter
ou altere o modificador de acesso.” E você terá razão, mas e se MyClass
estiver em uma biblioteca aar compilada ou em outro módulo fechado sem acesso de edição, e na prática isso acontece com muita frequência. E algum programador desatento simplesmente se esqueceu de escrever getter
. É hora de lembrar da reflexão! Vamos tentar chegar ao private
campo name
da classe MyClass
:
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 descobrir o que aconteceu aqui agora. Existe uma aula maravilhosa em java Class
. Representa classes e interfaces em um aplicativo Java executável. Não tocaremos na conexão entre Class
e ClassLoader
. este não é o tema do artigo. A seguir, para obter os campos desta classe, é necessário chamar o método getFields()
, este método nos retornará todos os campos disponíveis da classe. Isso não é adequado para nós, já que nosso campo é private
, então usamos o método getDeclaredFields()
.Este método também retorna um array de campos de classe, mas agora ambos private
e protected
. Na nossa situação, sabemos o nome do campo que nos interessa, e podemos utilizar o método getDeclaredField(String)
, onde String
está o nome do campo desejado. Observação: getFields()
e getDeclaredFields()
não retorne os campos da classe pai! Ótimo, recebemos um objeto Field
com um link para nosso arquivo name
. Porque o campo não era публичным
(público), deveria ser dado acesso para trabalhar com ele. O método setAccessible(true)
nos permite continuar trabalhando. Agora o campo name
está completamente sob nosso controle! Você pode obter seu valor chamando get(Object)
o objeto Field
, onde Object
está uma instância de nossa classe MyClass
. Nós o lançamos String
e o atribuímos à nossa variável name
. Caso de repente não tenhamos setter
'a, podemos usar o método para definir um novo valor para o campo de nome set
:
field.set(myClass, (String) "new value");
Parabéns! Você acabou de dominar o mecanismo básico de reflexão e conseguiu acessar o private
campo! Preste atenção ao bloco try/catch
e aos tipos de exceções tratadas. O próprio IDE indicará sua presença obrigatória, mas seu nome deixa claro por que estão aqui. Vá em frente! Como você deve ter notado, o nosso MyClass
já possui um método para exibir informações sobre os dados da classe:
private void printData(){
System.out.println(number + name);
}
Mas este programador também deixou um legado aqui. O método está sob o modificador de acesso private
e tivemos que escrever o código de saída todas as vezes. Não está em ordem, onde está o nosso reflexo?... Vamos escrever 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();
}
}
Aqui o procedimento é aproximadamente o mesmo de obter um campo - obtemos o método desejado pelo nome e damos acesso a ele. E para chamar o objeto Method
que usamos invoke(Оbject, Args)
, onde Оbject
também está uma instância da classe MyClass
. Args
- argumentos do método - o nosso não possui nenhum. Agora usamos a função para exibir informações printData
:
public static void main(String[] args) {
MyClass myClass = new MyClass();
int number = myClass.getNumber();
String name = null; //?
printData(myClass); // outout 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 ainda tiver argumentos e por que existe um construtor comentado? Tudo tem o seu tempo. Pela definição inicial fica claro que a reflexão permite criar instâncias de uma classe em um modo runtime
(enquanto o programa está em execução)! Podemos criar um objeto de uma classe pelo nome totalmente qualificado dessa classe. O nome completo da classe é o nome da classe, considerando o caminho para ela no arquivo package
.
package
o nome completo MyClass
será “ reflection.MyClass
”. Você também pode descobrir o nome da classe de uma forma simples (ele retornará o nome da classe como uma string):
MyClass.class.getName()
Vamos criar uma instância da classe usando reflexão:
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
}
No momento em que o aplicativo Java é iniciado, nem todas as classes são carregadas na JVM. Se o seu código não se referir à classe MyClass
, então quem for responsável por carregar as classes na JVM, ou seja ClassLoader
, nunca irá carregá-lo lá. Portanto, precisamos forçá- ClassLoader
lo a carregar e receber uma descrição da nossa classe na forma de uma variável do tipo Class
. Para esta tarefa existe um método forName(String)
, onde String
está o nome da classe cuja descrição necessitamos. Tendo recebido Сlass
, newInstance()
retornará a chamada do método Object
, que será criada de acordo com a mesma descrição. Resta trazer esse objeto para nossa classe MyClass
. Legal! Foi difícil, mas espero que seja compreensível. Agora podemos criar uma instância de uma classe literalmente a partir de uma linha! Infelizmente, o método descrito só funcionará com o construtor padrão (sem parâmetros). Como chamar métodos com argumentos e construtores com parâmetros? É hora de descomentar nosso construtor. Como esperado, newInstance()
ele não encontra o construtor padrão e não funciona mais. Vamos reescrever a criação de uma instância de 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
}
Para obter construtores de classe, chame o método da descrição da classe getConstructors()
e, para obter parâmetros do construtor, chame getParameterTypes()
:
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
Class[] paramTypes = constructor.getParameterTypes();
for (Class paramType : paramTypes) {
System.out.print(paramType.getName() + " ");
}
System.out.println();
}
Desta forma obtemos todos os construtores e todos os parâmetros para eles. No meu exemplo, há uma chamada para um construtor específico com parâmetros específicos e já conhecidos. E para chamar esse construtor, utilizamos o método newInstance
, no qual especificamos os valores desses parâmetros. O mesmo acontecerá invoke
com a chamada de métodos. Surge a pergunta: onde a chamada reflexiva de construtores pode ser útil? As tecnologias Java modernas, conforme mencionado no início, não podem prescindir da API Reflection. Por exemplo, DI (Injeção de Dependência), onde anotações combinadas com reflexão de métodos e construtores formam a biblioteca Dagger, popular no desenvolvimento Android. Depois de ler este artigo, você pode se considerar confiante nos mecanismos da API Reflection. Não é à toa que a reflexão é chamada de lado negro do java. Isso quebra completamente o paradigma OOP. Em java, o encapsulamento serve para ocultar e limitar o acesso de alguns componentes do programa a outros. Ao usar o modificador private queremos dizer que o acesso a este campo estará apenas dentro da classe onde este campo existe, com base nisso construímos a arquitetura posterior do programa. Neste artigo, vimos como você pode usar a reflexão para chegar a qualquer lugar. Um bom exemplo na forma de solução arquitetônica é o padrão de design generativo - Singleton
. Sua ideia principal é que durante todo o funcionamento do programa, a classe que implementa este template tenha apenas uma cópia. Isso é feito definindo o modificador de acesso padrão como privado para o construtor. E será muito ruim se algum programador, com sua própria reflexão, criar tais classes. Aliás, há uma pergunta muito interessante que ouvi recentemente do meu funcionário: uma classe que implementa um template pode ter Singleton
herdeiros? É possível que mesmo a reflexão seja impotente neste caso? Escreva sua opinião sobre o artigo e a resposta nos comentários, e também tire suas dúvidas! O verdadeiro poder da API Reflection vem em combinação com Runtime Annotations, sobre as quais provavelmente falaremos em um artigo futuro sobre o lado negro do Java. Obrigado pela sua atenção!
GO TO FULL VERSION