Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java: how to resolve generic type of lambda parameter?

Tags:

Well, we have FunctionalInterface:

public interface Consumer<T> {

    void accept(T t);

}

And I can use it like:

.handle(Integer p -> System.out.println(p * 2));

How can we resolve the actual generic type of that lambda parameter in our code?

When we use it as an inline implementation it isn't so difficult to extract the Integer from the method of that class.

Do I miss anything? Or just java doesn't support it for lambda classes ?

To be more cleaner:

That lambda is wrapped with MethodInvoker (in the mentioned handle), which in its execute(Message<?> message) extracts actual parameters for further reflection method invocation. Before that it converts provided arguments to target params using Spring's ConversionService.

The method handle in this case is some configurer before the real application work.

The different question, but with expectation for the solution for the same issue: Java: get actual type of generic method with lambda parameter

like image 273
Artem Bilan Avatar asked May 26 '14 06:05

Artem Bilan


People also ask

Can lambda expressions be generic?

A lambda expression can't specify type parameters, so it's not generic. However, a functional interface associated with lambda expression is generic.

Is it mandatory to declare type of parameter in lambda expression?

A lambda expression is characterized by the following syntax. Following are the important characteristics of a lambda expression. Optional type declaration − No need to declare the type of a parameter. The compiler can inference the same from the value of the parameter.

How do you provide a generic parameterized type?

In order to use a generic type we must provide one type argument per type parameter that was declared for the generic type. The type argument list is a comma separated list that is delimited by angle brackets and follows the type name. The result is a so-called parameterized type.

How do you pass a lambda function as a parameter?

Passing Lambda Expressions as Arguments You can pass lambda expressions as arguments to a function. If you have to pass a lambda expression as a parameter, the parameter type should be able to hold it. If you pass an integer as an argument to a function, you must have an int or Integer parameter.


2 Answers

I recently added support for resolving lambda type arguments to TypeTools. Ex:

MapFunction<String, Integer> fn = str -> Integer.valueOf(str);
Class<?>[] typeArgs = TypeResolver.resolveRawArguments(MapFunction.class, fn.getClass());

The resolved type args are as expected:

assert typeArgs[0] == String.class;
assert typeArgs[1] == Integer.class;

Note: The underlying implementation uses the ConstantPool approach outlined by @danielbodart which is known to work on Oracle JDK and OpenJDK.

like image 70
Jonathan Avatar answered Oct 16 '22 15:10

Jonathan


This is currently possible to solve but only in a pretty hackie way, but let me first explain a few things:

When you write a lambda, the compiler inserts a dynamic invoke instruction pointing to the LambdaMetafactory and a private static synthetic method with the body of the lambda. The synthetic method and the method handle in the constant pool both contain the generic type (if the lambda uses the type or is explicit as in your examples).

Now at runtime the LambdaMetaFactory is called and a class is generated using ASM that implements the functional interface and the body of the method then calls the private static method with any arguments passed. It is then injected into the original class using Unsafe.defineAnonymousClass (see John Rose post) so it can access the private members etc.

Unfortunately the generated Class does not store the generic signatures (it could) so you can't use the usual reflection methods that allow you to get around erasure

For a normal Class you could inspect the bytecode using Class.getResource(ClassName + ".class") but for anonymous classes defined using Unsafe you are out of luck. However you can make the LambdaMetaFactory dump them out with the JVM argument:

java -Djdk.internal.lambda.dumpProxyClasses=/some/folder

By looking at the dumped class file (using javap -p -s -v), one can see that it does indeed call the static method. But the problem remains how to get the bytecode from within Java itself.

This unfortunately is where it gets hackie:

Using reflection we can call Class.getConstantPool and then access the MethodRefInfo to get the type descriptors. We can then use ASM to parse this and return the argument types. Putting it all together:

Method getConstantPool = Class.class.getDeclaredMethod("getConstantPool");
getConstantPool.setAccessible(true);
ConstantPool constantPool = (ConstantPool) getConstantPool.invoke(lambda.getClass());
String[] methodRefInfo = constantPool.getMemberRefInfoAt(constantPool.size() - 2);

int argumentIndex = 0;
String argumentType = jdk.internal.org.objectweb.asm.Type.getArgumentTypes(methodRef[2])[argumentIndex].getClassName();
Class<?> type = (Class<?>) Class.forName(argumentType);

UPDATED with Jonathan's suggestion

Now ideally the classes generated by LambdaMetaFactory should store the generic type signatures (I might see if I can submit a patch to the OpenJDK) but currently this is the best we can do. The code above has the following problems:

  • It uses undocumented methods and classes
  • It is extremely vulnerable to code changes in the JDK
  • It doesn't preserve the generic types, so if you pass List<String> into a lambda it will come out as List
like image 37
Daniel Worthington-Bodart Avatar answered Oct 16 '22 15:10

Daniel Worthington-Bodart