javalambdajava-9methodhandlelambda-metafactory

LambdaMetaFactory with concrete implementation of generic type


I am trying to use Java's LambdaMetaFactory to dynamically implement a generic lambda, Handler<RoutingContext>:

public class RoutingContext {
    // ...
}

@FunctionalInterface
public interface Handler<X> {
    public void handle(X arg);
}

public class HomeHandler extends Handler<RoutingContext> {
    @Override
    public void handle(RoutingContext ctx) {
        // ...
    }
}

Here is my attempt at LambdaMetaFactory:

try {
    Class<?> homeHandlerClass = HomeHandler.class;

    Method method = homeHandlerClass.getDeclaredMethod(
            "handle", RoutingContext.class);
    Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.unreflect(method);

    MethodType factoryMethodType = MethodType.methodType(Handler.class);
    MethodType functionMethodType = mh.type();
    MethodHandle implementationMethodHandle = mh;

    Handler<RoutingContext> lambda =
            (Handler<RoutingContext>) LambdaMetafactory.metafactory(
                    lookup,
                    "handle",
                    factoryMethodType, 
                    functionMethodType,
                    implementationMethodHandle,
                    implementationMethodHandle.type()) 
            .getTarget()
            .invokeExact();

    lambda.handle(ctx);

} catch (Throwable e) {
    e.printStackTrace();
}

This gives the error:

java.lang.AbstractMethodError: Receiver class [...]$$Lambda$82/0x00000008001fa840
does not define or inherit an implementation of the resolved method abstract
handle(Ljava/lang/Object;)V of interface io.vertx.core.Handler.

I have tried a range of other options for functionMethodType and implementationMethodHandle, but have not managed to get this working yet. Also, even if I replace the RoutingContext.class reference with Object.class, this does not fix the error.

The only way I can get the lambda.handle(ctx) call to succeed is by changing HomeHandler so that it does not extend Handler, making HomeHandler::handle static, and changing RoutingContext.class to Object.class. Oddly I can still cast the resulting lambda to Handler<RoutingContext>, even though it no longer extends Handler.

My questions:

  1. How do I get LambdaMetaFactory to work with non-static methods?

  2. For this non-static SAM class HomeHandler, how does this work with instance allocation under the hood? Does LambdaMetaFactory create a single instance of the interface implementation, no matter how many method calls, since in this example there are no captured variables? Or does it create a new instance for each method call? Or was I supposed to create a single instance and pass it in to the API somehow?

  3. How do I get LambdaMetaFactory to work with generic methods?

Edit: in addition to the great answers below, I came across this blog post explaining the mechanisms involved:

https://medium.freecodecamp.org/a-faster-alternative-to-java-reflection-db6b1e48c33e


Solution

  • Or was I supposed to create a single instance and pass it in to the API somehow?

    Yes. HomeHandler::handle is an instance method, that means you need an instance to create a functional interface wrapper, or pass an instance every time you invoke it (for which Handler won't work as a FunctionalInterface type).

    To use a captured instance you should:

    -

    Class<?> homeHandlerClass = HomeHandler.class;
    
    Method method = homeHandlerClass.getDeclaredMethod(
            "handle", RoutingContext.class);
    Lookup lookup = MethodHandles.lookup();
    MethodHandle mh = lookup.unreflect(method);
    
    MethodType factoryMethodType = MethodType.methodType(Handler.class, HomeHandler.class);
    MethodType functionMethodType = MethodType.methodType(void.class, Object.class);
    MethodHandle implementationMethodHandle = mh;
    
    Handler<RoutingContext> lambda =
            (Handler<RoutingContext>) LambdaMetafactory.metafactory(
                    lookup,
                    "handle",
                    factoryMethodType, 
                    functionMethodType,
                    implementationMethodHandle,
                    implementationMethodHandle.type().dropParameterTypes(0, 1)) 
            .getTarget()
            .invokeExact(new HomeHandler()); // capturing instance
    lambda.handle(ctx);
    

    Of course, since HomeHandler implements Handler, you could just use the captured instance directly;

    new HomeHandler().handle(ctx);
    

    Or leverage the compiler to generate the metafactory code, which also uses invokedynamic, meaning that the CallSite returned by LambdaMetafactory.metafactory will only be created once:

    Handler<RoutingContext> lambda = new HomeHandler()::handle;
    lambda.handle(ctx);
    

    Or, if the functional interface type is statically know:

    MethodHandle theHandle = ...
    Object theInstance = ...
    MethodHandle adapted = theHandle.bindTo(theInstance);
    Handler<RoutingContext> lambda = ctxt -> {
        try {
            adapted.invokeExact(ctxt);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    };
    lambda.handle(new RoutingContext());