Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8: convert lambda to a Method instance with closure included

(This is difficult to search because results are all about "method reference")

I want to get a Method instance for a lambda expression for use with a legacy reflection-based API. The clousure should be included, so calling thatMethod.invoke(null, ...) should have the same effect as calling the lambda.

I have looked at MethodHandles.Lookup, but it only seems to be relevant for the reverse transform. But I guess the bind method may help to include the clousure?

Edit:

Say I have am lambda experssion:

Function<String, String> sayHello = name -> "Hello, " + name;

and I have a legacy framework (SpEL) that has an API like

registerFunction(String name, Method method)

which will call the given Method with no this argument (i.e. Method assumed to be static). So I'll need to get a special Method instance that includes the lambda logic + the clousure data.

like image 781
billc.cn Avatar asked Jan 21 '16 13:01

billc.cn


People also ask

How do you convert lambda expression to method?

To make the code clearer, you can turn that lambda expression into a method reference: Consumer<String> c = System. out::println; In a method reference, you place the object (or class) that contains the method before the :: operator and the name of the method after it without arguments.

Can we replace lambda expression with method reference?

out::println); The method references can only be used to replace a single method of the lambda expression. A code is more clear and short if one uses a lambda expression rather than using an anonymous class and one can use method reference rather than using a single function lambda expression to achieve the same.

How do you pass a lambda expression as an argument?

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.

What happens with lambda after compile time?

Lambda syntax that is written by the developer is desugared into JVM level instructions generated during compilation, which means the actual responsibility of constructing lambda is deferred to runtime.


2 Answers

In case you don't find an elegant way, here is the ugly way (Ideone). Usual warning when reflection is involved: may break in future releases etc.

public static void main(String[] args) throws Exception {
  Function<String, String> sayHello = name -> "Hello, " + name;
  Method m = getMethodFromLambda(sayHello);
  registerFunction("World", m);
}

static void registerFunction(String name, Method method) throws Exception {
  String result = (String) method.invoke(null, name);
  System.out.println("result = " + result);
}

private static Method getMethodFromLambda(Function<String, String> lambda) throws Exception {
  Constructor<?> c = Method.class.getDeclaredConstructors()[0];
  c.setAccessible(true);
  Method m = (Method) c.newInstance(null, null, null, null, null, 0, 0, null, null, null, null);
  m.setAccessible(true); //sets override field to true

  //m.methodAccessor = new LambdaAccessor(...)
  Field ma = Method.class.getDeclaredField("methodAccessor");
  ma.setAccessible(true);
  ma.set(m, new LambdaAccessor(array -> lambda.apply((String) array[0])));

  return m;
}

static class LambdaAccessor implements MethodAccessor {
  private final Function<Object[], Object> lambda;
  public LambdaAccessor(Function<Object[], Object> lambda) {
    this.lambda = lambda;
  }

  @Override public Object invoke(Object o, Object[] os) {
    return lambda.apply(os);
  }
}
like image 72
assylias Avatar answered Sep 30 '22 07:09

assylias


Well, lambda expressions are desugared into methods during compilation and as long as they don’t capture this (don’t access non-static members), these methods will be static. The tricky part is to get to these methods as there is no inspectable connection between the functional interface instance and its target method.

To illustrate this, here the simplest case:

public class LambdaToMethod {
    public static void legacyCaller(Object arg, Method m) {
        System.out.println("calling Method \""+m.getName()+"\" reflectively");
        try {
            m.invoke(null, arg);
        } catch(ReflectiveOperationException ex) {
            ex.printStackTrace();
        }
    }
    public static void main(String[] args) throws URISyntaxException
    {
        Consumer<String> consumer=s -> System.out.println("lambda called with "+s);
        for(Method m: LambdaToMethod.class.getDeclaredMethods())
            if(m.isSynthetic() && m.getName().contains("lambda")) {
                legacyCaller("a string", m);
                break;
            }
    }
}

This works smoothly as there is only one lambda expression and hence, one candidate method. The name of that method is compiler specific and may contain some serial numbers or hash codes, etc.

On kludge is to make the lambda expression serializable and inspect its serialized form:

static Method lambdaToMethod(Serializable lambda) {
    for(Class<?> cl=lambda.getClass(); cl!=null; cl=cl.getSuperclass()) try {
        Method m=cl.getDeclaredMethod("writeReplace");
        m.setAccessible(true);
        try {
            SerializedLambda sl=(SerializedLambda)m.invoke(lambda);
            return LambdaToMethod.class.getDeclaredMethod(sl.getImplMethodName(),
                MethodType.fromMethodDescriptorString(sl.getImplMethodSignature(),
                    LambdaToMethod.class.getClassLoader()).parameterArray());
        } catch(ReflectiveOperationException ex) {
            throw new RuntimeException(ex);
        }
    } catch(NoSuchMethodException ex){}
    throw new AssertionError();
}
public static void main(String[] args)
{
    legacyCaller("a string", lambdaToMethod((Consumer<String>&Serializable)
        s -> System.out.println("first lambda called with "+s)));
    legacyCaller("a string", lambdaToMethod((Consumer<String>&Serializable)
        s -> System.out.println("second lambda called with "+s)));
}

This works, however, serializable lambdas come at a high price.


The simplest solution would be to add an annotation to a parameter of the lambda expression to be found when iterating over the methods, however, currently, javac doesn’t store the annotation properly, see also this question about this topic.


But you may also consider just creating ordinary static methods holding the code instead of a lambda expression. Getting a Method object for a method is straight-forward and you still can create a functional interface instance out of them using method references…

like image 20
Holger Avatar answered Sep 30 '22 05:09

Holger