Я столкнулся с проблемой как получить из 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"));// successassertThat(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
:
@FunctionalInterfacepublic interface ThrowingFunction<T, R> extends java.io.Serializable { R apply(T t) throws Exception;}
Как следует из javadoc класса, чтобы получить объект
SerializedLambda
, следует вызвать приватный
writeReplace
на лямбда-объекте:
@Nullableprivate 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); }}
Остается только найти подходящий метод в интерфейсе, чтобы вернуть правильный результат:
@Nullablepublic 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()
.
@Nullablepublic 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-подобные).
Единственное неудобство для каждого вида функций придется создать
сериализуемое представление, например:
@FunctionalInterfacepublic 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());