Можем ли мы получить имя метода, используя 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 для обычных операций...