Представьте, что есть у нас объект 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()
.
Комментарии (11)
Mingun
05.10.2016 20:20А где такие хаки могут понадобиться?
ibessonov
05.10.2016 21:44+1Боюсь даже предположить. Вообще было бы удобно иметь такой универсальный инструмент, когда тебе в коде нужно получить конкретный метод конкретного класса.
getDeclaredMethod это конечно хорошо, но не так синтаксически красиво, как явная ссылка на метод через ::
tsabir
09.10.2016 13:23+1Иногда есть смысл сослаться статично на метод, известный при компиляции. Потом будет легко переименовать метод, и не поломать случайно код:
class SimpleWebFramework { public static String notFound() { return "Page not found!"; } public String invokeMethodForPath(Map<String, Method> methods, String path) { Method m = methods.getOrDefault(path, unreference(SimpleWebFramework::notFound)); .... return String.valueOf(m.invoke(....)); .... } }
dougrinch
06.10.2016 13:40Прикольно, но очень не нравится что делается вызов исходного метода. Если нам не повезет и он реально будет принимать Object, то мы на вызове unreference сначала сотворим side effect, а потом еще и замаскируем его с помощью RuntimeException("Something's wrong").
ibessonov
06.10.2016 13:55Всё верно, для методов Object этот код совершенно не годится.
dougrinch
06.10.2016 19:39Я про другое. С методами класса Object просто вылетит ексепшн мол "я такое не умею" и все будет хорошо. А вот если у меня есть статический метод, который принимает Object, то внутри он может делать какой-нибудь side effect. И вызов unreference этот side effect успешно после себя оставит.
Понятно, что это очень теоретическая проблема и статических методов с сайд эффектом, принимающих Object, не существует. Но, все-таки, в библиотечной функции такая, даже сугубо теоретическая, проблема недопустима.
p.s. Но повторюсь. Решение прикольное (в хорошем смысле этого слова).
apangin
Работает не только с интерфейсами. И со статическими методами тоже.
apangin
Но ваш вариант мне тоже понравился!
ibessonov
А подход с пулом костант хорош!
Правда у меня есть одно замечание — брать первый попавшийся метод некорректно, там может быть что-угодно (например java.lang.Integer.valueOf(int), используемый для боксинга). Во всяком случае мне неизвестно, всегда ли нужный метод будет встречаться первым. Нужен дополнительный анализ
apangin
Для методов без аргументов искомый Method всегда окажется первым. Так уж LambdaMetafactory устроена. А, вот, для преобразования аргументов могут вызываться и другие методы. И тут уже без анализа байткода не обойтись. Строго говоря, и байткода-то может не быть. Всё это детали конкретной реализации.