Invoking Methods
A Method
object let you get more information on the corresponding method: its type and its modifiers, the types and names of its parameters, and enables you to invoke it on a given object, passing the arguments you need. This section also covers how you can discover methods in a class.
Locating Methods
There are two categories of methods provided in Class
for accessing methods.
- First, you can look for a specific method. These methods suppose that you have a name for the method you are looking for, and the type of its parameters.
- Second, you can look for all the methods that are declared in this class, or for the methods declared in this class, and all its super classes, up to the
Object
class. In the first case, you will get all the methods: public, protected, default (package) access, and private. In the second case, you will get only the public methods.
The following tables provide a summary of all the member-locating methods and their characteristics.
Class API | Array of methods? | Inherited methods? | Private methods? |
---|---|---|---|
getDeclaredMethod(String, Object...) |
no | no | yes |
getMethod(String, Object...) |
no | yes | no |
getDeclaredMethods() |
yes | no | yes |
getMethods() |
yes | yes | no |
Let us see this in action. Suppose that you have the following class.
public class User {
private final String name;
public User(String name) {
validate(name);
this.name = name;
}
private void validate(String name) {
Objects.requireNonNull(name);
}
public String getName() {
return name;
}
@Override
public String toString() {
return "User[" + "name=" + name + "]";
}
}
First, let us run the following code, that gets all the public methods from the User
class and all of its super classes.
Class<?> c = User.class;
Method[] methods = c.getMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
Running the previous code prints the following. As you can see, there are the two methods from User
: getName()
and toString()
, and the methods from the Object
class, apart from the toString()
method, which is defined in the User
class. If you remove the toString()
method from the User
class, then you will see the toString()
from the Object
class in this list.
method = public java.lang.String org.devjava.User.getName()
method = public java.lang.String org.devjava.User.toString()
method = public boolean java.lang.Object.equals(java.lang.Object)
method = public native int java.lang.Object.hashCode()
method = public final native java.lang.Class java.lang.Object.getClass()
method = public final native void java.lang.Object.notify()
method = public final native void java.lang.Object.notifyAll()
method = public final void java.lang.Object.wait(long) throws java.lang.InterruptedException
method = public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method = public final void java.lang.Object.wait() throws java.lang.InterruptedException
Let us now run the following code, that is calling Class.getDeclaredMethods()
to get all the methods from the User
class.
Class<?> c = User.class;
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) {
System.out.println("method = " + method);
}
Running the previous code prints the following. This time, only the methods declared in the User
class are present, including the private methods.
method = public java.lang.String org.devjava.User.getName()
method = public java.lang.String org.devjava.User.toString()
method = private void org.devjava.User.validate(java.lang.String)
Obtaining the Return Type of a Method
The return type of a method is modeled in the same way as the type of a Field
. The Method.getReturnType()
method gives you the type as a Class
object, and the Method.getGenericReturnType()
returns a Type
object from which you can get information about the generic type.
Let us see these two methods in action on the List
interface.
First, you can get a reference on the method List.get(int)
. This method returns the element of the list with the index you passed as an argument. The return type of this method is the parameter type of the list you declared.
Note that we used the class int.class
to denote the int
type of the parameter that the List.get(index)
method takes. Using the Integer.class
type would have thrown a NoSuchMethodException
.
Class<?> listClass = List.class;
Method getWithIndex = listClass.getMethod("get", int.class);
Class<?> returnType = getWithIndex.getReturnType();
System.out.println("Return type = " + returnType);
Type genericReturnType = getWithIndex.getGenericReturnType();
System.out.println("Generic return type = " + genericReturnType);
Running the previous example gives you the following.
Return type = class java.lang.Object
Generic return type = E
As you can see, the Method.getReturnType()
gives you the raw type that this method returned, whereas the Method.getGenericReturnType()
gives you the information that it is in fact the parameter type of the List
interface.
Let us test it with another method: List.of()
.
Class<?> listClass = List.class;
Method listOf = listClass.getMethod("of", Object.class);
Class<?> returnType = listOf.getReturnType();
System.out.println("Return type = " + returnType);
Type genericReturnType = listOf.getGenericReturnType();
System.out.println("Generic return type = " + genericReturnType);
Running this example gives you the following.
Return type = interface java.util.List
Generic return type = java.util.List<E>
Obtaining the Parameters of a Method
The Method
class gives you access to the parameters of a given method, including their names.
Obtaining the Names of Method Parameters
You can obtain the names of the formal parameters of any method or constructor (that are covered in the next section) with the method getParameters()
. The classes Method
and Constructor
extend the class Executable
and therefore inherit this method. However, .class
files do not store formal parameter names by default. This is because many tools that produce and consume class files may not expect the larger static and dynamic footprint of .class
files that contain parameter names. In particular, these tools would have to handle larger .class
files, and the Java Virtual Machine (JVM) would use more memory. In addition, some parameter names, such as secret
or password
, may expose information about security-sensitive methods.
To store formal parameter names in a particular .class file, and thus enable the Reflection API to retrieve formal parameter names, compile the source file with the -parameters
option to the javac
compiler. You can find more information the javac
compiler in this section.
Obtaining the Return Type of a Method
The return type of a method is modeled in the same way as the type of a Field
. The Method.getReturnType()
method gives you the type as a Class
object, and the Method.getGenericReturnType()
returns a Type
from which you can get information about the generic type.
Obtaining the Exceptions of a Method
The Method
class gives you access to the declared exceptions of a method. These exceptions are returned in an array, which can be of two types: an array of Class
instances, or an array of Type
instances. As usual the Type
object can give you information about a parameter type.
Let us discover the numerous exceptions thrown by the Constructor.newInstance()
method, that is part of the Reflection API. Note how you can denote an array of Object
type, with the Object[].class
syntax.
Class<?> constructorClass = Constructor.class;
Method newInstance = constructorClass.getMethod("newInstance", Object[].class);
var exceptionTypes = newInstance.getExceptionTypes();
System.out.println("exceptionTypes:");
for (var exceptionType : exceptionTypes) {
System.out.println(" exceptionType = " + exceptionType);
}
Running this example produces the following:
Exception types:
Exception type = class java.lang.InstantiationException
Exception type = class java.lang.IllegalAccessException
Exception type = class java.lang.IllegalArgumentException
Exception type = class java.lang.reflect.InvocationTargetException
You can run the same code using the other pattern, that returns an array of Type
objects. Note that in that case, you are calling Method.getGenericExceptionTypes()
.
Class<?> constructorClass = Constructor.class;
Method newInstance = constructorClass.getMethod("newInstance", Object[].class);
var exceptionTypes = newInstance.getGenericExceptionTypes();
System.out.println("Exception types:");
for (var exceptionType : exceptionTypes) {
System.out.println(" Exception type = " + exceptionType);
}
The result printed on the console is the same.
Exception types:
Exception type = class java.lang.InstantiationException
Exception type = class java.lang.IllegalAccessException
Exception type = class java.lang.IllegalArgumentException
Exception type = class java.lang.reflect.InvocationTargetException
Retrieving Method Modifiers
The modifiers of a method work in the same way as the modifiers of a class or a field. The method Method.getModifiers()
returns an int
in which each bit represent a single modifier. For instance, if the bit 0 is set, it means that this method is public. If the bit 3 is set, then this method is static.
You can read the modifiers of the String.join()
method with the following code.
Class<?> stringClass = String.class;
Method join = stringClass.getMethod("join", CharSequence.class, Iterable.class);
int modifiers = join.getModifiers();
System.out.println("is static? " + Modifier.isStatic(modifiers));
System.out.println("is public? " + Modifier.isPublic(modifiers));
This code prints the following on the console.
is static? true
is public? true
You can also use the access flags, covered in this section
Class<?> stringClass = String.class;
Method join = stringClass.getMethod("join", CharSequence.class, Iterable.class);
Set<AccessFlag> accessFlags = join.accessFlags();
System.out.println("Access flags: " + accessFlags);
This code prints the following on the console.
Access flags: [PUBLIC, STATIC]
Invoking a Method
So far you learnt that a method can be modeled by an object instance of the Method
class, and that you can use this object to get information on the method it models, including its name, parameters, return type, and the exceptions this method may declare.
Using this object, you can also invoke this method, passing it arguments and getting its result. If it throws exceptions, you can also catch them and analyze them. This feature of the Method
class is widely used in many frameworks, because it allows you to execute a method from elements that are discoverable.
A method can be invoked using its invoke()
method, that takes two parameters.
- The first one is the object on which you want to invoke the method.
- The second one is actually a vararg, so you can pass any number of arguments with this declaration, even no argument. This vararg receives the parameters you need to pass to the method you are invoking. You can get more information on this variable argument feature on this page.
If the method you are invoking does not take any parameter, then you need to call the invoke()
method with only one argument: the object on which you are invoking this method. On the other hand, if the method you are invoking takes two parameters, then you invocation of the invoke()
method needs three arguments: the object on which you are invoking this method, and the two arguments this method takes.
Getting the Result of an Invoked Method
Let us start with a simple example. Suppose you want to get the length of a string of characters using the Reflection API. The String.length()
does not take any parameter, which make the invocation of this method easier. All you need to do is to call the invoke()
method of the Method
class, passing it the string on which you want to invoke the length()
method.
String hello = "Hello";
Class<?> stringClass = String.class;
Method lengthMethod = stringClass.getMethod("length");
int length = (int)lengthMethod.invoke(hello);
System.out.println("length = " + length);
Note that the getMethod()
method returns a type Object
, meaning that you need to cast the return object to the correct type, if you know it. Running this code prints the following:
length = 5
Passing Parameters to the Invoked Method
The objects and values you need to pass to the method you invoke can be passed to invoke()
as further arguments. In the following example, we are calling the substring()
method, that takes two integers. Note that there are two overloads of the String.substring()
method, one that takes one int
, and the other that takes two. Here we are getting the second overload, by passing the two int.class
arguments.
String hello = "Hello world!";
Class<?> stringClass = String.class;
Method substringMethod = stringClass.getMethod("substring", int.class, int.class);
String substring = (String)substringMethod.invoke(hello, 6, 11);
System.out.println("substring = " + substring);
The previous code prints the following.
substring = world
Getting the Thrown Exceptions
There are cases where the method you invoke using the Reflection API may throw exceptions that you need to be aware of. The Reflection API catches these exceptions for you, and wrap them in a InvocationTargetException
that you can catch and analyze.
Suppose you need to open a file for writing using the Reflection API, but pass a file that cannot be opened. You can do that with the following code.
Note that we are using the Files.newBufferedWriter()
method, that declares a variable argument. To get such a method, you need to consider this variable argument as an array, and pass an empty array of the correct type to the invoke()
call.
Note also that the Files.newBufferedWriter()
is static. So you do not invoke it on any object. In that case, the first argument passed to the invoke()
method is null
.
Path doesNotExist = Path.of("/does/not/exist");
Class<?> filesClass = Files.class;
Method newBufferedWriterMethod =
filesClass.getMethod("newBufferedWriter", Path.class, OpenOption[].class);
try (Writer writer = (Writer)newBufferedWriterMethod.invoke(null, doesNotExist, new OpenOption[0]);) {
writer.write("Hello");
} catch (InvocationTargetException ite) {
Throwable targetException = ite.getTargetException();
System.out.println("ite.getTargetException() = " + targetException);
System.out.println("Stack trace:");
targetException.printStackTrace();
} catch (IOException e) {
throw new RuntimeException(e);
}
The opening and the closing of the writer
(which is done by the use of this try-with-resources statement) throws an IOException
that is caught by the Reflection API, and wrapped in a InvocationTargetException
. The InvocationTargetException
class has a getTargetException()
method that gives you this exception. We are using this pattern in this example. Running it will give you the following.
ite.getTargetException() = java.nio.file.NoSuchFileException: \does\not\exist
Stack trace:
java.nio.file.NoSuchFileException: \does\not\exist
// this part of the stack trace depends on your file system
at java.base/java.nio.file.spi.FileSystemProvider.newOutputStream(FileSystemProvider.java:484)
at java.base/java.nio.file.Files.newOutputStream(Files.java:228)
at java.base/java.nio.file.Files.newBufferedWriter(Files.java:3007)
at java.base/java.nio.file.Files.newBufferedWriter(Files.java:3055)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at java.base/java.lang.reflect.Method.invoke(Method.java:578)
at org.devjava.Main.main(Main.java:48)
Accessing Private Methods
Trying to invoke a private method from outside the class, or any other case of a non-visible method will throw a IllegalAccessException
.
You can still get access to such a method and invoke it, by first calling method.setAccessible(true)
on it. This call may fail in case you are trying to invoke a method from a class in another module, due to the restrictions on the class members a module. You can check the page Reflective Access with Open Modules and Open Packages for more information.
Note that calling setAccessible(true)
on a method does not make this method public. It just suppresses the checks for access control.
Generics and Type Erasure
You need to keep in mind that the discovery of a method depends on how this method has been compiled. The type parameter of a method is erased at compile time, meaning that you cannot use it with the Reflection API.
Consider the following example, where you want to access the add()
method of a List
object.
List<String> strings = new ArrayList<>();
Class<?> stringsClass = strings.getClass();
Method addString = stringsClass.getMethod("add", String.class);
Here you are trying to get a reference on the add()
method of the class of an object of type List<String>
. This method declares a String
parameter. But because this parameter has in fact the type E
(that receives the value String
in this example), it is erased, and has in fact the type Object
.
So running this code produces the following result, because there is no add()
method on this class, that takes a String
as a parameter.
Exception in thread "main" java.lang.NoSuchMethodException: java.util.ArrayList.add(java.lang.String)
at java.base/java.lang.Class.getMethod(Class.java:2277)
at org.devjava.Main.main(Main.java:47)
The correct code is the following, that looks for a method that declares a parameter of type Object
:
List<String> strings = new ArrayList<>();
Class<?> stringsClass = strings.getClass();
Method addString = stringsClass.getMethod("add", Object.class);
You can then invoke this method, passing a string to it. But by doing so you loose the type safety of calling the method directly.
Last update: July 19, 2024