[Java] Получение Method из Method Reference в Java
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Я столкнулся с проблемой — как получить из method reference вида
Function<String, Integer> fun = String::length;
вызываемый метод класса (или хотя бы его имя), т.е. в примере это java.lang.String.length(). Как выяснилось, не одного меня волновал этот вопрос, нашлись такие обсуждения на stackoverflow [1], [2], проекты на GitHub, которые так или иначе касаются этой проблемы [1], [2], [3], но не один из них не дает ровно то, что нужно. На Хабре ibessonov предложил свое решение, а apangin — свое в комментарии. Вариант Андрея мне понравился, но вскоре выяснились некоторые связанные с ним проблемы. Во-первых, это основано на внутренних классах JDK, все это работает только в Java 8, но не в более поздних версиях. Во-вторых, этот метод имеет ряд ограничений, например для BiFunction или BiConsumer он выдает неверное значение (это как раз обсуждается в комментариях).
В общем, перебрав несколько вариантов, удалось найти тот, который не имеет этих изьянов и работает в поздних версиях JDK — SerializedLambda. Забегая вперед сразу скажу, что это работает только с функциональными интерфейсами, помеченными как java.io.Serializable (т.е. с java.util.function.Function работать не будет), но в целом это не проблемное ограничение.
Зачем вообще это нужно?
Перед тем как перейти к решению, отвечу на резонный вопрос а зачем это может понадобиться?
В моем случае это используется в тестовых фреймворках, чтобы добавить диагностическую информацию о вызываемом методе, который определялся через method reference. Это позволяет сделать одновременно лаконичный и compile-безопасный код. Например, можно сделать "where" hamcrest matcher, использующий функцию-extractor для значений:
List<SamplePojo> list = Arrays.asList(
new SamplePojo()
.setName("name1"),
new SamplePojo()
.setName("name2")
);
// success
assertThat(list, hasItem(where(SamplePojo::getName, equalTo("name1"))));
// fails with diagnostics:
// java.lang.AssertionError:
// Expected: every item is Object that matches "name1" after call SamplePojo.getName
// but: an item was "name2"
assertThat(list, everyItem(where(SamplePojo::getName, equalTo("name1"))));
Обратите внимание, что в случае ошибки тест упадет с диагностикой "after call SamplePojo.getName". Для однострочного теста это кажется избыточным, но hamcrest-выражения могут иметь многоуровневую вложенность, поэтому лишняя детализация не помешает.
SerializedLambda
Класс java.lang.invoke.SerializedLambda — это сериализованное представление лямбда-выражения. Важно уточнить, что интерфейс лямбда-выражения должен быть помечен как Serializable:
@FunctionalInterface
public interface ThrowingFunction<T, R> extends java.io.Serializable {
R apply(T t) throws Exception;
}
Как следует из javadoc класса, чтобы получить объект SerializedLambda, следует вызвать приватный writeReplace на лямбда-объекте:
@Nullable
private static SerializedLambda getSerializedLambda(Serializable lambda) {
for (Class<?> cl = lambda.getClass(); cl != null; cl = cl.getSuperclass()) {
try {
Method m = cl.getDeclaredMethod("writeReplace");
m.setAccessible(true);
Object replacement = m.invoke(lambda);
if (!(replacement instanceof SerializedLambda)) {
break;
}
return (SerializedLambda) replacement;
} catch (NoSuchMethodException e) {
// skip, continue
} catch (IllegalAccessException | InvocationTargetException | SecurityException e) {
throw new IllegalStateException("Failed to call writeReplace", e);
}
}
return null;
}
Дальше все нужные детали достаем из SerializedLambda, нужно только несложное преобразование. Например, имена классов записаны через "/"(слеш) вместо точек, а примитивные типы сделаны сокращениями. Например getImplMethodSignature() возвращает строку "(Z)V", это означает один аргумент (внутри скобок) типа boolean ("Z"), тип возвращаемого значения — void ("V"):
static Class<?>[] parseArgumentClasses(String implMethodSignature) {
int parenthesesPos = implMethodSignature.indexOf(')');
if (!implMethodSignature.startsWith("(") || parenthesesPos <= 0) {
throw new IllegalStateException("Wrong format of implMethodSignature " + implMethodSignature);
}
String argGroup = implMethodSignature.substring(1, parenthesesPos);
List<Class<?>> classes = new ArrayList<>();
for (String token : argGroup.split(";")) {
if (token.isEmpty()) {
continue;
}
classes.add(parseType(token, false));
}
return classes.toArray(new Class[0]);
}
private static Class<?> parseType(String typeName, boolean allowVoid) {
if ("Z".equals(typeName)) {
return boolean.class;
} else if ("B".equals(typeName)) {
return byte.class;
} else if ("C".equals(typeName)) {
return char.class;
} else if ("S".equals(typeName)) {
return short.class;
} else if ("I".equals(typeName)) {
return int.class;
} else if ("J".equals(typeName)) {
return long.class;
} else if ("F".equals(typeName)) {
return float.class;
} else if ("D".equals(typeName)) {
return double.class;
} else if ("V".equals(typeName)) {
if (allowVoid) {
return void.class;
} else {
throw new IllegalStateException("void (V) type is not allowed");
}
} else {
if (!typeName.startsWith("L")) {
throw new IllegalStateException("Wrong format of argument type "
+ "(should start with 'L'): " + typeName);
}
String implClassName = typeName.substring(1);
return implClassForName(implClassName);
}
}
private static Class<?> implClassForName(String implClassName) {
String className = implClassName.replace('/', '.');
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Failed to load class " + implClassName, e);
}
}
Остается только найти подходящий метод в интерфейсе, чтобы вернуть правильный результат:
@Nullable
public static Method unreferenceLambdaMethod(Serializable lambda) {
SerializedLambda serializedLambda = getSerializedLambda(lambda);
if (serializedLambda != null
&& (serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_invokeVirtual
|| serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_invokeStatic)) {
Class<?> cls = implClassForName(serializedLambda.getImplClass());
Class<?>[] argumentClasses = parseArgumentClasses(serializedLambda.getImplMethodSignature());
return Stream.of(cls.getDeclaredMethods())
.filter(method -> method.getName().equals(serializedLambda.getImplMethodName())
&& Arrays.equals(method.getParameterTypes(), argumentClasses))
.findFirst().orElse(null);
}
return null;
}
А с конструкторами работает?
Работает. Тут будет другой тип serializedLambda.getImplMethodKind().
@Nullable
public static Constructor<?> unreferenceLambdaConstructor(Serializable lambda) {
SerializedLambda serializedLambda = getSerializedLambda(lambda);
if (serializedLambda != null
&& (serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_newInvokeSpecial)) {
Class<?> cls = implClassForName(serializedLambda.getImplClass());
Class<?>[] argumentClasses = parseArgumentClasses(serializedLambda.getImplMethodSignature());
return Stream.of(cls.getDeclaredConstructors())
.filter(constructor -> Arrays.equals(constructor.getParameterTypes(), argumentClasses))
.findFirst().orElse(null);
}
return null;
}
Пример:
// new Integer(String)
ThrowingFunction<String, Integer> fun = Integer::new;
Constructor<?> constructor = unreferenceLambdaConstructor(fun);
Совместимость
Это работает в Java 8, Java 11, Java 14, не требует внешних библиотек или доступа к приватным api JDK, не требует дополнительных параметров запуска JVM. Кроме того, применимо и к статическим методам и к методам с разным количеством аргументов (т.е. не только Function-подобные).
Единственное неудобство — для каждого вида функций придется создать сериализуемое представление, например:
@FunctionalInterface
public interface SerializableBiFunction<T, U, R> extends Serializable {
R apply(T arg1, U arg2);
}
// Integer.parseInt(String, int)
SerializableBiFunction<String, Integer, Integer> fun = Integer::parseInt;
Method method = unreferenceLambdaMethod(fun);
Полную реализацию можно найти тут.
Готовый утилитарный метод
Вы можете скопировать класс в свой проект, но я бы не рекомендовал использовать это вне scope test. Кроме того, можно добавить зависимость:
<dependency>
<groupId>com.github.seregamorph</groupId>
<artifactId>hamcrest-more-matchers</artifactId>
<version>0.1</version>
<scope>test</scope>
</dependency>
и вызвать
import static com.github.seregamorph.hamcrest.TestLambdaUtils.unreferenceLambdaMethod;
...
ThrowingFunction<String, String> fun = String::toLowerCase;
Method method = unreferenceLambdaMethod(fun);
assertEquals("toLowerCase", method.getName());
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, ReactJS, Программирование, Разработка веб-сайтов] Кастомные хуки. Part 1
- [Java, Oracle, Программирование, Промышленное программирование] Бинарные операторы в Java
- [Разработка веб-сайтов, JavaScript, Программирование, VueJS] Создание блога с помощью Nuxt Content(часть первая) (перевод)
- [JavaScript, VueJS] Vue 3 Composition API: Ref или Reactive (перевод)
- [JavaScript, Программирование] Эффектное программирование. Часть 1: итераторы и генераторы
- [Программирование] Структурное против ООП программирование
- [Big Data, Java, Scala] Big Data Tools EAP 11: Zeppelin в DataGrip и spark-submit во всех поддерживаемых IDE
- [Big Data, Java, Scala] Big Data Tools Update 11 Is Out
- [Разработка под Android] Автоматический Code Improvement при коммите в Android Studio
- [Разработка веб-сайтов, JavaScript, ReactJS] Почему мы выбрали MobX, а не Redux, и как его использовать эффективнее
Теги для поиска: #_java, #_java, #_hamcrest, #_lambda_functions, #_nenormalnoe_programmirovanie (ненормальное программирование), #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:56
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Я столкнулся с проблемой — как получить из method reference вида Function<String, Integer> fun = String::length;
вызываемый метод класса (или хотя бы его имя), т.е. в примере это java.lang.String.length(). Как выяснилось, не одного меня волновал этот вопрос, нашлись такие обсуждения на stackoverflow [1], [2], проекты на GitHub, которые так или иначе касаются этой проблемы [1], [2], [3], но не один из них не дает ровно то, что нужно. На Хабре ibessonov предложил свое решение, а apangin — свое в комментарии. Вариант Андрея мне понравился, но вскоре выяснились некоторые связанные с ним проблемы. Во-первых, это основано на внутренних классах JDK, все это работает только в Java 8, но не в более поздних версиях. Во-вторых, этот метод имеет ряд ограничений, например для BiFunction или BiConsumer он выдает неверное значение (это как раз обсуждается в комментариях). В общем, перебрав несколько вариантов, удалось найти тот, который не имеет этих изьянов и работает в поздних версиях JDK — SerializedLambda. Забегая вперед сразу скажу, что это работает только с функциональными интерфейсами, помеченными как java.io.Serializable (т.е. с java.util.function.Function работать не будет), но в целом это не проблемное ограничение. Зачем вообще это нужно? Перед тем как перейти к решению, отвечу на резонный вопрос а зачем это может понадобиться? В моем случае это используется в тестовых фреймворках, чтобы добавить диагностическую информацию о вызываемом методе, который определялся через method reference. Это позволяет сделать одновременно лаконичный и compile-безопасный код. Например, можно сделать "where" hamcrest matcher, использующий функцию-extractor для значений: List<SamplePojo> list = Arrays.asList(
new SamplePojo() .setName("name1"), new SamplePojo() .setName("name2") ); // success assertThat(list, hasItem(where(SamplePojo::getName, equalTo("name1")))); // fails with diagnostics: // java.lang.AssertionError: // Expected: every item is Object that matches "name1" after call SamplePojo.getName // but: an item was "name2" assertThat(list, everyItem(where(SamplePojo::getName, equalTo("name1")))); Обратите внимание, что в случае ошибки тест упадет с диагностикой "after call SamplePojo.getName". Для однострочного теста это кажется избыточным, но hamcrest-выражения могут иметь многоуровневую вложенность, поэтому лишняя детализация не помешает. SerializedLambda Класс java.lang.invoke.SerializedLambda — это сериализованное представление лямбда-выражения. Важно уточнить, что интерфейс лямбда-выражения должен быть помечен как Serializable: @FunctionalInterface
public interface ThrowingFunction<T, R> extends java.io.Serializable { R apply(T t) throws Exception; } Как следует из javadoc класса, чтобы получить объект SerializedLambda, следует вызвать приватный writeReplace на лямбда-объекте: @Nullable
private static SerializedLambda getSerializedLambda(Serializable lambda) { for (Class<?> cl = lambda.getClass(); cl != null; cl = cl.getSuperclass()) { try { Method m = cl.getDeclaredMethod("writeReplace"); m.setAccessible(true); Object replacement = m.invoke(lambda); if (!(replacement instanceof SerializedLambda)) { break; } return (SerializedLambda) replacement; } catch (NoSuchMethodException e) { // skip, continue } catch (IllegalAccessException | InvocationTargetException | SecurityException e) { throw new IllegalStateException("Failed to call writeReplace", e); } } return null; } Дальше все нужные детали достаем из SerializedLambda, нужно только несложное преобразование. Например, имена классов записаны через "/"(слеш) вместо точек, а примитивные типы сделаны сокращениями. Например getImplMethodSignature() возвращает строку "(Z)V", это означает один аргумент (внутри скобок) типа boolean ("Z"), тип возвращаемого значения — void ("V"): static Class<?>[] parseArgumentClasses(String implMethodSignature) {
int parenthesesPos = implMethodSignature.indexOf(')'); if (!implMethodSignature.startsWith("(") || parenthesesPos <= 0) { throw new IllegalStateException("Wrong format of implMethodSignature " + implMethodSignature); } String argGroup = implMethodSignature.substring(1, parenthesesPos); List<Class<?>> classes = new ArrayList<>(); for (String token : argGroup.split(";")) { if (token.isEmpty()) { continue; } classes.add(parseType(token, false)); } return classes.toArray(new Class[0]); } private static Class<?> parseType(String typeName, boolean allowVoid) { if ("Z".equals(typeName)) { return boolean.class; } else if ("B".equals(typeName)) { return byte.class; } else if ("C".equals(typeName)) { return char.class; } else if ("S".equals(typeName)) { return short.class; } else if ("I".equals(typeName)) { return int.class; } else if ("J".equals(typeName)) { return long.class; } else if ("F".equals(typeName)) { return float.class; } else if ("D".equals(typeName)) { return double.class; } else if ("V".equals(typeName)) { if (allowVoid) { return void.class; } else { throw new IllegalStateException("void (V) type is not allowed"); } } else { if (!typeName.startsWith("L")) { throw new IllegalStateException("Wrong format of argument type " + "(should start with 'L'): " + typeName); } String implClassName = typeName.substring(1); return implClassForName(implClassName); } } private static Class<?> implClassForName(String implClassName) { String className = implClassName.replace('/', '.'); try { return Class.forName(className); } catch (ClassNotFoundException e) { throw new IllegalStateException("Failed to load class " + implClassName, e); } } Остается только найти подходящий метод в интерфейсе, чтобы вернуть правильный результат: @Nullable
public static Method unreferenceLambdaMethod(Serializable lambda) { SerializedLambda serializedLambda = getSerializedLambda(lambda); if (serializedLambda != null && (serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_invokeVirtual || serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_invokeStatic)) { Class<?> cls = implClassForName(serializedLambda.getImplClass()); Class<?>[] argumentClasses = parseArgumentClasses(serializedLambda.getImplMethodSignature()); return Stream.of(cls.getDeclaredMethods()) .filter(method -> method.getName().equals(serializedLambda.getImplMethodName()) && Arrays.equals(method.getParameterTypes(), argumentClasses)) .findFirst().orElse(null); } return null; } А с конструкторами работает? Работает. Тут будет другой тип serializedLambda.getImplMethodKind(). @Nullable
public static Constructor<?> unreferenceLambdaConstructor(Serializable lambda) { SerializedLambda serializedLambda = getSerializedLambda(lambda); if (serializedLambda != null && (serializedLambda.getImplMethodKind() == MethodHandleInfo.REF_newInvokeSpecial)) { Class<?> cls = implClassForName(serializedLambda.getImplClass()); Class<?>[] argumentClasses = parseArgumentClasses(serializedLambda.getImplMethodSignature()); return Stream.of(cls.getDeclaredConstructors()) .filter(constructor -> Arrays.equals(constructor.getParameterTypes(), argumentClasses)) .findFirst().orElse(null); } return null; } Пример: // new Integer(String)
ThrowingFunction<String, Integer> fun = Integer::new; Constructor<?> constructor = unreferenceLambdaConstructor(fun); Совместимость Это работает в Java 8, Java 11, Java 14, не требует внешних библиотек или доступа к приватным api JDK, не требует дополнительных параметров запуска JVM. Кроме того, применимо и к статическим методам и к методам с разным количеством аргументов (т.е. не только Function-подобные). Единственное неудобство — для каждого вида функций придется создать сериализуемое представление, например: @FunctionalInterface
public interface SerializableBiFunction<T, U, R> extends Serializable { R apply(T arg1, U arg2); } // Integer.parseInt(String, int) SerializableBiFunction<String, Integer, Integer> fun = Integer::parseInt; Method method = unreferenceLambdaMethod(fun); Полную реализацию можно найти тут. Готовый утилитарный метод Вы можете скопировать класс в свой проект, но я бы не рекомендовал использовать это вне scope test. Кроме того, можно добавить зависимость: <dependency>
<groupId>com.github.seregamorph</groupId> <artifactId>hamcrest-more-matchers</artifactId> <version>0.1</version> <scope>test</scope> </dependency> и вызвать import static com.github.seregamorph.hamcrest.TestLambdaUtils.unreferenceLambdaMethod;
... ThrowingFunction<String, String> fun = String::toLowerCase; Method method = unreferenceLambdaMethod(fun); assertEquals("toLowerCase", method.getName()); =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:56
Часовой пояс: UTC + 5