Перетворення Method Reference в Method в мові Java

Уявіть, що у нас є об'єкт
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 to cast (.*)$");

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()
.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.