Чи є спосіб порівняти лямбди?


78

Скажімо, у мене є Перелік об’єктів, які були визначені за допомогою лямбда-виразів (закриття). Чи є спосіб їх перевірити, щоб їх можна було порівняти?

Код, який мене найбільше цікавить, це

    List<Strategy> strategies = getStrategies();
    Strategy a = (Strategy) this::a;
    if (strategies.contains(a)) { // ...

Повний код

import java.util.Arrays;
import java.util.List;

public class ClosureEqualsMain {
    interface Strategy {
        void invoke(/*args*/);
        default boolean equals(Object o) { // doesn't compile
            return Closures.equals(this, o);
        }
    }

    public void a() { }
    public void b() { }
    public void c() { }

    public List<Strategy> getStrategies() {
        return Arrays.asList(this::a, this::b, this::c);
    }

    private void testStrategies() {
        List<Strategy> strategies = getStrategies();
        System.out.println(strategies);
        Strategy a = (Strategy) this::a;
        // prints false
        System.out.println("strategies.contains(this::a) is " + strategies.contains(a));
    }

    public static void main(String... ignored) {
        new ClosureEqualsMain().testStrategies();
    }

    enum Closures {;
        public static <Closure> boolean equals(Closure c1, Closure c2) {
            // This doesn't compare the contents 
            // like others immutables e.g. String
            return c1.equals(c2);
        }

        public static <Closure> int hashCode(Closure c) {
            return // a hashCode which can detect duplicates for a Set<Strategy>
        }

        public static <Closure> String asString(Closure c) {
            return // something better than Object.toString();
        }
    }    

    public String toString() {
        return "my-ClosureEqualsMain";
    }
}

Здається, єдиним рішенням є визначення кожної лямбди як поля та використання лише цих полів. Якщо ви хочете роздрукувати викликаний метод, вам краще використовувати Method. Чи є кращий спосіб із лямбда-виразами?

Крім того, чи можна надрукувати лямбду і отримати щось зрозуміле для людини? Якщо ви друкуєте this::aзамість

ClosureEqualsMain$$Lambda$1/821270929@3f99bd52

отримати щось на зразок

ClosureEqualsMain.a()

або навіть використовувати this.toStringі метод.

my-ClosureEqualsMain.a();

1
Ви можете визначити toString, дорівнює і hashhCode методи в рамках закриття.
Анкіт Залані,

@AnkitZalani Чи можете ви навести приклад, який компілює?
Пітер Лорі

@PeterLawrey, Оскільки toStringце визначено далі Object, я думаю, ви можете визначити інтерфейс, який забезпечує реалізацію за замовчуванням, toStringне порушуючи вимоги до одного методу, щоб інтерфейси були функціональними. Однак я цього не перевіряв.
Майк Семюел

6
@MikeSamuel Це неправильно. Класи не успадковують метод об'єктів за замовчуванням, оголошений в інтерфейсах; див. stackoverflow.com/questions/24016962/… для пояснення.
Brian Goetz

@BrianGoetz, Дякую за вказівник.
Mike Samuel

Відповіді:


82

Це питання можна трактувати щодо специфікації або реалізації. Очевидно, реалізації можуть змінитися, але ви можете переписати свій код, коли це станеться, тому я відповім в обох випадках.

Це також залежить від того, що ви хочете зробити. Ви хочете оптимізувати, чи ви шукаєте запевнених гарантій того, що два екземпляри є (або не є) однаковою функцією? (Якщо останнє, ви зіткнетесь з розбіжностями з обчислювальною фізикою, оскільки навіть такі прості завдання, як запитання, чи обчислюють дві функції одне і те ж, не можна вирішити.)

З точки зору специфікації специфікація мови обіцяє лише те, що результат оцінки (не викликання) лямбда-виразу є екземпляром класу, що реалізує цільовий функціональний інтерфейс. Це не дає обіцянок щодо ідентичності або ступеня згладжування результату. Це задумано, щоб надати реалізаціям максимальну гнучкість, щоб запропонувати кращу продуктивність (це те, як лямбди можуть бути швидшими за внутрішні класи; ми не прив’язані до обмеження "повинен створити унікальний екземпляр", яким є внутрішні класи.)

Отже, в основному, специфікація не дає вам багато, за винятком того, що очевидно, що дві лямбди, які мають посилання-рівні (==), збираються обчислити одну і ту ж функцію.

З точки зору реалізації ви можете зробити висновок трохи більше. Існує (на даний момент, може змінитися) взаємозв'язок 1: 1 між класи синтетичних, які реалізують лямбди, і сайтами захоплення в програмі. Отже, два окремі біти коду, які фіксують "x -> x + 1", цілком можуть бути зіставлені з різними класами. Але якщо ви оцінюєте ту саму лямбду на одному і тому ж місці захоплення, і ця лямбда не захоплює, ви отримаєте той самий екземпляр, який можна порівняти з еталонною рівністю.

Якщо ваші лямбди можна серіалізувати, вони відмовляться від свого стану легше, в обмін на жертву певною продуктивністю та безпекою (без безкоштовного обіду).

Одним із напрямків, де може бути доцільним змінити визначення рівності, є посилання на методи, оскільки це дозволить їх використовувати як слухачів та належним чином реєструвати. Це розглядається.

Я думаю, до чого ви намагаєтесь дістатись: якщо дві лямбди перетворені в один і той же функціональний інтерфейс, представлені однаковою функцією поведінки та мають однакові захоплені аргументи, вони однакові

На жаль, це одночасно важко зробити (для несеріалізованих лямбд неможливо отримати зовсім їхні компоненти), і недостатньо (оскільки два окремо скомпільовані файли можуть перетворити одну і ту ж лямбду на той самий функціональний тип інтерфейсу, і ви не змогла б сказати.)

ЕГ обговорив, чи виставляти достатньо інформації, щоб мати можливість приймати ці судження, а також обговорював, чи слід лямбдам застосовувати більш вибірковий equals/ hashCodeабо більш описовий до String. Висновок полягав у тому, що ми не готові платити нічого за вартість роботи, щоб зробити цю інформацію доступною для абонента (невдалий компроміс, караючи 99,99% користувачів за те, що приносить користь .01%).

Остаточного висновку щодо toStringне було зроблено, але залишено відкритим для перегляду в майбутньому. Однак з цього питання з обох сторін було кілька вагомих аргументів; це не шлем.


2
+1 Хоча я розумію, що підтримка ==рівності загалом важко вирішити, я міг би подумати, що існують прості випадки, коли компілятор, якби не JVM міг визнати, що this::aв одному рядку те саме, що і в this::aіншому рядку. Насправді для мене все ще не очевидно, що ви отримуєте, надаючи кожному сайту виклику власну реалізацію. Можливо, їх можна оптимізувати по-різному, але я б подумав, що це може зробити inlining. ??
Пітер Лорі

1
Подобається ArrayіArrays утилітні клаузули для масивів, оскільки вони не могли отримати гідну рівність, hashCode або toString, я можу уявити Closuresклас корисності одного дня. Оскільки існують мови, де ви можете друкувати та створювати масиви та бачити їх зміст, я думаю, є мови, де ви можете друкувати закриття та отримати деяке розуміння того, що робить закриття. (Можливо, рядок коду кращий, але для
когось

6
Ми дослідили низку можливих реалізацій, включаючи одну, в якій проксі-класи були спільними між сайтами викликів. Той, з яким ми пішли зараз (одна велика перевага підходу "метафабрики" полягає в тому, що це можна змінити без перекомпіляції файлів класів користувачів), був найпростішим і найефективнішим. Ми будемо продовжувати відстежувати відносну ефективність між варіантами в міру розвитку ВМ, і коли один з інших стане швидшим, ми перейдемо.
Brian Goetz

2
Як зауваження, навіть MethodHandles, що використовуються в базовому двійковому інтерфейсі , не гарантовано будуть канонічними . Отже, метафабрика не може визначити, чи націлені два посилання на один метод, просто порівнявши посилання. Йому довелося провести глибший аналіз, якщо він намагався повернути ту саму реалізацію для еквівалентних посилань на методи.
Holger

4
Без змін для Java 9.
Брайан Гетц,

7

Для порівняння labmdas я зазвичай дозволяю інтерфейсу розширюватися, Serializableа потім порівнюю серіалізовані байти. Не дуже приємно, але працює в більшості випадків.


Те саме стосується і hashCode лямбдас, чи не так? Я маю на увазі серіалізацію лямбда-сигналу до байтового масиву (за допомогою ByteArrayOutputStream та ObjectOutputStream) та хешування його за допомогою Arrays.hash (...).
mmirwaldt

6

Я не бачу можливості отримати цю інформацію із самого закриття. Закриття не забезпечує стан.

Але ви можете використовувати Java-Reflection, якщо хочете перевірити та порівняти методи. Звичайно, це не дуже гарне рішення, через продуктивність та винятки, які слід вловити. Але таким чином ви отримуєте ці метаінформації.


1
Відбиття +1 дозволяє мені отримати thisяк, arg$1але не порівняти викликаний метод. Можливо, мені доведеться прочитати байт-код, щоб перевірити, чи однаковий він.
Пітер Лорі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.