Представьте, что есть у нас объект 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().
