Можем ли мы получить имя метода, используя java.util.function?
Я пытался сделать:
public class HelloWorld {
public static void main(String... args){
final String string = "a";
final Supplier<?> supplier = string::isEmpty;
System.out.println(supplier);
}
}
я получаю:
HelloWorld$$Lambda$1/[email protected]
Я хотел бы получить строку isEmpty
. Как я могу это сделать?
ОБНОВЛЕНИЕ: код метода, который я создал, это:
public class EnumHelper {
private final static String values = "values";
private final static String errorTpl = "Can't find element with value '{0}' for enum {1} using getter {2}()";
public static <T extends Enum<T>, U> T getFromValue(T enumT, U value, String getter) {
@SuppressWarnings("unchecked")
final T[] elements = (T[]) ReflectionHelper.callMethod(enumT, values);
for (final T enm: elements) {
if (ReflectionHelper.callMethod(enm, getter).equals(value)) {
return enm;
}
}
throw new InvalidParameterException(MessageFormat.format(errorTpl, value, enumT, getter));
}
}
Проблема в том, что я не могу передать в качестве параметра T :: getValue, поскольку getValue не является статическим. И я не могу передать someEnumElem :: getValue, так как get()
вернет значение этого элемента. Я мог бы использовать внутри цикла for:
Supplier<U> getterSupllier = enm:getValue;
if (getterSupllier.get().equals(value)) {
[...]
}
но таким образом getValue
исправлен, я не могу передать его в качестве параметра. Я мог бы использовать какую-нибудь стороннюю библиотеку для создания eval()
, но я действительно не хочу открывать эту вазу Пандоры: D
ОБНОВЛЕНИЕ 2: Function
работает без методов параметров, но только в Java
11. К сожалению, я застрял с Java
8.
Ответы
Ответ 1
string::isEmpty
будет построен методом LambdaMetafactory.metafactory
, который имеет implMethod
среди своих параметров.
final String methodName = implMethod.internalMemberName().getName();
вернет имя метода (здесь "isEmpty"
), если у нас будет доступ к аргументам, переданным этому фабричному методу, и, в частности, implMethod
. Аргументы, сгенерированные вызовами из JVM, которые предоставляют очень специфическую информацию для API java.lang.invoke
.
Например, чтобы инициализировать DirectMethodHandle
, который представляет string::isEmpty
, JVM вызовет следующий метод.
/**
* The JVM is resolving a CONSTANT_MethodHandle CP entry. And it wants our help.
* It will make an up-call to this method. (Do not change the name or signature.)
* The type argument is a Class for field requests and a MethodType for non-fields.
* <p>
* Recent versions of the JVM may also pass a resolved MemberName for the type.
* In that case, the name is ignored and may be null.
*/
static MethodHandle linkMethodHandleConstant(Class<?> callerClass, int refKind,
Class<?> defc, String name, Object type)
Это name
(именно то, что вы просили) будет помещено туда JVM, и у нас нет средств для доступа к нему. Сейчас.
Читать:
Ответ 2
Короче говоря: нет.
После использования ссылки на метод у вас будет реализация запрошенного вами функционального интерфейса (в данном случае Supplier<?>
), но в основном вся специфика этого объекта неопределена (или определена реализацией, если быть точным).
В спецификации ничего не говорится о том, что это отдельный объект, каким должен быть его toString()
или что еще можно с ним сделать. Это Supplier<?>
и, по сути, больше ничего.
То же самое относится и к лямбда-выражениям.
Так что, если вы использовали
final Supplier<?> supplier = () -> string.isEmpty();
Supplier
сделал бы то же самое, и вы также не могли вернуться к "коду" лямбды.
Ответ 3
Короче говоря: нет, это невозможно.
Обходной путь, который я использовал, - это создание методов, которые обертывают экземпляры java.util.functional
в "именованные" версии.
import java.util.Objects;
import java.util.function.Supplier;
public class Named {
public static void main(String[] args) {
String string = "a";
Supplier<?> supplier = string::isEmpty;
Supplier<?> named = named("isEmpty", supplier);
System.out.println(named);
}
static <T> Supplier<T> named(String name, Supplier<? extends T> delegate) {
Objects.requireNonNull(delegate, "The delegate may not be null");
return new Supplier<T>() {
@Override
public T get() {
return delegate.get();
}
@Override
public String toString() {
return name;
}
};
}
}
Конечно, это не имеет смысла для всех случаев применения. Наиболее важно, что он не позволяет вам "выводить" вещи, такие как имя метода Supplier
, задним числом, когда вы просто получаете его, например, в качестве аргумента метода. Причина этого более техническая, но самое главное: поставщик не обязательно должен быть ссылкой на метод.
Но когда вы управляете созданием Supplier
, изменение string::isEmpty
на Named.named("isEmpty", string::isEmpty)
может быть разумным способом.
На самом деле, я делал это так систематически для всех функциональных типов, что даже думал о том, чтобы вставить это в какую-то общедоступную (GitHub/Maven) библиотеку...
Ответ 4
Странно, что вы спрашиваете об обратном тому, что вам действительно нужно.
У вас есть метод, который получает строку и хочет выполнить метод с этим именем, но по какой-то неизвестной причине вы запрашиваете обратное, чтобы получить имя метода от существующего поставщика.
И уже написано в комментарии, прежде чем узнавать реальный код, вы можете решить реальную проблему, заменив параметр String getter
на Function<T,U> getter
.
Вам не нужен инструмент отражения здесь:
public class EnumHelper {
private final static String errorTpl
= "Can't find element with value '{0}' for enum {1} using getter {2}()";
public static <T extends Enum<T>, U> T getFromValue(
T enumT, U value, Function<? super T, ?> getter) {
final T[] elements = enumT.getDeclaringClass().getEnumConstants();
for (final T enm: elements) {
if(getter.apply(enm).equals(value)) {
return enm;
}
}
throw new IllegalArgumentException(
MessageFormat.format(errorTpl, value, enumT, getter));
}
}
Метод получения Function
может быть реализован через ссылку на метод, например,
ChronoUnit f = EnumHelper.getFromValue(
ChronoUnit.FOREVER, Duration.ofMinutes(60), ChronoUnit::getDuration);
System.out.println(f);
Я сделал сигнатуру параметра функции более щедрой по сравнению с Function<T,U>
, чтобы повысить гибкость в отношении существующих функций, например
Function<Object,Object> func = Object::toString;
ChronoUnit f1 = EnumHelper.getFromValue(ChronoUnit.FOREVER, "Years", func);
System.out.println(f1.name());
Если печать значимых имен в ошибочном регистре действительно важна, просто добавьте параметр имени только для отчетов:
public static <T extends Enum<T>, U> T getFromValue(
T enumT, U value, Function<? super T, ?> getter, String getterName) {
final T[] elements = enumT.getDeclaringClass().getEnumConstants();
for (final T enm: elements) {
if(getter.apply(enm).equals(value)) {
return enm;
}
}
throw new IllegalArgumentException(
MessageFormat.format(errorTpl, value, enumT, getterName));
}
называться как
ChronoUnit f = EnumHelper.getFromValue(
ChronoUnit.FOREVER, Duration.ofMinutes(60), ChronoUnit::getDuration, "getDuration");
Это все же лучше, чем использовать Reflection для обычных операций...