How to get the MethodInfo of a Java 8 method reference?

JavaReflectionLambdaJava 8

Java Problem Overview


Please have a look at the following code:

Method methodInfo = MyClass.class.getMethod("myMethod");

This works, but the method name is passed as a string, so this will compile even if myMethod does not exist.

On the other hand, Java 8 introduces a method reference feature. It is checked at compile time. It is possible to use this feature to get method info?

printMethodName(MyClass::myMethod);

Full example:

@FunctionalInterface
private interface Action {
    
    void invoke();
}

private static class MyClass {
    
    public static void myMethod() {
    }
}

private static void printMethodName(Action action) {
}

public static void main(String[] args) throws NoSuchMethodException {
    // This works, but method name is passed as a string, so this will compile
    // even if myMethod does not exist
    Method methodInfo = MyClass.class.getMethod("myMethod");
    
    // Here we pass reference to a method. It is somehow possible to
    // obtain java.lang.reflect.Method for myMethod inside printMethodName?
    printMethodName(MyClass::myMethod);
}

In other words I would like to have a code which is the equivalent of the following C# code:

    private static class InnerClass
    {
        public static void MyMethod()
        {
            Console.WriteLine("Hello");
        }
    }

    static void PrintMethodName(Action action)
    {
        // Can I get java.lang.reflect.Method in the same way?
        MethodInfo methodInfo = action.GetMethodInfo();
    }

    static void Main()
    {
        PrintMethodName(InnerClass.MyMethod);
    }

Java Solutions


Solution 1 - Java

No, there is no reliable, supported way to do this. You assign a method reference to an instance of a functional interface, but that instance is cooked up by LambdaMetaFactory, and there is no way to drill into it to find the method you originally bound to.

Lambdas and method references in Java work quite differently than delegates in C#. For some interesting background, read up on invokedynamic.

Other answers and comments here show that it may currently be possible to retrieve the bound method with some additional work, but make sure you understand the caveats.

Solution 2 - Java

In my case I was looking for a way to get rid of this in unit tests:

Point p = getAPoint();
assertEquals(p.getX(), 4, "x");
assertEquals(p.getY(), 6, "x");

As you can see someone is testing Method getAPoint and checks that the coordinates are as expected, but in the description of each assert was copied and is not in sync with what is checked. Better would be to write this only once.

From the ideas by @ddan I built a proxy solution using Mockito:

private<T> void assertPropertyEqual(final T object, final Function<T, ?> getter, final Object expected) {
    final String methodName = getMethodName(object.getClass(), getter);
    assertEquals(getter.apply(object), expected, methodName);
}

@SuppressWarnings("unchecked")
private<T> String getMethodName(final Class<?> clazz, final Function<T, ?> getter) {
    final Method[] method = new Method[1];
    getter.apply((T)Mockito.mock(clazz, Mockito.withSettings().invocationListeners(methodInvocationReport -> {
        method[0] = ((InvocationOnMock) methodInvocationReport.getInvocation()).getMethod();
    })));
    return method[0].getName();
}

No I can simply use

assertPropertyEqual(p, Point::getX, 4);
assertPropertyEqual(p, Point::getY, 6);

and the description of the assert is guaranteed to be in sync with the code.

Downside:

  • Will be slightly slower than above
  • Needs Mockito to work
  • Hardly useful to anything but the usecase above.

However it does show a way how it could be done.

Solution 3 - Java

Though I haven't tried it myself, I think the answer is "no," since a method reference is semantically the same as a lambda.

Solution 4 - Java

You can add safety-mirror to your classpath and do like this:

Method m1 = Types.createMethod(Thread::isAlive)  // Get final method
Method m2 = Types.createMethod(String::isEmpty); // Get method from final class
Method m3 = Types.createMethod(BufferedReader::readLine); // Get method that throws checked exception
Method m4 = Types.<String, Class[]>createMethod(getClass()::getDeclaredMethod); //to get vararg method you must specify parameters in generics
Method m5 = Types.<String>createMethod(Class::forName); // to get overloaded method you must specify parameters in generics
Method m6 = Types.createMethod(this::toString); //Works with inherited methods

The library also offers a getName(...) method:

assertEquals("isEmpty", Types.getName(String::isEmpty));

The library is based on Holger's answer: https://stackoverflow.com/a/21879031/6095334

Edit: The library have various shortcomings which I am slowly becoming aware of. See fx Holger's comment here: https://stackoverflow.com/q/47313521/6095334

Solution 5 - Java

There may not be a reliable way, but under some circumstances:

  1. your MyClass is not final, and has an accessible constructor (limitation of cglib)
  2. your myMethod is not overloaded, and not static

The you can try using cglib to create a proxy of MyClass, then using an MethodInterceptor to report the Method while the method reference is invoked in a following trial run.

Example code:

public static void main(String[] args) {
    Method m = MethodReferenceUtils.getReferencedMethod(ArrayList.class, ArrayList::contains);
    System.out.println(m);
}

You will see the following output:

public boolean java.util.ArrayList.contains(java.lang.Object)

While:

public class MethodReferenceUtils {

	@FunctionalInterface
	public static interface MethodRefWith1Arg<T, A1> {
		void call(T t, A1 a1);
	}

	public static <T, A1> Method getReferencedMethod(Class<T> clazz, MethodRefWith1Arg<T, A1> methodRef) {
		return findReferencedMethod(clazz, t -> methodRef.call(t, null));
	}

	@SuppressWarnings("unchecked")
	private static <T> Method findReferencedMethod(Class<T> clazz, Consumer<T> invoker) {
		AtomicReference<Method> ref = new AtomicReference<>();
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(clazz);
		enhancer.setCallback(new MethodInterceptor() {
			@Override
			public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
				ref.set(method);
				return null;
			}
		});
		try {
			invoker.accept((T) enhancer.create());
		} catch (ClassCastException e) {
			throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
		}
		
		Method method = ref.get();
		if (method == null) {
			throw new IllegalArgumentException(String.format("Invalid method reference on class [%s]", clazz));
		}
		
		return method;
	}
}

In the above code, MethodRefWith1Arg is just a syntax sugar for you to reference an non-static method with one arguments. You can create as many as MethodRefWithXArgs for referencing your other methods.

Solution 6 - Java

If you can make the interface Action extend Serializable, then this answer from another question seems to provide a solution (at least on some compilers and runtimes).

Solution 7 - Java

We have published the small library reflection-util that can be used to capture a method name.

Example:

class MyClass {

    private int value;

	public void myMethod() {
	}

	public int getValue() {
        return value;
	}

}

String methodName = ClassUtils.getMethodName(MyClass.class, MyClass::myMethod);
System.out.println(methodName); // prints "myMethod"

String getterName = ClassUtils.getMethodName(MyClass.class, MyClass::getValue);
System.out.println(getterName); // prints "getValue"

Implementation details: A Proxy subclass of MyClass is created with ByteBuddy and a call to the method is captured to retrieve its name. ClassUtils caches the information such that we do not need to create a new proxy on every invocation.

Please note that this approach is no silver bullet and there are some known cases that don’t work:

  • It doesn’t work for static methods.
  • It doesn’t work if the class is final.
  • We currently do not support all potential method signatures. It should work for methods that do not take an argument such as a getter method.

Solution 8 - Java

You can use my library Reflect Without String

Method myMethod = ReflectWithoutString.methodGetter(MyClass.class).getMethod(MyClass::myMethod);

Solution 9 - Java

So, I play with this code

import sun.reflect.ConstantPool;

import java.lang.reflect.Method;
import java.util.function.Consumer;

public class Main {
    private Consumer<String> consumer;

    Main() {
        consumer = this::test;
    }

    public void test(String val) {
        System.out.println("val = " + val);
    }

    public void run() throws Exception {
        ConstantPool oa = sun.misc.SharedSecrets.getJavaLangAccess().getConstantPool(consumer.getClass());
        for (int i = 0; i < oa.getSize(); i++) {
            try {
                Object v = oa.getMethodAt(i);
                if (v instanceof Method) {
                    System.out.println("index = " + i + ", method = " + v);
                }
            } catch (Exception e) {
            }
        }
    }

    public static void main(String[] args) throws Exception {
        new Main().run();
    }
}

output of this code is:

index = 30, method = public void Main.test(java.lang.String)

And as I notice index of referenced method is always 30. Final code may look like

public Method unreference(Object methodRef) {
        ConstantPool constantPool = sun.misc.SharedSecrets.getJavaLangAccess().getConstantPool(methodRef.getClass());
        try {
            Object method = constantPool.getMethodAt(30);
            if (method instanceof Method) {
                return (Method) method;
            }
        }catch (Exception ignored) {
        }
        throw new IllegalArgumentException("Not a method reference.");
    }

Be careful with this code in production!

Solution 10 - Java

Try this

Thread.currentThread().getStackTrace()[2].getMethodName();

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRafalView Question on Stackoverflow
Solution 1 - JavaMike StrobelView Answer on Stackoverflow
Solution 2 - JavayankeeView Answer on Stackoverflow
Solution 3 - JavaMatt BallView Answer on Stackoverflow
Solution 4 - JavaHervianView Answer on Stackoverflow
Solution 5 - JavavivimiceView Answer on Stackoverflow
Solution 6 - Javauser102008View Answer on Stackoverflow
Solution 7 - JavaBenedikt WaldvogelView Answer on Stackoverflow
Solution 8 - JavaDean XuView Answer on Stackoverflow
Solution 9 - JavaDmitriy V.View Answer on Stackoverflow
Solution 10 - JavaAsghar NematiView Answer on Stackoverflow