PolarSPARC |
Quick Primer on Java Method Handles
Bhaskar S | 03/07/2025 |
Overview
Java Reflection is a very flexible and powerful mechanism that provides one with an ability to dynamically retrieve information about the properties of classes (variables and methods) by name and manipulate or modify their behavior at runtime.
Method Handles were first introduced in Java 7 and are a typesafe, directly invocable references to an underlying method or field of any Java object at runtime. In other words, they are the newer, more efficient type of Java Reflection technique for introspecting and manipulating Java objects at runtime.
Setup
The setup will be on a Ubuntu 24.04 LTS based Linux desktop. Ensure at least Java 17 or above is installed and setup. Also, ensure Apache Maven is installed and setup.
To setup the Java directory structure for the demonstrations in this article, execute the following commands:
$ cd $HOME
$ mkdir -p $HOME/java/MethodHandles
$ cd $HOME/java/MethodHandles
$ mkdir -p src/main/java src/main/resources target
$ mkdir -p src/test/java src/test/resources
$ mkdir -p src/main/java/com/polarsparc/methodhandles
$ mkdir -p src/test/java/com/polarsparc/methodhandles/test
The following is the listing for the Maven project file pom.xml that will be used:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.polarsparc.scripting</groupId> <artifactId>Scripting</artifactId> <version>1.0</version> <properties> <java.version>23</java.version> <slf4j.version>2.0.16</slf4j.version> <junit5.version>5.12.0</junit5.version> <maven.compiler.version>3.13.0</maven.compiler.version> <maven.surefire.version>3.5.2</maven.surefire.version> </properties> <build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven.compiler.version}</version> <configuration> <fork>true</fork> <meminitial>128m</meminitial> <maxmem>512m</maxmem> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>${maven.surefire.version}</version> </plugin> </plugins> </pluginManagement> </build> <dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit5.version}</version> </dependency> </dependencies> </project>
The following is the listing for the slf4j-simple logger properties file simplelogger.properties located in the directory src/main/resources and the directory src/test/resources:
# ### SLF4J Simple Logger properties # org.slf4j.simpleLogger.defaultLogLevel=info org.slf4j.simpleLogger.showDateTime=true org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS org.slf4j.simpleLogger.showThreadName=true
Without much further delay, let us jump right into a simple example to illustrate this powerful capability of method handles support in Java.
Hands-on Java Method Handles
The following is a simple "Hello World" style Java class that will be used for testing Java method handles at run-time:
/* * Name: HelloWorld * Author: Bhaskar S * Date: 03/01/2025 * Blog: https://www.polarsparc.com */ package com.polarsparc.methodhandles; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HelloWorld { private static final Logger LOGGER = LoggerFactory.getLogger(HelloWorld.class); private static String PREFIX = "Hello "; private String greet; public HelloWorld(String greet) { this.greet = PREFIX + greet; } private void logGetHello() { LOGGER.info("invoked: HelloWorld.logGetHello() ..."); } public String getGreet() { logGetHello(); return greet; } public static String holaGreetings() { return "Hola World"; } }
It is a simple Java class with a private static variable, a private member variable, a private method, a static method, and a public method.
The following is our first simplest Java class that leverages the most important method handle API:
/* * Name: HwLookupTest * Author: Bhaskar S * Date: 03/01/2025 * Blog: https://www.polarsparc.com */ package com.polarsparc.methodhandles.test; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; public class HwLookupTest { private static final Logger LOGGER = LoggerFactory.getLogger(HwLookupTest.class); @Test public void testHwLookup() { MethodHandles.Lookup lookup = MethodHandles.lookup(); LOGGER.info("This class is: {}", lookup.lookupClass()); } }
The core APIs for the Java method handle are defined in the package java.lang.invoke.
To get a handle for any field or any method from a Java class, we need a lookup context. The idea of a lookup context is to encapsulate the knowledge of which fields or methods can be invoked at the point where the lookup object is created. To get the lookup context, invoke the static helper method MethodHandles.lookup().
The lookup object will only return fields and methods that are accessible to the context where the lookup was created. They are access-checked at the point where the lookup context is created. The lookup object will not return handles to any fields or methods to which it does not have proper access.
The method lookupClass() on the lookup object returns the class where the lookup is invoked.
To execute the code from Listing.2, open a terminal window and run the following commands:
$ cd $HOME/java/MethodHandles
$ mvn test -Dtest="com.polarsparc.methodhandles.test.HwLookupTest"
The following would be the typical output:
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.polarsparc.methodhandles.test.HwLookupTest 2025-03-07 20:18:08:365 [main] INFO com.polarsparc.methodhandles.test.HwLookupTest - This class is: class com.polarsparc.methodhandles.test.HwLookupTest [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.047 s -- in com.polarsparc.methodhandles.test.HwLookupTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.541 s [INFO] Finished at: 2025-03-07T20:18:08-05:00 [INFO] ------------------------------------------------------------------------
Shifting gears into how one can access the Java class field(s).
The following is the Java class that leverages the commonly used Java method handle APIs to introspect and manipulate the field(s) from the Java class from Listing.1 above:
/* * Name: HwVarHandleTest * Author: Bhaskar S * Date: 03/01/2025 * Blog: https://www.polarsparc.com */ package com.polarsparc.methodhandles.test; import com.polarsparc.methodhandles.HelloWorld; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; public class HwVarHandleTest { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Test public void testHwVarHandle() { HelloWorld helloWorld = new HelloWorld("World"); MethodHandles.Lookup lookup = MethodHandles.lookup(); try { // This will generate a java.lang.IllegalAccessException: member is private VarHandle vh = lookup.findVarHandle(HelloWorld.class, "greet", String.class); } catch (NoSuchFieldException | IllegalAccessException ex) { LOGGER.error(ex.getMessage()); } try { MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(HelloWorld.class, lookup); VarHandle vh = privateLookup.findVarHandle(HelloWorld.class, "greet", String.class); LOGGER.info("Value of variable 'greet': {}", vh.get(helloWorld)); vh.set(helloWorld, "Super World"); LOGGER.info("Value of variable 'greet': {}", vh.get(helloWorld)); } catch (NoSuchFieldException | IllegalAccessException ex) { LOGGER.error(ex.getMessage()); } try { MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(HelloWorld.class, lookup); VarHandle vh = privateLookup.findStaticVarHandle(HelloWorld.class, "PREFIX", String.class); LOGGER.info("Value of static variable 'PREFIX': {}", vh.get()); vh.set("Namaste "); LOGGER.info("Value of static variable 'PREFIX': {}", vh.get()); } catch (NoSuchFieldException | IllegalAccessException ex) { LOGGER.error(ex.getMessage()); } } }
A VarHandle is a dynamic strongly typed reference to a field variable, including static fields, non-static fields, array elements, or components of an off-heap data structure.
The default lookup method MethodHandles.lookup() only provides access to public class members and methods. In order to access the private class fields or methods, one needs to invoke the static helper method MethodHandles.privateLookupIn().
To get a reference to a member field of a Java class, invoke the findVarHandle() method on the lookup object by providing the class that holds the field, the name of the field, and the class type of the field.
Also, to get a reference to a static field of a Java class, invoke the findStaticVarHandle() method on the lookup object by providing the class that holds the field, the name of the field, and the class type of the field.
The get() method on an instance of VarHandle returns the value of the referenced field variable. On the other hand, the set(Object... args) method modifies the current value of the referenced field variable with the provided new value.
To execute the code from Listing.3, open a terminal window and run the following commands:
$ cd $HOME/java/MethodHandles
$ mvn test -Dtest="com.polarsparc.methodhandles.test.HwVarHandleTest"
The following would be the typical output:
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.polarsparc.methodhandles.test.HwVarHandleTest 2025-03-07 20:18:58:582 [main] ERROR com.polarsparc.methodhandles.test.HwVarHandleTest - member is private: com.polarsparc.methodhandles.HelloWorld.greet/java.lang.String/getField, from class com.polarsparc.methodhandles.test.HwVarHandleTest (unnamed module @27f723) 2025-03-07 20:18:58:582 [main] INFO com.polarsparc.methodhandles.test.HwVarHandleTest - Value of variable 'greet': Hello World 2025-03-07 20:18:58:583 [main] INFO com.polarsparc.methodhandles.test.HwVarHandleTest - Value of variable 'greet': Super World 2025-03-07 20:18:58:583 [main] INFO com.polarsparc.methodhandles.test.HwVarHandleTest - Value of static variable 'PREFIX': Hello 2025-03-07 20:18:58:583 [main] INFO com.polarsparc.methodhandles.test.HwVarHandleTest - Value of static variable 'PREFIX': Namaste [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.049 s -- in com.polarsparc.methodhandles.test.HwVarHandleTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.715 s [INFO] Finished at: 2025-03-07T20:18:58-05:00 [INFO] ------------------------------------------------------------------------
Moving along on how one can access the Java class method(s).
The following is the Java class that leverages the commonly used Java method handle APIs to introspect and manipulate the method(s) from the Java class from Listing.1 above:
/* * Name: HwMethodHandleTest * Author: Bhaskar S * Date: 03/01/2025 * Blog: https://www.polarsparc.com */ package com.polarsparc.methodhandles.test; import com.polarsparc.methodhandles.HelloWorld; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Method; public class HwMethodHandleTest { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Test public void testHwMethodHandle() { HelloWorld helloWorld = new HelloWorld("World"); MethodHandles.Lookup lookup = MethodHandles.lookup(); try { MethodType stringNoArg = MethodType.methodType(String.class); LOGGER.info("stringNoArg return type: {}", stringNoArg); MethodHandle mh = lookup.findVirtual(HelloWorld.class, "getGreet", stringNoArg); LOGGER.info("Invoke 'getHello': {}", mh.invoke(helloWorld)); } catch (Throwable ex) { LOGGER.error(ex.getMessage()); } try { MethodType voidNoArg = MethodType.methodType(void.class); // This will generate java.lang.IllegalAccessError: tried to access private method MethodHandle mh = lookup.findVirtual(HelloWorld.class, "logGetHello", voidNoArg); mh.invoke(helloWorld); } catch (Throwable ex) { LOGGER.error(ex.getMessage()); } try { Class<?>[] noArg = {}; Method method = HelloWorld.class.getDeclaredMethod("logGetHello", noArg); method.setAccessible(true); // To gain access to private method MethodHandle mh = lookup.unreflect(method); mh.invoke(helloWorld); } catch (Throwable ex) { LOGGER.error(ex.getMessage()); } try { MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn(HelloWorld.class, lookup); MethodType voidNoArg = MethodType.methodType(void.class); MethodHandle mh = privateLookup.findVirtual(HelloWorld.class, "logGetHello", voidNoArg); mh.invoke(helloWorld); } catch (Throwable ex) { LOGGER.error(ex.getMessage()); } try { MethodType stringNoArg = MethodType.methodType(String.class); MethodHandle smh = lookup.findStatic(HelloWorld.class, "holaGreetings", stringNoArg); LOGGER.info("Invoke 'holaGreetings': {}", smh.invoke()); } catch (Throwable ex) { LOGGER.error(ex.getMessage()); } } }
A MethodType is an immutable, typesafe way to represent the type signature of a method. The first argument is the return type of the method, followed by the types of the method arguments in positional order.
To get a MethodHandle reference to a method from a Java class, one needs to invoke the findVirtual() method for a class method or the findStatic() method for the static method, by providing the class that holds the method, the name of the method, and a MethodType representing the appropriate signature.
The method unreflect() on the method inferred via Java reflection creates a direct method handle using the lookup context.
To method invoke(Object... args) on a method handle instance will call the referenced method after transforming call arguments if needed, such as, performing an asType() conversions if necessary (i.e., boxing or unboxing of arguments as required).
Note that the access checking for method handle invocations is only done once when the method handle is created. In other words, access control for a method handle is checked when the method is found, not when the handle is executed.
To execute the code from Listing.4, open a terminal window and run the following commands:
$ cd $HOME/java/MethodHandles
$ mvn test -Dtest="com.polarsparc.methodhandles.test.HwMethodHandleTest"
The following would be the typical output:
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.polarsparc.methodhandles.test.HwMethodHandleTest 2025-03-07 20:22:02:940 [main] INFO com.polarsparc.methodhandles.test.HwMethodHandleTest - stringNoArg return type: ()String 2025-03-07 20:22:02:941 [main] INFO com.polarsparc.methodhandles.HelloWorld - invoked: HelloWorld.logGetHello() ... 2025-03-07 20:22:02:941 [main] INFO com.polarsparc.methodhandles.test.HwMethodHandleTest - Invoke 'getHello': Hello World 2025-03-07 20:22:02:941 [main] ERROR com.polarsparc.methodhandles.test.HwMethodHandleTest - no such method: com.polarsparc.methodhandles.HelloWorld.logGetHello()void/invokeVirtual 2025-03-07 20:22:02:941 [main] INFO com.polarsparc.methodhandles.HelloWorld - invoked: HelloWorld.logGetHello() ... 2025-03-07 20:22:02:941 [main] INFO com.polarsparc.methodhandles.HelloWorld - invoked: HelloWorld.logGetHello() ... 2025-03-07 20:22:02:941 [main] INFO com.polarsparc.methodhandles.test.HwMethodHandleTest - Invoke 'holaGreetings': Hola World [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.047 s -- in com.polarsparc.methodhandles.test.HwMethodHandleTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.512 s [INFO] Finished at: 2025-03-07T20:22:02-05:00 [INFO] ------------------------------------------------------------------------
For the final example, we will make use of the following Java interface:
/* * Name: Transformer * Author: Bhaskar S * Date: 03/01/2025 * Blog: https://www.polarsparc.com */ package com.polarsparc.methodhandles; import com.polarsparc.methodhandles.Transformer; public interface Transformer { String transform(String in); }
The following example demonstrates how one can bind a method from a Java class that implements an interface from the Listing.5 above to a method handle and invoke it:
/* * Name: TransformerTest * Author: Bhaskar S * Date: 03/01/2025 * Blog: https://www.polarsparc.com */ package com.polarsparc.methodhandles.test; import com.polarsparc.methodhandles.Transformer; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class TransformerTest { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @Test public void testTransformer() { MethodHandles.Lookup lookup = MethodHandles.lookup(); try { Transformer xformObj = new Transformer() { public String transform(String in) { if (in != null && in.trim().length() >= 8) { return in.trim().substring(0, 4) + "XxXxX"; } return "NilXxXxX"; } }; MethodType stringStringArg = MethodType.methodType(String.class, String.class); MethodHandle mh = lookup.findVirtual(Transformer.class, "transform", stringStringArg); mh = mh.bindTo(xformObj); LOGGER.info("Transformed: {}", mh.invoke("123987046")); } catch (Throwable ex) { LOGGER.error(ex.getMessage()); } } }
The bindTo() method on a method handle instance binds the method handle to a specific object instance. The method handle will be invoked on the provided object instance when the method handle is invoked.
To execute the code from Listing.6, open a terminal window and run the following commands:
$ cd $HOME/java/MethodHandles
$ mvn test -Dtest="com.polarsparc.methodhandles.test.TransformerTest"
The following would be the typical output:
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.polarsparc.methodhandles.test.TransformerTest 2025-03-07 20:59:13:907 [main] INFO com.polarsparc.methodhandles.test.TransformerTest - Transformed: 1239XxXxX [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.046 s -- in com.polarsparc.methodhandles.test.TransformerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.726 s [INFO] Finished at: 2025-03-07T20:59:13-05:00 [INFO] ------------------------------------------------------------------------
With this, we conclude our hands-on demonstrations on the topic of Java Method Handles !!!
References