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