javalambdareflectionlambda-metafactory

Is the built-in LambdaMetafactory parameter type checking correct?


For example, when I execute the following:

public static int addOne(Number base) {
  return base.intValue() + 1;
}

public static interface AddOneLambda {
  public int addOne(Integer base);
}

public static void main(String[] a) throws Throwable {
  Method lambdaMethod = AddOneLambda.class.getMethod("addOne", Integer.class);
  Class<?>[] lambdaParameters = Stream.of(lambdaMethod.getParameters()).map(p -> p.getType()).toArray(Class[]::new);

  Method sourceMethod = Main.class.getMethod("addOne", Number.class);
  Class<?>[] sourceParameters = Stream.of(sourceMethod.getParameters()).map(p -> p.getType()).toArray(Class[]::new);

  MethodHandles.Lookup lookup = MethodHandles.lookup();
  CallSite site = LambdaMetafactory.metafactory(lookup, //
      lambdaMethod.getName(), //
      MethodType.methodType(lambdaMethod.getDeclaringClass()), //
      MethodType.methodType(lambdaMethod.getReturnType(), lambdaParameters), //
      lookup.unreflect(sourceMethod), //
      MethodType.methodType(sourceMethod.getReturnType(), sourceParameters));

  AddOneLambda addOneLambda = (AddOneLambda) site.getTarget().invoke();
  System.out.println("1 + 1 = " + addOneLambda.addOne(1));
}

I receive the following exception from metafactory:

LambdaConversionException: Type mismatch for dynamic parameter 0: class java.lang.Number is not a subtype of class java.lang.Integer

I don't understand this. Passing an Integer to the AddOneLambda should always be fine, because the underlying addOne method can accept Integers as part of it's Number signature - so I believe this configuration should be "safe".

On the other hand, when I execute the above with this change:

public static int addOne(Integer base) {
  return base.intValue() + 1;
}

public interface AddOneLambda {
  public int addOne(Number base);
}

The metafactory allows now this without exception, but it doesn't seem right. I can pass any kind of Number to AddOneLambda, even though the underlying method can only handle Integers - so I believe this configuration to be "unsafe". Indeed, if I now call addOneLambda.addOne(1.5) I receive an exception for inability to cast Double to Integer.

Why then is my initial code not allowed, while the change which ultimately allows for invalid types to be passed ok? Is it something to do with the values I'm passing to metafactory, is metafactory incorrectly checking the types, or does this prevent some other kind of situation I haven't considered? If relevant, I'm using JDK 17.0.3.


Solution

  • You seem to have misunderstood the purpose of the dynamicMethodType argument of metafactory (the sixth one, which you have as MethodType.methodType(sourceMethod.getReturnType(), sourceParameters)). The point of it is to produce runtime type errors: the generated method will have the type given by interfaceMethodType (fourth parameter) but will check that its parameters and return value obey the dynamicMethodType. For example, after erasure, Consumer contains void accept(Object), and if you do something like (Consumer<String>)String::intern the generated accept(Object) must do a checked cast to String. The underlying call to metafactory will pass the method type void(Object) as interfaceMethodType and void(String) as dynamicMethodType.

    As the purpose of dynamicMethodType is to act as "further" restrictions on interfaceMethodType, it is considered an error for it to be looser. From a theoretical/user standpoint this seems a bit ugly, but from an implementation standpoint (why generate a useless cast? does producing a wider type indicate a bug in the caller?) you might consider it justified.

    To get all language lawyery, you have violated the "linkage invariants" in LambdaMetafactory's documentation

    Assume the linkage arguments are as follows:

    • ...
    • interfaceMethodType (describing the implemented method type) has N parameters, of types (U1..Un) and return type Ru;
    • ...
    • dynamicMethodType (allowing restrictions on invocation) has N parameters, of types (T1..Tn) and return type Rt.

    Then the following linkage invariants must hold:

    • interfaceMethodType and dynamicMethodType have the same arity N, and for i=1..N, Ti and Ui are the same type, or Ti and Ui are both reference types and Ti is a subtype of Ui
    • ...

    As you are not dealing with generics or anything similarly interesting, you should do as the Javadoc for metafactory suggests and pass the same int(Integer) type for interfaceMethodType and dynamicMethodType.