22

We have this functional interface:

public interface Consumer<T> {

    void accept(T t);

}

And I can use it like this:

.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 clear:

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 parameters 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

user207421
  • 305,947
  • 44
  • 307
  • 483
Artem Bilan
  • 113,505
  • 11
  • 91
  • 118
  • Not sure I understand; in your example you can do without `Integer`, the compiler does type inference here. – fge May 26 '14 at 06:11
  • No. I really need to know that type: we have some `converter` subsystem which can convert `"2"` to `int` (the simplest sample), if it can determine the type of parameter. Otherwise we end up with `java.lang.String cannot be cast to java.lang.Integer`. Having that Java somehow understands the actual type. But how can I do that from code? – Artem Bilan May 26 '14 at 06:16
  • No op - `java.lang.Object` – Artem Bilan May 26 '14 at 06:21
  • What? Then you passed an object of type `java.lang.Object` as an argument. – Sotirios Delimanolis May 26 '14 at 06:24
  • I still don't understand where you need the information and what that information is. – Sotirios Delimanolis May 26 '14 at 06:24
  • I'm really confused here; do you want to prevent the passing of anything other than a `Number` of some kind to your `Consumer`? Or do you want to guard against someone passing in a parsable `String` to the consumer? – Makoto May 26 '14 at 06:26
  • We wrap that inline class (or lambda) to some `MethodInvoker` with `converter` ability and call the method using reflection. That's why I need to know actual types to do conversion for provided object before method call – Artem Bilan May 26 '14 at 06:27
  • 1
    One more time: I want to know **actual generic type of method argument**. I can do that for classes, but can't for lambdas. That's all. Don't try to understand the logic of my application - it is Spring and it has powerfull `Converter` sub-system. E.g. I can write and register some `UserToPersonConverter` and just send the `User` to my consumer and it will be converted to the `Person` method parameter. It works with inline classes, but doesn't with lambdas – Artem Bilan May 26 '14 at 06:32
  • What is the method `handle` above? You want to know the generic type of the argument used for the `accept` method invocation? – Sotirios Delimanolis May 26 '14 at 06:39
  • Yes, the type of argument of the `accept` method – Artem Bilan May 26 '14 at 06:48
  • 2
    And what is being invoked through reflection? Please edit your question with all this relevant information. – Sotirios Delimanolis May 26 '14 at 06:52
  • Added more info to the question – Artem Bilan May 26 '14 at 07:03
  • possible duplicate of [Reflection type inference on Java 8 Lambdas](http://stackoverflow.com/questions/21887358/reflection-type-inference-on-java-8-lambdas) – Holger May 27 '14 at 08:11
  • Artem, did you use this functionality in some open source project? If so could you point me to it? Basically I need it to back up my decision and say: "This solution is not 100% portable but hey, X uses it in the following way". Also I would like to know whether you have any asserts that could determine in runtime if the current platform doesn't support it. – wheleph Jul 24 '15 at 13:43
  • 1
    You can find it in my project https://github.com/spring-projects/spring-integration-java-dsl/. Where generics resolution is done via an additional `Class` argument: https://github.com/spring-projects/spring-integration-java-dsl/blob/master/src/main/java/org/springframework/integration/dsl/IntegrationFlowDefinition.java#L1018 and an internal solution `LambdaMessageProcessor` does the stuff for us. Nothing third-party, BTW: just Java as is in its current state... – Artem Bilan Jul 24 '15 at 14:07

2 Answers2

11

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
  • 2
    I've accepted this answer as it is enough smart and tricky. Anyway we decided to overcome the issue with additional param - see my linked question. Thanks – Artem Bilan Sep 03 '14 at 07:04
  • 1
    This example will work for `java.util.function.Function` but not for some other functional interfaces since the member ref is located at different indexes. For Oracle JDK and Open JDK the member ref can be reliably obtained via `constantPool.getMemberRefInfoAt(constantPool.size() - 2)` – Jonathan Dec 08 '14 at 18:21
  • I came across a few more corner cases where this doesn't work as is - involving type arguments resolved from certain method references, and from lambdas/method refs with boxed integers. See [TypeTools](https://github.com/jhalterman/typetools) for the updates. – Jonathan Apr 21 '15 at 04:26
  • @DanielWorthington-Bodart about "Now ideally the classes generated by LambdaMetaFactory should store the generic type signatures", did you follow up on this in OpenJDK? I have one issue where the generated synthetic method does not contain the generic signature, verified by looking at javap output. – Jörn Horstmann Jun 12 '16 at 10:07
  • There are fundamental flaws with the assumption that generic parameter types must be resolvable at runtime. Take for example, `Function f1 = Function.identity(); Function f2 = Function.identity();` (the function returned by `identity()` is implemented as lambda expression, `t -> t`). In that regard, lambda expressions are not different to generic classes. So even in environments where this hack works, it only serves a certain fraction of functions. – Holger Jan 09 '20 at 16:04
1

Use a TypeRef to wrap the lambda expression.

@FunctionalInterface
interface TypeRef<T> {
    T create();

    default Class<?> getGenericType() {
        return create().getClass();
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println(getFactoryTypeParameter(() -> "hello"));
    }

    private static <T> Class<?> getFactoryTypeParameter(TypeRef<T> typeRef) {
        return typeRef.getGenericType();
    }
}

output:

class java.lang.String
Adam Horvath
  • 1,249
  • 1
  • 10
  • 25
byx
  • 11
  • 2