Почему я не могу .invokeExact() здесь, хотя MethodType в порядке?

Для одного из моих проектов мне нужно сделать динамические вызовы конструктора. Но так как это Java 7, вместо "классического" API отражения я использую java.lang.invoke.

код:

@ParametersAreNonnullByDefault
public class PathMatcherProvider
{
    private static final MethodHandles.Lookup LOOKUP
        = MethodHandles.publicLookup();
    private static final MethodType CONSTRUCTOR_TYPE
        = MethodType.methodType(void.class, String.class);

    private final Map<String, Class<? extends PathMatcher>> classMap
        = new HashMap<>();
    private final Map<Class<? extends PathMatcher>, MethodHandle> handleMap
        = new HashMap<>();

    public PathMatcherProvider()
    {
        registerPathMatcher("glob", GlobPathMatcher.class);
        registerPathMatcher("regex", RegexPathMatcher.class);
    }

    public final PathMatcher getPathMatcher(final String name, final String arg)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(arg);

        final Class<? extends PathMatcher> c = classMap.get(name);
        if (c == null)
            throw new UnsupportedOperationException();

        try {
            return c.cast(handleMap.get(c).invoke(arg));
        } catch (Throwable throwable) {
            throw new RuntimeException("Unhandled exception", throwable);
        }
    }

    protected final void registerPathMatcher(@Nonnull final String name,
        @Nonnull final Class<? extends PathMatcher> matcherClass)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(matcherClass);
        try {
            classMap.put(name, matcherClass);
            handleMap.put(matcherClass, findConstructor(matcherClass));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException("cannot find constructor", e);
        }
    }

    private static <T extends PathMatcher> MethodHandle findConstructor(
        final Class<T> matcherClass)
        throws NoSuchMethodException, IllegalAccessException
    {
        Objects.requireNonNull(matcherClass);
        return LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
    }

    public static void main(final String... args)
    {
        new PathMatcherProvider().getPathMatcher("regex", "^a");
    }
}

ОК, это работает.

У меня есть проблема с этой строкой:

return c.cast(handleMap.get(c).invoke(arg));

Если я заменил invoke на invokeExact, я получаю эту трассировку стека:

Exception in thread "main" java.lang.RuntimeException: Unhandled exception
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.getPathMatcher(PathMatcherProvider.java:62)
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.main(PathMatcherProvider.java:89)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.invoke.WrongMethodTypeException: expected (String)RegexPathMatcher but found (String)Object
    at java.lang.invoke.Invokers.newWrongMethodTypeException(Invokers.java:350)
    at java.lang.invoke.Invokers.checkExactType(Invokers.java:361)
    at com.github.fge.filesystem.path.matchers.PathMatcherProvider.getPathMatcher(PathMatcherProvider.java:60)

Я не совсем понимаю. Оба GlobPathMatcher и RegexPathMatcher используют один конструктор с String в качестве аргумента, а MethodType для обоих - это то, что определено в CONSTRUCTOR_TYPE. Если бы это было не так, я бы не смог "схватить" MethodHandle.

Но я получаю WrongMethodTypeException. Почему?


EDIT: вот код после того, как я прочитал ответ; теперь мне не нужна промежуточная карта: мне просто нужно иметь одну карту, сопоставляя String с MethodHandle:

@ParametersAreNonnullByDefault
public class PathMatcherProvider
{
    private static final MethodHandles.Lookup LOOKUP
        = MethodHandles.publicLookup();
    private static final MethodType CONSTRUCTOR_TYPE
        = MethodType.methodType(void.class, String.class);

    private final Map<String, MethodHandle> handleMap
        = new HashMap<>();

    public PathMatcherProvider()
    {
        registerPathMatcher("glob", GlobPathMatcher.class);
        registerPathMatcher("regex", RegexPathMatcher.class);
    }

    public final PathMatcher getPathMatcher(final String name, final String arg)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(arg);

        final MethodHandle handle = handleMap.get(name);
        if (handle == null)
            throw new UnsupportedOperationException();

        try {
            return (PathMatcher) handle.invokeExact(arg);
        } catch (Throwable throwable) {
            throw new RuntimeException("Unhandled exception", throwable);
        }
    }

    protected final void registerPathMatcher(@Nonnull final String name,
        @Nonnull final Class<? extends PathMatcher> matcherClass)
    {
        Objects.requireNonNull(name);
        Objects.requireNonNull(matcherClass);

        final MethodHandle handle;
        final MethodType type;

        try {
            handle = LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
            type = handle.type().changeReturnType(PathMatcher.class);
            handleMap.put(name, handle.asType(type));
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException("cannot find constructor", e);
        }
    }
}

Ответ 1

Когда компилятор выдает вызов invokeExact, он записывает Object как ожидаемый тип возвращаемого значения. Из метода Jandad javadoc (основное внимание):

Как обычно с виртуальными методами, вызовы на уровне исходного кода invokeExact и ссылаются на компиляцию в invokevirtual инструкцию. Более необычно, компилятор должен записывать фактические типы аргументов и не может выполнять преобразования вызова метода в аргументы. Вместо этого он должен подталкивать их к стеку в соответствии с их собственными непроизнесенными типами. Сам объект дескриптора метода помещается в стек перед аргументами. Затем компилятор вызывает дескриптор метода с дескриптором символьного типа, который описывает типы аргументов и возвращаемых данных.

Чтобы создать полный дескриптор символьного типа, компилятор также должен определить тип возвращаемого значения. Это основано на приведении выражения вызова метода, если оно есть, или Object, если вызов является выражением или недействительным, если вызов является выражением. Листинг может быть примитивным тип (но не пусто).

Во время выполнения дескриптор метода фактически возвращает RegexPathMatcher, поэтому invokeExact завершается с ошибкой WrongMethodTypeException.

Вам нужно явно указать тип возвращаемого значения с использованием (время компиляции):

return (RegexPathMatcher)handleMap.get(c).invokeExact(arg);

За исключением того, что вам нужно быть общим по сравнению с различными реализациями PathMatcher, поэтому вы должны преобразовать дескрипторы методов для возврата PathMatcher с использованием asType, а затем вызвать с PathMatcher как ожидаемый тип возвращаемого значения.

//in findConstructor
MethodHandle h = LOOKUP.findConstructor(matcherClass, CONSTRUCTOR_TYPE);
return h.asType(h.type().changeReturnType(PathMatcher.class));

//in getPathMatcher
return (PathMatcher)handleMap.get(c).invokeExact(arg);

Ответ 2

После трех лет публикации я пришел, чтобы прочитать это, и, хотя ответ действительно правильный, было довольно сложно понять все. Поэтому, при всем моем уважении, я опубликую несколько иной подход (в случае, если кто-то вроде меня должен дважды поцарапать голову, чтобы понять).

Основная проблема здесь - это две разные вызовы: invoke и invokeExact. Но сначала эти два метода в исходном коде аннотируются с помощью

@PolymorphicSignature

которые также называются compiler overloads. Эти методы очень специфичны для java-компилятора - никакие другие методы не обрабатываются одинаково.

Чтобы понять, давайте представим пример. Вот простой класс с единственным методом:

static class Calle {

    public Object go(Object left, Object right) {
        // do something with left and right
        return new Object();
    }

}

Скомпилируйте это и посмотрите, как выглядит сгенерированный байт-код (javap -c Calle.class). Среди некоторых строк будет этот метод:

public java.lang.Object go (java.lang.Object, java.lang.Object);

Подпись: two arguments of type java.lang.Object and a return of type java.lang.Object. Все идет нормально.

Так что это совершенно законно для этого:

 Calle c = new Calle();
 int left = 3;
 int right = 4;
 c.go(left, right);

И байт-код для этого будет выглядеть:

invokevirtual # 5//Метод CompilerOverloads $Calle.go: (Ljava/lang/Object; Ljava/lang/Object;) Ljava/lang/Object;

Метод принимает два объекта, и два целых числа вполне законны для передачи в качестве параметров.

Теперь подумайте об определении метода:

 MethodHandle#invoke

это подпись java.lang.Object var arg и возвращает java.lang.Object.

Итак, как этот код будет компилироваться?

 Lookup l = MethodHandles.lookup();
 MethodType type = MethodType.methodType(Object.class, Object.class, Object.class);
 MethodHandle handle = l.findVirtual(Calle.class, "go", type);
 Object result = handle.invoke(c, left, right); // what is generated here?

Интересно, что он компилируется совсем по-другому, чем наш Calle::go

  Method java/lang/invoke/MethodHandle.invoke:(LCalle;II)Ljava/lang/Object;

Вводимые параметры: Integer, Integer, а тип возврата - java.lang.Object. Он, как компилятор, доверял объявлению метода компиляции и сгенерировал его.

Если мы хотим, например, изменить тип возвращаемого значения на int, нам нужно указать, что в качестве даты при компиляции:

 int result = (int) handle.invoke(c, left, right); 

И затем изменения подписи на уровне байтового кода (акцент мой):

Метод java/lang/invoke/MethodHandle.invoke: (LCalle; II) I

Это не происходит нигде в мире jdk, насколько я знаю.

И теперь проблема invoke vs invokeExact становится немного очевидной (одна является сигнатурой точной, а другая немного больше свободно).