Greetings, young Padawan. In this article, I'll tell you about the Force, a power that Java programmers only use in seemingly impossible situations. The dark side of Java is the Reflection API.
In Java, reflection is implemented using the Java Reflection API.
What is Java reflection?
There is a short, accurate, and popular definition on the Internet. Reflection (from late Latin reflexio - to turn back) is a mechanism to explore data about a program while it is running. Reflection lets you explore information about fields, methods, and class constructors. Reflection lets you work with types that weren't present at compile time, but which became available during run time. Reflection and a logically consistent model for issuing error information make it possible to create correct dynamic code. In other words, an understanding how reflection works in Java will open up a number of amazing opportunities for you. You can literally juggle classes and their components. Here is a basic list of what reflection allows:- Learn/determine an object's class;
- Get information about a class's modifiers, fields, methods, constants, constructors, and superclasses;
- Find out what methods belong to implemented interface(s);
- Create an instance of a class whose class name is unknown until run time;
- Get and set values of an object's fields by name;
- Call an object's method by name.
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);
}
}
As you can see, this is a very basic class. The constructor with parameters is deliberately commented out. We'll come back to that later. If you looked carefully at the contents of the class, you probably noticed the absence of a getter for the name field. The name field itself is marked with the private access modifier: we can't access it outside of the class itself which means we can't retrieve its value.
"So what's the problem?" you say. "Add a getter or change the access modifier". And you would be right, unless MyClass
was in a compiled AAR library or in another private module with no ability to make changes. In practice, this happens all the time. And some careless programmer simply forgot to write a getter. This is the very time to remember reflection!
Let's try to get to the private name field of the MyClass
class:
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
}
Let's analyze what just happened. In Java, there is a wonderful class called Class
. It represents classes and interfaces in an executable Java application. We won't cover the relationship between Class
and ClassLoader
, since that isn't the topic of this article.
Next, to retrieve this class's fields, you need to call the getFields()
method. This method will return all of this class's accessible fields. This doesn't work for us, because our field is private, so we use the getDeclaredFields()
method. This method also returns an array of class fields, but now it includes private and protected fields. In this case, we know the name of the field we're interested in, so we can use the getDeclaredField(String)
method, where String
is the desired field's name.
Note: getFields()
and getDeclaredFields()
do not return the fields of a parent class!
Great. We got a Field
object referencing our name. Since the field was not public, we must grant access to work with it. The setAccessible(true)
method lets us proceed further. Now the name field is under our complete control! You can retrieve its value by calling the Field
object's get(Object)
method, where Object
is an instance of our MyClass
class. We convert the type to String
and assign the value to our name variable.
If we can't find a setter to set a new value to the name field, you can use the set method:
field.set(myClass, (String) "new value");
Congratulations! You've just mastered the basics of reflection and accessed a private field!
Pay attention to the try/catch
block, and the types of exceptions being handled. The IDE will tell you their presence is required on its own, but you can clearly tell by their names why they are here.
Moving on! As you might have noticed, our MyClass
class already has a method for displaying information about class data:
private void printData(){
System.out.println(number + name);
}
But this programmer left his fingerprints here too. The method has a private access modifier, and we have to write our own code to display data every time. What a mess. Where'd our reflection go? Write the following function:
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();
}
}
The procedure here is about the same as the one used for retrieving a field. We access the desired method by name and grant access to it. And on the Method
object we call the invoke(Object, Args)
method, where Object
is also an instance of the MyClass
class. Args
are the method's arguments, though ours doesn't have any. Now we use the printData
function to display information:
public static void main(String[] args) {
MyClass myClass = new MyClass();
int number = myClass.getNumber();
String name = null; //?
printData(myClass); // Output: 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
}
Hurray! Now we have access to the class's private method. But what if the method does have arguments, and why is the constructor commented out? Everything in its own due time.
It is clear from the definition at the beginning that reflection lets you create instances of a class at run time (while the program is running)! We can create an object using the class's full name. The class's full name is the class name, including the path of its package.
In my package hierarchy, the full name of MyClass would be "reflection.MyClass". There's also a simple way to learn a class's name (return the class's name as a String):
MyClass.class.getName()
Let's use Java reflection to create an instance of the class:
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
}
When a Java application starts, not all classes are loaded into the JVM. If your code doesn't refer to the MyClass
class, then ClassLoader
, which is responsible for loading classes into the JVM, will never load the class. That means you have to force ClassLoader
to load it and get a class description in the form of a Class
variable. This is why we have the forName(String)
method, where String
is the name of the class whose description we need.
After getting the Сlass
object, calling the method newInstance()
will return an Object
object created using that description. All that's left is to supply this object to our MyClass
class.
Cool! That was difficult, but understandable, I hope. Now we can create an instance of a class in literally one line! Unfortunately, the described approach will only work with the default constructor (without parameters).
How do you call methods and constructors with parameters?
It's time to uncomment our constructor. As expected, newInstance()
can't find the default constructor, and no longer works.
Let's rewrite the class instantiation:
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
}
The getConstructors()
method should be called on the class definition to obtain class constructors, and then getParameterTypes()
should be called to get a constructor's parameters:
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
Class[] paramTypes = constructor.getParameterTypes();
for (Class paramType : paramTypes) {
System.out.print(paramType.getName() + " ");
}
System.out.println();
}
That gets us all of the constructors and their parameters. In my example, I refer to a specific constructor with specific, previously known parameters. And to call this constructor, we use the newInstance
method, to which we pass the values of these parameters. It will be the same when using invoke
to call methods. This begs the question: when does calling constructors through reflection come in handy? As already mentioned at the beginning, modern Java technologies can't get by without the Java Reflection API. For example, Dependency Injection (DI), which combines annotations with reflection of methods and constructors to form the popular Darer library for Android development.
After reading this article, you can confidently consider yourself educated in the ways of the Java Reflection API. They don't call reflection the dark side of Java for nothing. It completely breaks the OOP paradigm. In Java, encapsulation hides and restricts others' access to certain program components. When we use the private modifier, we intend for that field to only be accessed from within the class where it exists. And we build the program's subsequent architecture based on this principle. In this article, we've seen how you can use reflection to force your way anywhere.
The creational design pattern Singleton is a good example of this as an architectural solution. The basic idea is that a class implementing this pattern will only have one instance during execution of the entire program. This is accomplished by adding the private access modifier to the default constructor. And it would be very bad if a programmer used reflection ot create more instances of such classes.
By the way, I recently heard a coworker ask a very interesting question: can a class that implements the Singleton pattern be inherited? Could it be that, in this case, even reflection would be powerless?
Leave your feedback about the article and your answer in the comments below, and ask your own questions there!More reading: |
---|
GO TO FULL VERSION