javagenericsreflectionmethodhandlelambda-metafactory

LambdaMetaFactory with Generic Static Methods?


So I'm creating a library that allows users to pass a Class<?> and collect all static methods with a specific annotation (and other criteria, such as a certain parameter count and types) and convert them into lambda FunctionalInterfaces that my library will use internally for processing.

For example:

Say I have the following class tree:

public abstract class AbstractParent {
  
  public String sayHi() {
    return getClass().getName() + " instance says hi!";
  }

}

with subclasses:

public class ChildOne extends AbstractParent {

  public int childOneSpecialMethod() {
    return 2558445;
  }
}
public class ChildTwo extends AbstractParent {

  public int childTwoSpecialMethod() {
    return 484848;
  }
  
}

My library allows for users to annotate a class's static methods with the following annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ProcessAnnotation {}

with the following rules: the static method first parameter be an instance of AbstractParent, and its second parameter must be a String, and must return a String. So, something like this:

public class GeneralProcessor {
  
  @ProcessAnnotation
  public static String easyProcessing(ChildOne one, String otherArg) {
    //Some code
    System.out.println(" === In processing for ChildOne types");
    return otherArg + one.toString();
  }

  @ProcessAnnotation
  public static String easyProcessing(ChildTwo two, String otherArg) {
    //Some code
    System.out.println(" === In processing for ChildTwo types");
    return otherArg + two.toString();
  }
}

On the library-side of things, I want to collect all these methods so that I can use them for some processing while doing it in a relatively fast manner and I found that MethodHandles and LambdaMetaFactory is the best way to do this.

Specifically, I want to invoke these collected methods using my own FunctionalInterface :

@FunctionalInterface
public interface ProcessInterface<T extends AbstractParent> {
  
  public String process(T obj, String extraArg);
}

So far, what I've tried is something like this:

public static List<ProcessInterface<? extends AbstractParent>> generate(Class<?> targetClass) throws Throwable {
    ArrayList<ProcessInterface<? extends AbstractParent>> processors = new ArrayList<>();

    for (Method method : targetClass.getDeclaredMethods()) {
      if (method.isAnnotationPresent(ProcessAnnotation.class) &&
          method.getParameterCount() == 2 && 
          AbstractParent.class.isAssignableFrom(method.getParameterTypes()[0]) &&
          method.getParameterTypes()[1] == String.class) {
        
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle handle = lookup.unreflect(method);
        CallSite callSite = LambdaMetafactory.metafactory(lookup, 
                                                          "process", 
                                                          MethodType.methodType(ProcessInterface.class), 
                                                          MethodType.methodType(String.class, 
                                                                                method.getParameterTypes()[0],
                                                                                String.class), 
                                                          handle, 
                                                          handle.type());

        ProcessInterface<? extends AbstractParent> func = (ProcessInterface<? extends AbstractParent>) callSite.getTarget().invoke();
        processors.add(func);
      }
    }

    return processors;
  }

However, I get the following error when I actually invoke the lambda. For example:

List<ProcessInterface<? extends AbstractParent>> interfaces = generate(GeneralProcessor.class);

ChildOne childOne = new ChildOne();
interfaces.get(0).process(childOne, "");

Is there a fix to do this? Or maybe even a better way to achieve this?


Solution

  • What you are doing is inherently not type safe. How do you ensure that interfaces.get(0) can process a ChildOne? What if it takes a ChildTwo instead?

    This is why you are not allowed to pass childOne to interfaces.get(0).process. interfaces.get(0) is a ProcessInterface<Something>, but you don't know what Something is. It could be ChildOne, or ChildTwo. This is what the type ProcessInterface<? extends AbstractParent> means. See also PECS.

    If Something were actually ChildTwo, then passing a ChildOne will not work at all. The implementing method does not know how to deal with that. In fact, you can only safely pass null to process.

    So let's suppose the user's of your library are responsible for ensuring that type safety, and that you are sure that this is always safe.

    In that case, cast generate's return value to List<ProcessorInterface<AbstractParent>>:

    List<ProcessInterface<AbstractParent>> interfaces = 
        (List<ProcessInterface<AbstractParent>>)(Object)generate(GeneralProcessor.class);
    

    (You can change the return type of generate to this type, if you really want.)

    ProcessInterface<AbstractParent> is a type that you can pass all kinds of AbstractParents to its process method. In fact, since type safety is gone at this point, it doesn't matter which exact type you cast to, as long as it can take any AbstractParent.

    Now if you happen to pass the wrong type of instance to process, a ClassCastException will be thrown.

    As for your LambdaMetafactory, you should pass the method type of the interface method for the fourth argument of metafactory, not the implementing method's method type.

    CallSite callSite = LambdaMetafactory.metafactory(lookup,
            "process",
            MethodType.methodType(ProcessInterface.class),
            // this should be the interface method's type
            MethodType.methodType(String.class, AbstractParent.class, String.class),
            handle,
            handle.type());