Преобразование Method Reference в Method в языке Java

в 11:54, , рубрики: java, lambda, proxy, reflection, ненормальное программирование

Представьте, что есть у нас объект Function<A, B> foo = SomeClass::someMethod; Это лямбда, которая гарантированно является ссылкой на не статический метод. Как можно из объекта foo достать экземпляр класса Method, соответствующий написанному методу?

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

Итак, наша цель это метод:

static <A, B> Method unreference(Function<A, B> foo) {
    //...
}

который можно было бы использовать следующим образом:

Method m = unreference(SomeClass::someMethod)

Первое, с чего стоит начать, это поиск непосредственно класса, которому метод принадлежит. То есть для типа Function<A, B> нужно найти конкретный A. Обычно, если у нас есть параметризованный тип и его конкретная реализация, мы можем найти значения типов-параметров вызовом getGenericSuperClass() у конкретной реализации. По хорошему этот метод должен нам вернуть экземпляр класса ParameterizedType, которых уже предоставляет массив конкретных типов через вызов getActualTypeArguments().

Type genericSuperclass = foo.getClass().getGenericSuperclass();
Class actualClass = (Class) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];

Но вот с лямбдами такой фокус не работает — рантайм ради простоты и эффективности забивает на эти детали и по факту выдаёт нам объект типа Function<Object, Object> (в такой ситуации нет необходимости генерировать bridge-методы и всякие-там метаданные). GenericSuperclass для него совпадает с просто суперклассом, и приведённый выше код работать не будет.

Для нас это конечно не лучшая новость, но не всё потеряно, поскольку реализация apply (или любого другого "функционального" метода) для лямбд выглядит примерно так:

public Object apply(Object param) {
    SomeClass cast = (SomeClass) param;
    return invokeSomeMethod(cast);
}

Именно этот cast нам и нужен, поскольку это одно из немногих мест, реально хранящих информацию о типе (второе такое место — вызываемый в следующей строке метод). Что будет, если передать в apply объект не того класса? Верно, ClassCastException.

try {
    foo.apply(new Object());
} catch (ClassCastException e) {
    //...
}

Сообщение в нашем исключении будет выглядеть таким образом: java.lang.Object cannot be cast to sample.SomeClass (во всяком случае в тестируемой версии JRE, спецификация на тему этого сообщения ничего не говорит). Если только это не метод класса Object — тут мы ничего гарантировать, увы, не сможем.

Зная имя класса, нам не составит труда получить соответствующий ему экземпляр класса Class. Теперь осталось получить имя метода. С этим уже посложнее, поскольку информация о методе, как упоминалось ранее, есть только в байткоде. Если бы у нас был свой экземпляр класса SomeClass, вызовы методов которого мы могли бы отслеживать, то мы могли бы передать его в apply и посмотреть, что же вызвалось (пролгядев стэк вызовов или как-нибудь ещё).

Первое, что приходит мне на ум, это конечно-же java.lang.reflect.Proxy, но он вводит нам новое и очень сильное ограничение — SomeClass должен быть интерфейсом, чтобы мы могли сгенерировать для него проксю. С другой стороны, код при этом получается абсолютно элементарным — мы создаём прокси, вызываем apply и внутри метода invoke нашего объекта InvocationHandler получаем готовый Method, который и хотели найти!

Полный код выглядит примерно так. Пользоваться им в реальных проектах я, естественно, не рекомендую.

private static final Pattern classNameExtractor = Pattern.compile("cannot be cast to (.*)$");

public static <A, B> Method unreference(Function<A, B> reference) {
    Function erased = reference;
    try {
        erased.apply(new Object());
    } catch (ClassCastException cce) {
        Matcher matcher = classNameExtractor.matcher(cce.getMessage());
        if (matcher.find()) {
            try {
                Class<?> iface = Class.forName(matcher.group(1));
                if (iface.isInterface()) {
                    AtomicReference<Method> resultHolder = new AtomicReference<>();
                    Object obj = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                            new Class[]{iface},
                            (proxy, method, args) -> {
                                resultHolder.set(method);
                                return null;
                            }
                    );
                    try {
                        erased.apply(obj);
                    } catch (Throwable ignored) {
                    }
                    return resultHolder.get();
                }
            } catch (ClassNotFoundException ignored) {
            }
        }
    }
    throw new RuntimeException("Something's wrong");
}

Проверить можно вот так (отдельная переменная нужна потому, что джава не всегда справляется с выводом типов):

Function<List, Integer> size = List::size;
System.out.println(unreference(size));

Данный код корректно отработает и выведет public abstract int java.util.List.size().

Автор: ibessonov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js