6

Given:

import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.function.Function;

class Testcase
{
    @FunctionalInterface
    public interface MyBuilder1<R>
    {
        R apply(String message);
    }

    @FunctionalInterface
    public interface MyBuilder2<R>
    {
        R apply(Object message);
    }

    public static void main(String[] args) throws Throwable
    {
        Class<?> clazz = IllegalArgumentException.class;

        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle mh = lookup.findConstructor(clazz, MethodType.methodType(void.class, String.class));
        MethodHandle myFunctionConstructor = LambdaMetafactory.metafactory(
            lookup,
            "apply",
            MethodType.methodType(Function.class),
            mh.type().erase(),
            mh,
            mh.type()
        ).getTarget();

        MethodHandle myBuilderConstructor1 = LambdaMetafactory.metafactory(
            lookup,
            "apply",
            MethodType.methodType(MyBuilder1.class),
            mh.type().erase(),
            mh,
            mh.type()
        ).getTarget();

        MethodHandle myBuilderConstructor2 = LambdaMetafactory.metafactory(
            lookup,
            "apply",
            MethodType.methodType(MyBuilder2.class),
            mh.type().erase(),
            mh,
            mh.type()
        ).getTarget();

        @SuppressWarnings("unchecked")
        Function<String, IllegalArgumentException> functionFactory =
            (Function<String, IllegalArgumentException>) myFunctionConstructor.invokeExact();

        @SuppressWarnings("unchecked")
        MyBuilder1<IllegalArgumentException> myBuilder1Factory =
            (MyBuilder1<IllegalArgumentException>) myBuilderConstructor1.invokeExact();

        @SuppressWarnings("unchecked")
        MyBuilder2<IllegalArgumentException> myBuilder2Factory =
            (MyBuilder2<IllegalArgumentException>) myBuilderConstructor2.invokeExact();

        IllegalArgumentException runFunction = functionFactory.apply("test");
//      IllegalArgumentException runBuilder1 = myBuilder1Factory.apply("test");
        IllegalArgumentException runBuilder2 = myBuilder2Factory.apply("test");

    }
}

Why do runFunction and runBuilder2 work while runBuilder1 throws the following exception?

java.lang.AbstractMethodError: Receiver class Testcase$$Lambda$233/0x0000000800d21d88 does not define or inherit an implementation of the resolved method 'abstract java.lang.Object apply(java.lang.String)' of interface MyBuilder1.

Given that the IllegalArgumentException constructor takes a String parameter, not an Object, shouldn't the JVM accept runBuilder1 and complain about the parameter type of the other two?

Gili
  • 86,244
  • 97
  • 390
  • 689

1 Answers1

8

Your MyBuilder1<R> has a functional method

R apply(String message);

whose erased type is

Object apply(String message);

In other words, unlike Function or MyBuilder2, the erased parameter type is String, rather than Object. The erase() method of MethodType just replaces all reference types with Object, which was handy for Function and MyBuilder2 but is not suitable for MyBuilder1 anymore. There is no similarly simple method for non-trivial types. You have to include type transformation code specifically for your case (unless you want to lookup the interface method via Reflection).

For example, we can just change the return type to Object and keep the parameter types:

class Testcase
{
    @FunctionalInterface
    public interface MyBuilder1<R>
    {
        R apply(String message);
    }

    public static void main(String[] args) throws Throwable
    {
        Class<?> clazz = IllegalArgumentException.class;

        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle mh = lookup.findConstructor(clazz,
            MethodType.methodType(void.class, String.class));

        MethodHandle myBuilderConstructor1 = LambdaMetafactory.metafactory(
            lookup,
            "apply",
            MethodType.methodType(MyBuilder1.class),
            mh.type().changeReturnType(Object.class), // instead of erase()
            mh,
            mh.type()
        ).getTarget();

        @SuppressWarnings("unchecked")
        MyBuilder1<IllegalArgumentException> myBuilder1Factory =
            (MyBuilder1<IllegalArgumentException>) myBuilderConstructor1.invokeExact();

        IllegalArgumentException runBuilder1 = myBuilder1Factory.apply("test");

        runBuilder1.printStackTrace();
    }

Regarding your last question, the erased type is the type to implement, whereas the last parameter to metafactory determines the intended type, i.e. derived from the Generic interface type. The generated code may have type casts from the erased type to this type when necessary. Since this type matches the constructor signature in all cases, all variants can invoke the constructor.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 1
    [this answer](https://stackoverflow.com/a/61594842/2711488) might also help understanding the last part, i.e. about the type conversions in the generated interface implementation. Note that the “instantiatedMethodType” parameter has been renamed to “dynamicMethodType” in newer versions. – Holger Sep 16 '22 at 20:44
  • You, sir, are a life saver. So in this particular case, the error message is misleading. The error was complaining about the implementation type when in fact the problem was in the interface method. It is so painful filing bug reports with Oracle for ease-of-use issues that I don't bother any more :( They rarely get fixed. I hope that one of the committers runs across this post one day and improves the situation... – Gili Sep 17 '22 at 01:12
  • 2
    The error wasn’t from the `LambdaMetafactory` at all. It created a class according to the information provided by you, without checking. So it created a class having a method with the raw type `Object apply(Object)`. Then, you tried to invoke the method from the `MyBuilder1` interface, which wasn’t there, hence, got the `AbstractMethodError` reporting that the method `Object apply(String)` is not there despite it should be, as the class implements the interface. – Holger Sep 19 '22 at 07:28