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


  1. apangin
    05.10.2016 16:16
    +4

    Работает не только с интерфейсами. И со статическими методами тоже.

    import sun.misc.SharedSecrets;
    import sun.reflect.ConstantPool;
    
    import java.lang.reflect.Member;
    import java.lang.reflect.Method;
    import java.util.function.Function;
    
    public class MethodRefs {
    
        public static void main(String[] args) throws Exception {
            Function<Object, Integer> hashCodeRef = Object::hashCode;
            System.out.println(unreference(hashCodeRef));
        }
    
        public static Method unreference(Function<?, ?> ref) {
            ConstantPool pool = SharedSecrets.getJavaLangAccess().getConstantPool(ref.getClass());
            int size = pool.getSize();
            for (int i = 1; i < size; i++) {
                try {
                    Member member = pool.getMethodAt(i);
                    if (member instanceof Method) {
                        return (Method) member;
                    }
                } catch (IllegalArgumentException e) {
                    // skip non-method entry
                }
            }
            throw new IllegalArgumentException("Not a method reference");
        }
    }
    


    1. apangin
      05.10.2016 16:16
      +1

      Но ваш вариант мне тоже понравился!


      1. ibessonov
        05.10.2016 16:27

        А подход с пулом костант хорош!
        Правда у меня есть одно замечание — брать первый попавшийся метод некорректно, там может быть что-угодно (например java.lang.Integer.valueOf(int), используемый для боксинга). Во всяком случае мне неизвестно, всегда ли нужный метод будет встречаться первым. Нужен дополнительный анализ


        1. apangin
          05.10.2016 17:56
          +1

          Для методов без аргументов искомый Method всегда окажется первым. Так уж LambdaMetafactory устроена. А, вот, для преобразования аргументов могут вызываться и другие методы. И тут уже без анализа байткода не обойтись. Строго говоря, и байткода-то может не быть. Всё это детали конкретной реализации.


  1. apangin
    05.10.2016 17:53

    (удалил)


  1. Mingun
    05.10.2016 20:20

    А где такие хаки могут понадобиться?


    1. ibessonov
      05.10.2016 21:44
      +1

      Боюсь даже предположить. Вообще было бы удобно иметь такой универсальный инструмент, когда тебе в коде нужно получить конкретный метод конкретного класса.
      getDeclaredMethod это конечно хорошо, но не так синтаксически красиво, как явная ссылка на метод через ::


    1. 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(....));
              ....
          }
      }


  1. dougrinch
    06.10.2016 13:40

    Прикольно, но очень не нравится что делается вызов исходного метода. Если нам не повезет и он реально будет принимать Object, то мы на вызове unreference сначала сотворим side effect, а потом еще и замаскируем его с помощью RuntimeException("Something's wrong").


    1. ibessonov
      06.10.2016 13:55

      Всё верно, для методов Object этот код совершенно не годится.


      1. dougrinch
        06.10.2016 19:39

        Я про другое. С методами класса Object просто вылетит ексепшн мол "я такое не умею" и все будет хорошо. А вот если у меня есть статический метод, который принимает Object, то внутри он может делать какой-нибудь side effect. И вызов unreference этот side effect успешно после себя оставит.


        Понятно, что это очень теоретическая проблема и статических методов с сайд эффектом, принимающих Object, не существует. Но, все-таки, в библиотечной функции такая, даже сугубо теоретическая, проблема недопустима.


        p.s. Но повторюсь. Решение прикольное (в хорошем смысле этого слова).