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.
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) hasN
parameters, of types (U1
..Un
) and return typeRu
;- ...
dynamicMethodType
(allowing restrictions on invocation) hasN
parameters, of types (T1
..Tn
) and return typeRt
.Then the following linkage invariants must hold:
interfaceMethodType
anddynamicMethodType
have the same arityN
, and fori
=1
..N
,Ti
andUi
are the same type, orTi
andUi
are both reference types andTi
is a subtype ofUi
- ...
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
.