Examples of reflection

Published in the Java Developer group
Maybe you've encountered the concept of "reflection" in ordinary life. This word usually refers to the process of studying oneself. In programming, it has a similar meaning — it is a mechanism for analyzing data about a program, and even changing the structure and behavior of a program, while the program is running. Examples of reflection - 1 The important here is that we're doing this at runtime, not at compile time. But why examine the code at runtime? After all, you can already read the code :/ There is a reason why the idea of reflection may not be immediately clear: up to this point, you always knew which classes you were working with. For example, you could write a Cat class:

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 +
           '}';
}

}
You know everything about it, and you can see the fields and methods that it has. Suppose you suddenly need to introduce other animal classes to the program. You could probably create a class inheritance structure with an Animal parent class for convenience. Earlier, we even created a class representing a veterinary clinic, to which we could pass an Animal object (instance of a parent class), and the program treated the animal appropriately based on whether it was a dog or a cat. Even though these aren't the simplest tasks, the program is able to learn all the necessary information about the classes at compile time. Accordingly, when you pass a Cat object to the methods of the veterinary clinic class in the main() method, the program already knows that it is a cat, not a dog. Now let's imagine that we're face a different task. Our goal is to write a code analyzer. We need to create a CodeAnalyzer class with a single method: void analyzeObject(Object o). This method should:
  • determine the class of the object passed to it and display the class name on the console;
  • determine the names of all fields of the passed class, including private ones, and display them on the console;
  • determine the names of all methods of the passed class, including private ones, and display them on the console.
It will look something like this:

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
      
       // Print the name of the class of object o
       // Print the names of all variables of this class
       // Print the names of all methods of this class
   }
  
}
Now we can clearly see how this task differs from other tasks that you have solved previously. With our current objective, the difficulty lies in the fact that neither we nor the program knows what exactly will be passed to the analyzeClass() method. If you write such a program, other programmers will start using it, and they might pass anything to this method — any standard Java class or any other class they write. The passed class can have any number of variables and methods. In other words, we (and our program) have no idea what classes we will be working with. But still, we need to complete this task. And this is where the standard Java Reflection API comes to our aid. The Reflection API is a powerful tool of the language. Oracle's official documentation recommends that this mechanism should only be used by experienced programmers who know what they are doing. You will soon understand why we're giving this sort of warning in advance :) Here is a list of things you can do with the Reflection API:
  1. Identify/determine the class of an object.
  2. Get information about class modifiers, fields, methods, constants, constructors, and superclasses.
  3. Find out which methods belong to an implemented interface(s).
  4. Create an instance of a class whose class name is not known until the program is executed.
  5. Get and set the value of an instance field by name.
  6. Call an instance method by name.
Impressive list, huh? :) Note: the reflection mechanism can do all this stuff "on the fly", regardless of the type of object we pass to our code analyzer! Let's explore the Reflection API's capabilities by looking at some examples.

How to identify/determine the class of an object

Let's start with the basics. The entry point to the Java reflection engine is the Class class. Yes, it looks really funny, but that's what reflection is :) Using the Class class, we first determine the class of any object passed to our method. Let's try to do this:

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("Fluffy", 6));
   }
}
Console output:

class learn.javarush.Cat
Pay attention to two things. First, we deliberately put the Cat class in a separate learn.javarush package. Now you can see that the getClass() method returns the full name of the class. Second, we named our variable clazz. That looks a little strange. It would make sense to call it "class", but "class" is a reserved word in Java. The compiler won't allow variables to be called that. We had to get around that somehow :) Not bad for a start! What else did we have on that list of capabilities? Examples of reflection - 2

How to get information about class modifiers, fields, methods, constants, constructors, and superclasses.

Now things are getting more interesting! In the current class, we don't have any constants or a parent class. Let's add them to create a complete picture. Create the simplest Animal parent class:

package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
And we'll make our Cat class inherit Animal and add one constant:

package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Feline family";

   private String name;
   private int age;

   // ...the rest of the class
}
Now we have the complete picture! Let's see what reflection is capable of :)

import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Class name: " + clazz);
       System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Parent class: " + clazz.getSuperclass());
       System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Here's what we see on the console:

Class name:  class learn.javarush.Cat 
Class fields: [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] 
Parent class: class learn.javarush.Animal 
Class methods: [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()] 
Class constructors: [public learn.javarush.Cat(java.lang.String, int)]
Look at all that detailed class information that we were able to get! And not just public information, but also private information! Note: private variables are also displayed in the list. Our "analysis" of the class can be considered essentially complete: we're using the analyzeObject() method to learn everything we can. But this isn't everything we can do with reflection. We are not limited to simple observation — we'll move on to taking action! :)

How to create an instance of a class whose class name is not known until the program is executed.

Let's start with the default constructor. Our Cat class doesn't have one yet, so let's add it:

public Cat() {
  
}
Here's the code for creating a Cat object using reflection (createCat() method):

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());
   }
}
Console input:

learn.javarush.Cat
Console output:

Cat{name='null', age=0}
This is not an error: the values of name and age are displayed on the console because we wrote code to output them in the toString() method of the Cat class. Here we read the name of a class whose object we will create from the console. The program recognizes the name of the class whose object is to be created. Examples of reflection - 3For brevity's sake, we omitted proper exception handling code, which would take up more space than the example itself. In a real program, of course, you should handle situations involving incorrectly entered names, etc. The default constructor is pretty simple, so as you can see, it is easy to use it to create an instance of the class :) Using the newInstance() method, we create a new object of this class. It's another matter if the Cat constructor takes arguments as input. Let's remove the class's default constructor and try to run our code again.

null
java.lang.InstantiationException: learn.javarush.Cat 
at java.lang.Class.newInstance(Class.java:427)
Something went wrong! We got an error because we called a method to create an object using the default constructor. But we don't have such a constructor now. So when the newInstance() method runs, the reflection mechanism uses our old constructor with two parameters:

public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
But we didn't do anything with the parameters, as if we had entirely forgotten about them! Using reflection to passing arguments to the constructor requires a little "creativity":

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.codegym.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 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());
   }
}
Console output:

Cat{name='Fluffy', age=6}
Let's take a closer look at what's happening in our program. We created an array of Class objects.

Class[] catClassParams = {String.class, int.class};
They correspond to the parameters of our constructor (which just has String and int parameters). We pass them to the clazz.getConstructor()method and gain access to the desired constructor. After that, all we need to do is call the newInstance() method with the necessary arguments, and don't forget to explicitly cast the object to the desired type: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Now our object is successfully created! Console output:

Cat{name='Fluffy', age=6}
Moving right along :)

How to get and set the value of an instance field by name.

Imagine that you're using a class written by another programmer. Additionally, you don't have the ability to edit it. For example, a ready-made class library packaged in a JAR. You can read the code of the classes, but you cannot change it. Suppose that the programmer who created one of the classes in this library (let it be our old Cat class), failing to get enough sleep on the night before the design was finalized, removed the getter and setter for the age field. Now this class has come to you. It meets all your needs, since you just need Cat objects in your program. But you need them to have an age field! This is a problem: we can't reach the field, because it has the private modifier, and the getter and setter were deleted by the sleep-deprived developer who created the class :/ Well, reflection can help us in this situation! We have access to the code for the Cat class, so we can at least find out what fields it has and what they are called. Armed with this information, we can solve our problem:

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.codegym.Cat");
           cat = (Cat) clazz.newInstance();

           // We got lucky with the name field, since it has a setter
           cat.setName("Fluffy");

           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());
   }
}
As stated in the comments, everything with the name field is straightforward, since the class developers provided a setter. You already know how to create objects from default constructors: we have the newInstance() for this. But we'll have to do some tinkering with the second field. Let's figure out what's going on here :)

Field age = clazz.getDeclaredField("age");
Here, using our Class clazzobject , we access the age field via the getDeclaredField() method. It lets us get the age field as a Field age object. But this isn't enough, because we cannot simply assign values to private fields. To do this, we need to make the field accessible by using the setAccessible() method:

age.setAccessible(true);
Once we do this to a field, we can assign a value:

age.set(cat, 6);
As you can see, our Field age object has a kind of inside-out setter to which we pass an int value and the object whose field is to be assigned. We run our main() method and see:

Cat{name='Fluffy', age=6}
Excellent! We did it! :) Let's see what else we can do...

How to call an instance method by name.

Let's slightly change the situation in the previous example. Let's say that the Cat class developer didn't make a mistake with the getters and setters. Everything is okay in that regard. Now the problem is different: there is a method that we definitely need, but the developer made it private:

private void sayMeow() {

   System.out.println("Meow!");
}
This means that if we create Cat objects in our program, then we won't be able to call the sayMeow() method on them. We'll have cats that don't meow? That's odd :/ How would we fix this? Once again, the Reflection API helps us! We know the name of the method we need. Everything else is a technicality:

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("Fluffy", 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();
   }
}
Here we do much of the same thing that we did when accessing a private field. First, we get the method we need. It's encapsulated in a Method object:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
The getDeclaredMethod() method lets us get to private methods. Next, we make the method callable:

sayMeow.setAccessible(true);
And finally, we call the method on the desired object:

sayMeow.invoke(cat);
Here, our method call looks like a "callback": we're accustomed to using a period to pointing an object at the desired method (cat.sayMeow()), but when working with reflection, we pass to the method the object on which we want to call that method. What's on our console?

Meow!
Everything worked! :) Now you can see the vast possibilities that Java's reflection mechanism gives us. In difficult and unexpected situations (such as our examples with a class from a closed library), it can really help us a lot. But, as with any great power, it brings great responsibility. The disadvantages of reflection are described in a special section on the Oracle website. There are three main disadvantages:
  1. Performance is worse. Methods called using reflection have worse performance than methods called in the normal way.

  2. There are security restrictions. The reflection mechanism lets us change a program's behavior at runtime. But at your workplace, when working on a real project, you may face limitations that do not allow this.

  3. Risk of exposure of internal information. It is important to understand reflection is a direct violation of the principle of encapsulation: it lets us access private fields, methods, etc. I don't think I need to mention that a direct and flagrant violation of the principles of OOP should be resorted to only in the most extreme cases, when there are no other ways to solve a problem for reasons beyond your control.

Use reflection wisely and only in situations where it cannot be avoided, and don't forget about its shortcomings. With this, our lesson has come to an end. It turned out to be pretty long, but you learned a lot today :)
Comments (4)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
Deng Xin Level 25, Shanghai, China
1 June 2021
That's an impressive class!
Seb Level 41, Crefeld, Germany
5 February 2021
Excellent lesson! Thanks heaps. :-)
insert mario Level 41, Chiajna, Romania
19 December 2020
maybe the best lesson.
Ifeanyi Level 22, Lagos, Nigeria
3 June 2020
So so interesting!