[Из песочницы] Преобразование Method Reference в Method в языке Java
Представьте, что есть у нас объект Function foo = SomeClass::someMethod;
Это лямбда, которая гарантированно является ссылкой на не статический метод. Как можно из объекта foo
достать экземпляр класса Method
, соответствующий написанному методу?
Если в кратце, то никак, информация о конкретном методе хранится исключительно в байткоде (всякие там инструментации я не учитываю). Но это не мешает нам в определённых случаях получить желаемое в обход.
Итак, наша цель это метод:
static Method unreference(Function foo) {
//...
}
который можно было бы использовать следующим образом:
Method m = unreference(SomeClass::someMethod)
Первое, с чего стоит начать, это поиск непосредственно класса, которому метод принадлежит. То есть для типа Function
нужно найти конкретный A
. Обычно, если у нас есть параметризованный тип и его конкретная реализация, мы можем найти значения типов-параметров вызовом getGenericSuperClass()
у конкретной реализации. По хорошему этот метод должен нам вернуть экземпляр класса ParameterizedType
, которых уже предоставляет массив конкретных типов через вызов getActualTypeArguments()
.
Type genericSuperclass = foo.getClass().getGenericSuperclass();
Class actualClass = (Class) ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
Но вот с лямбдами такой фокус не работает — рантайм ради простоты и эффективности забивает на эти детали и по факту выдаёт нам объект типа Function
(в такой ситуации нет необходимости генерировать 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 Method unreference(Function 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 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 size = List::size;
System.out.println(unreference(size));
Данный код корректно отработает и выведет public abstract int java.util.List.size()
.