Java 8: преобразование лямбды в экземпляр метода с включенным закрытием

(Трудно выполнить поиск, потому что все результаты связаны с "ссылкой на метод" )

Я хочу получить экземпляр Method для выражения лямбда для использования с устаревшим API на основе отражения. Клавиатура должна быть включена, поэтому вызов thatMethod.invoke(null, ...) должен иметь тот же эффект, что и вызов лямбда.

Я просмотрел MethodHandles.Lookup, но он кажется, что он имеет отношение к обратному преобразованию. Но я полагаю, что метод bind может помочь включить clousure?

Edit:

Скажем, у меня есть lambda experssion:

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

и у меня есть устаревшая структура (SpEL), которая имеет API как

registerFunction(String name, Method method)

который будет вызывать данный Method без аргумента this (т.е. метод считается статическим). Поэтому мне нужно получить специальный экземпляр Method, который включает в себя лямбда-логику + данные clousure.

Ответ 1

Если вы не найдете элегантный способ, вот уродливый способ (Ideone). Обычное предупреждение при отражении: может нарушаться в будущих выпусках и т.д.

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);
  }
}

Ответ 2

Ну, лямбда-выражения выделяются в методы во время компиляции и до тех пор, пока они не захватывают this (не имеют доступа к элементам не static), эти методы будут static. Сложная часть состоит в том, чтобы перейти к этим методам, поскольку нет никакого контролируемого соединения между экземпляром функционального интерфейса и его целевым методом.

Чтобы проиллюстрировать это, здесь простейший случай:

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;
            }
    }
}

Это работает гладко, поскольку существует только одно лямбда-выражение и, следовательно, один метод кандидата. Имя этого метода является специфичным для компилятора и может содержать некоторые серийные номера или хэш-коды и т.д.

В kludge необходимо сделать сериализуемое лямбда-выражение и проверить его сериализованную форму:

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)));
}

Это работает, однако, сериализуемые лямбды приходят по высокой цене.


Простейшим решением было бы добавить аннотацию к параметру лямбда-выражения, которое будет найдено при итерации по методам, однако в настоящее время javac не сохраняет аннотацию должным образом, см. также этот вопрос об этой теме.


Но вы можете также рассмотреть возможность создания обычных методов static, содержащих код вместо выражения лямбда. Получение объекта Method для метода выполняется прямолинейно, и вы все же можете создать экземпляр функционального интерфейса из них, используя ссылки на методы...