Fundamentally, reflection means using the code of your program as data.
Therefore, using reflection might be a good idea when your program's code is a useful source of data. (But there are trade-offs, so it might not always be a good idea.)
For example, consider a simple class:
public class Foo {
public int value;
public string anotherValue;
}
and you want to generate XML from it. You could write code to generate the 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;
}
But this is a lot of boilerplate code, and everytime you change the class, you have to update the code. Really, you could describe what this code does as
- create an XML element with the name of the class
- for each property of the class
- create an XML element with the name of the property
- put the value of the property into the XML element
- add the XML element to the root
This is an algorithm, and the algorithm's input is the class: we need its name, and the names, types and values of its properties. This is where reflection comes in: it gives you access to this information. Java allows you to inspect types using the methods of the Class
class.
Some more use cases:
- define URLs in a webserver based on a class's method names, and URL parameters based on the method arguments
- convert the structure of a class into a GraphQL type definition
- call every method of a class whose name starts with "test" as a unit test case
However, full reflection means not only looking at existing code (which by itself is known as "introspection"), but also modifying or generating code. There are two prominent use cases in Java for this: proxies and mocks.
Let's say you have an interface:
public interface Froobnicator {
void froobnicateFruits(List<Fruit> fruits);
void froobnicateFuel(Fuel fuel);
// lots of other things to froobnicate
}
and you have an implementation that does something interesting:
public class PowerFroobnicator implements Froobnicator {
// awesome implementations
}
And in fact you have a second implementation too:
public class EnergySaverFroobnicator implements Froobnicator {
// efficient implementations
}
Now you also want some log output; you simply want a log message whenever a method is called. You could add log output to every method explicitly, but that would be annoying, and you'd have to do it twice; once for each implementation. (So even more when you add more implementations.)
Instead, you can write a 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
}
Again, though, there is a repetitive pattern that can be described by an algorithm:
- a logger proxy is a class that implements an interface
- it has a constructor that takes another implementation of the interface and a logger
- for every method in the interface
- the implementation logs a message "$methodname called"
- and then calls the same method on the inner interface, passing along all arguments
and the input of this algorithm is the interface definition.
Reflection allows you to define a new class using this algorithm. Java allows you to do this using the methods of the java.lang.reflect.Proxy
class, and there are libraries that give you even more power.
So what are the downsides of reflection?
- Your code becomes harder to understand. Your are one level of abstraction further removed from the concrete effects of your code.
- Your code becomes harder to debug. Especially with code-generating libraries, the code that is executed may not be the code that you wrote, but the code you generated, and the debugger may not be able to show you that code (or let you place breakpoints).
- Your code becomes slower. Dynamically reading type information and accessing fields by their runtime handles instead of hard-coding access is slower. Dynamic code generation can mitigate this effect, at the cost of being even harder to debug.
- Your code may become more fragile. Dynamic reflection access is not type-checked by the compiler, but throws errors at runtime.