Fundamentalmente, reflexão significa usar o código do seu programa como dados.
Portanto, usar a reflexão pode ser uma boa idéia quando o código do seu programa é uma fonte útil de dados. (Mas existem compensações, portanto nem sempre é uma boa ideia.)
Por exemplo, considere uma classe simples:
public class Foo {
public int value;
public string anotherValue;
}
e você deseja gerar XML a partir dele. Você pode escrever um código para gerar o XML:
public XmlNode generateXml(Foo foo) {
XmlElement root = new XmlElement("Foo");
XmlElement valueElement = new XmlElement("value");
valueElement.add(new XmlText(Integer.toString(foo.value)));
root.add(valueElement);
XmlElement anotherValueElement = new XmlElement("anotherValue");
anotherValueElement.add(new XmlText(foo.anotherValue));
root.add(anotherValueElement);
return root;
}
Mas isso é muito código clichê, e toda vez que você altera a classe, é necessário atualizar o código. Realmente, você pode descrever o que esse código faz como
- crie um elemento XML com o nome da classe
- para cada propriedade da classe
- crie um elemento XML com o nome da propriedade
- coloque o valor da propriedade no elemento XML
- adicione o elemento XML à raiz
Este é um algoritmo, e a entrada do algoritmo é a classe: precisamos do nome e dos nomes, tipos e valores de suas propriedades. É aqui que entra a reflexão: fornece acesso a essas informações. Java permite inspecionar tipos usando os métodos da Class
classe.
Mais alguns casos de uso:
- definir URLs em um servidor da Web com base nos nomes de métodos de uma classe e parâmetros de URL com base nos argumentos do método
- converter a estrutura de uma classe em uma definição de tipo GraphQL
- chama todos os métodos de uma classe cujo nome começa com "test" como um caso de teste de unidade
No entanto, reflexão completa significa não apenas olhar para o código existente (que por si só é conhecido como "introspecção"), mas também modificar ou gerar código. Existem dois casos de uso proeminentes em Java para isso: proxies e zombarias.
Digamos que você tenha uma interface:
public interface Froobnicator {
void froobnicateFruits(List<Fruit> fruits);
void froobnicateFuel(Fuel fuel);
// lots of other things to froobnicate
}
e você tem uma implementação que faz algo interessante:
public class PowerFroobnicator implements Froobnicator {
// awesome implementations
}
E, de fato, você tem uma segunda implementação também:
public class EnergySaverFroobnicator implements Froobnicator {
// efficient implementations
}
Agora você também deseja alguma saída de log; você simplesmente deseja uma mensagem de log sempre que um método é chamado. Você pode adicionar saída de log a todos os métodos explicitamente, mas isso seria irritante e você teria que fazer isso duas vezes; uma vez para cada implementação. (Ainda mais quando você adiciona mais implementações.)
Em vez disso, você pode escrever um proxy:
public class LoggingFroobnicator implements Froobnicator {
private Logger logger;
private Froobnicator inner;
// constructor that sets those two
public void froobnicateFruits(List<Fruit> fruits) {
logger.logDebug("froobnicateFruits called");
inner.froobnicateFruits(fruits);
}
public void froobnicateFuel(Fuel fuel) {
logger.logDebug("froobnicateFuel( called");
inner.froobnicateFuel(fuel);
}
// lots of other things to froobnicate
}
Novamente, porém, há um padrão repetitivo que pode ser descrito por um algoritmo:
- um proxy de logger é uma classe que implementa uma interface
- ele tem um construtor que leva outra implementação da interface e um logger
- para cada método na interface
- a implementação registra uma mensagem "$ methodname chamada"
- e depois chama o mesmo método na interface interna, transmitindo todos os argumentos
e a entrada desse algoritmo é a definição da interface.
O Reflection permite definir uma nova classe usando esse algoritmo. Java permite fazer isso usando os métodos da java.lang.reflect.Proxy
classe, e existem bibliotecas que oferecem ainda mais poder.
Então, quais são as desvantagens da reflexão?
- Seu código fica mais difícil de entender. Você é um nível de abstração mais afastado dos efeitos concretos do seu código.
- Seu código fica mais difícil de depurar. Especialmente nas bibliotecas geradoras de código, o código executado pode não ser o código que você escreveu, mas o código que você gerou e o depurador pode não ser capaz de mostrar esse código (ou permitir que você coloque pontos de interrupção).
- Seu código fica mais lento. A leitura dinâmica de informações de tipo e o acesso a campos por seus identificadores de tempo de execução, em vez do acesso codificado, é mais lento. A geração dinâmica de código pode atenuar esse efeito, ao custo de ser ainda mais difícil de depurar.
- Seu código pode se tornar mais frágil. O acesso à reflexão dinâmica não é verificado pelo tipo pelo compilador, mas gera erros no tempo de execução.