5

It is difficult to find any clues for the topic. All I could find is questions about converting one functional interface to another and some articles on type casting in Java. Not what I was looking for.

This question is about converting lambda → Method and I want the opposite, to convert Method to any functional interface, for example, to Consumer.

The way I found is to create a lambda adapter around the Method#invoke method:

    public void registerCallbacks(final Object annotated) {
        Class clazz = annotated.getClass();
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Callback.class)) {
                Callback registration = method.getAnnotation(Callback.class);

                List<String> warnings = new ArrayList<>(3);

                if (!Modifier.isPublic(method.getModifiers()))
                    warnings.add(String.format("Method %s must be public", method));
                if (method.getParameterCount() != 1)
                    warnings.add(String.format("Method %s must consume only one argument", method));
                if (method.getParameterCount() == 1 && !method.getParameterTypes()[0].equals(Integer.class))
                    warnings.add(String.format("Method %s must consume %s", method, Integer.class));

                if (!warnings.isEmpty()) {
                    warnings.forEach(log::warn);
                    continue;
                }

                CALLBACKS_MAPPER.registerCallback((param) -> {
                    try {
                        method.invoke(annotated, param);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        // Should not happen due to checks before.
                        log.warn(String.format("Could not invoke %s on %s with %s", method, annotated, param), e);
                    }
                });
                log.info("Registered {} as a callback", method);
            }
        }
    }

However I want to avoid writing

    CALLBACKS_MAPPER.registerCallback((param) -> {
        try {
            method.invoke(annotated, param);
        } catch (IllegalAccessException | InvocationTargetException e) {
            // Should not happen due to checks before.
            log.warn(String.format("Could not invoke %s on %s with %s", method, annotated, param), e);
        }
    });

in favor of something simpler, like

    CALLBACKS_MAPPER.registerCallback(SomeApacheLib.methodToFunction(annotated, method));

➥ So, is there a way to map old Java 1.1 reflection library to newer Java 8 functional interfaces, or it is me being stupid and the abovementioned solution with lambda is fine as it is?

Xobotun
  • 1,121
  • 1
  • 18
  • 29
  • Possible duplicate of [Java 8: convert lambda to a Method instance with clousure included](https://stackoverflow.com/questions/34925524/java-8-convert-lambda-to-a-method-instance-with-clousure-included) – CodeMatrix Jul 04 '19 at 09:07
  • @CodeMatrix, that's not it, unfortunately. Though it was interesting to understand the solution. Thanks :) The referenced question is about converting `lambda → Method` and what I want is `Method → lambda`. It seems that none of the techniques in that question will help me, though. – Xobotun Jul 04 '19 at 09:27

1 Answers1

9

If you're content with using reflection under the hood, just don't like the try/catch around the invoke, you can just make a simple utility function like:

public static <T> Consumer<T> toConsumer(Object annotated, Method m) {
    return param -> {
        try {
            m.invoke(annotated, param);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    };
}

And that gives you exactly the syntax you wanted:

CALLBACKS_MAPPER.registerCallback(toConsumer(annotated, method));

But if you want to actually avoid reflection altogether, you can use LambdaMetafactory to create a Consumer:

static Consumer<String> toConsumer(MethodHandles.Lookup lookup, Object annotated, Method method) throws Throwable {
    MethodType consumeString = MethodType.methodType(void.class, String.class);
    MethodHandle handle = lookup.unreflect(method);
    final CallSite site = LambdaMetafactory.metafactory(lookup, "accept",
            MethodType.methodType(Consumer.class, annotated.getClass()),
            consumeString.changeParameterType(0, Object.class),
            handle,
            consumeString);
    return (Consumer<String>) site.getTarget().invoke(annotated);
}

Change String to whatever your callbacks are expected to accept. And then:

CALLBACKS_MAPPER.registerCallback(toConsumer(MethodHandles.lookup(), annotated, method));

Of course, the only proper solution here is that you refactor your code to use a known callback interface on which you can normally call a defined method, instead of passing Methods around.

kaqqao
  • 12,984
  • 10
  • 64
  • 118
  • Thanks for the great answer! Yes, I tried to avoid reflection in runtime to avoid repeated `invoke` calls, I guess. And I never knew about `LambdaMetaFactory` thing. That is really the answer for my question. Thank you. – Xobotun Jul 04 '19 at 20:34
  • And besides, I thought that using annotations to mark methods as callbacks is a great idea, but maybe I should really create interface like `interface CallbackProvider { List getCallbacks(); }`if I ever encounter such a need in the future?.. I will think about it. :) – Xobotun Jul 04 '19 at 20:42