Gibt es eine Möglichkeit, Lambdas zu vergleichen?

78

Angenommen, ich habe eine Liste von Objekten, die mit Lambda-Ausdrücken (Abschlüssen) definiert wurden. Gibt es eine Möglichkeit, sie zu inspizieren, damit sie verglichen werden können?

Der Code, der mich am meisten interessiert, ist

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

Der vollständige Code lautet

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";
    }
}

Es scheint, dass die einzige Lösung darin besteht, jedes Lambda als Feld zu definieren und nur diese Felder zu verwenden. Wenn Sie die aufgerufene Methode ausdrucken möchten, sollten Sie sie besser verwenden Method. Gibt es einen besseren Weg mit Lambda-Ausdrücken?

Ist es auch möglich, ein Lambda zu drucken und etwas menschlich Lesbares zu bekommen? Wenn Sie drucken , this::astatt

ClosureEqualsMain$$Lambda$1/821270929@3f99bd52

so etwas bekommen

ClosureEqualsMain.a()

oder sogar verwenden this.toStringund die Methode.

my-ClosureEqualsMain.a();
Peter Lawrey
quelle
1
Sie können toString-, equals- und hashhCode-Methoden innerhalb von Closure definieren.
Ankit Zalani
@AnkitZalani Können Sie ein Beispiel geben, das kompiliert wird?
Peter Lawrey
@PeterLawrey, Da toStringdefiniert ist Object, können Sie eine Schnittstelle definieren, die eine Standardimplementierung von bietet, toStringohne die Anforderung einer einzelnen Methode zu verletzen, damit Schnittstellen funktionsfähig sind. Ich habe das allerdings nicht überprüft.
Mike Samuel
6
@ MikeSamuel Das ist falsch. Klassen erben keine Standard-Objektmethoden, die in Schnittstellen deklariert sind. Erläuterungen finden Sie unter stackoverflow.com/questions/24016962/… .
Brian Goetz
@BrianGoetz, Danke für den Zeiger.
Mike Samuel

Antworten:

82

Diese Frage könnte in Bezug auf die Spezifikation oder die Implementierung interpretiert werden. Natürlich könnten sich die Implementierungen ändern, aber Sie könnten bereit sein, Ihren Code in diesem Fall neu zu schreiben, also werde ich auf beide antworten.

Es hängt auch davon ab, was Sie tun möchten. Möchten Sie optimieren oder suchen Sie nach Garantien, dass zwei Instanzen dieselbe Funktion haben (oder nicht)? (Wenn letzteres der Fall ist, werden Sie im Widerspruch zur Computerphysik stehen, da selbst Probleme, die so einfach sind wie die Frage, ob zwei Funktionen dasselbe berechnen, unentscheidbar sind.)

Aus Sicht der Spezifikation verspricht die Sprachspezifikation nur, dass das Ergebnis der Auswertung (nicht des Aufrufs) eines Lambda-Ausdrucks eine Instanz einer Klasse ist, die die funktionale Zielschnittstelle implementiert. Es gibt keine Zusagen über die Identität oder den Grad des Aliasing des Ergebnisses. Dies ist beabsichtigt, um Implementierungen maximale Flexibilität zu bieten, um eine bessere Leistung zu bieten (auf diese Weise können Lambdas schneller sein als innere Klassen; wir sind nicht an die Einschränkung gebunden, dass innere Klassen eine eindeutige Instanz erstellen müssen).

Im Grunde genommen gibt Ihnen die Spezifikation nicht viel, außer dass zwei Lambdas, die referenzgleich (==) sind, dieselbe Funktion berechnen werden.

Aus Sicht der Implementierung können Sie etwas mehr schließen. Zwischen den synthetischen Klassen, die Lambdas implementieren, und den Erfassungsorten im Programm besteht (kann sich derzeit ändern) eine 1: 1-Beziehung. Daher können zwei separate Codebits, die "x -> x + 1" erfassen, durchaus verschiedenen Klassen zugeordnet werden. Wenn Sie jedoch dasselbe Lambda an derselben Erfassungsstelle auswerten und dieses Lambda nicht erfasst, erhalten Sie dieselbe Instanz, die mit der Referenzgleichheit verglichen werden kann.

Wenn Ihre Lambdas serialisierbar sind, geben sie ihren Zustand leichter auf, als Gegenleistung für Leistungseinbußen und Sicherheit (kein kostenloses Mittagessen).

Ein Bereich, in dem es möglicherweise sinnvoll ist, die Definition der Gleichheit zu optimieren, sind Methodenreferenzen, da diese es ihnen ermöglichen würden, als Zuhörer verwendet zu werden und ordnungsgemäß nicht registriert zu werden. Dies wird derzeit geprüft.

Ich denke, Sie versuchen zu erreichen: Wenn zwei Lambdas in dieselbe Funktionsschnittstelle konvertiert werden, durch dieselbe Verhaltensfunktion dargestellt werden und identische erfasste Argumente haben, sind sie gleich

Leider ist dies sowohl schwierig (für nicht serialisierbare Lambdas können Sie überhaupt nicht alle Komponenten davon erhalten) als auch nicht ausreichend (da zwei separat kompilierte Dateien dasselbe Lambda in denselben funktionalen Schnittstellentyp konvertieren könnten, und Sie würde es nicht sagen können.)

Die EG erörterte, ob genügend Informationen veröffentlicht werden sollten, um diese Urteile fällen zu können, und erörterte, ob Lambdas selektiver equals/ hashCodeoder beschreibender für String implementieren sollten . Die Schlussfolgerung war, dass wir nicht bereit waren, Leistungskosten zu zahlen, um diese Informationen dem Anrufer zur Verfügung zu stellen (schlechter Kompromiss, der 99,99% der Benutzer für etwas bestraft, das 0,01% zugute kommt).

Eine endgültige Schlussfolgerung toStringwurde nicht erreicht, sondern offen gelassen, um in Zukunft erneut geprüft zu werden. Zu diesem Thema wurden jedoch auf beiden Seiten einige gute Argumente vorgebracht. Dies ist kein Slam-Dunk.

Brian Goetz
quelle
2
+1 Obwohl ich verstehe, dass die Unterstützung der ==Gleichstellung im Allgemeinen ein schwer zu lösendes Problem ist, hätte ich gedacht, dass es einfache Fälle geben würde, in denen der Compiler, wenn nicht die JVM erkennen könnte, dass this::ain einer Zeile dasselbe ist wie this::ain einer anderen Zeile. Tatsächlich ist mir immer noch nicht klar, was Sie gewinnen, wenn Sie jeder Anrufstelle eine eigene Implementierung geben. Vielleicht können sie anders optimiert werden, aber ich hätte gedacht, dass Inlining dies tun könnte.
Peter Lawrey
1
Wie Arrayund ArraysUtility-Klauseln für Arrays, da sie keine anständigen Equals, HashCode oder toString erhalten konnten, kann ich mir Closureseines Tages eine Utility-Klasse vorstellen . Da es Sprachen gibt, in denen Sie drucken und anordnen und deren Inhalt anzeigen können, stelle ich mir vor, dass es Sprachen gibt, in denen Sie Verschlüsse drucken und einen Einblick in die Funktionsweise des Verschlusses erhalten können. (Möglicherweise ist eine Zeichenfolge des Codes besser, aber für einige unbefriedigend)
Peter Lawrey
6
Wir haben eine Reihe möglicher Implementierungen untersucht, darunter eine, bei der die Proxy-Klassen von mehreren Call-Sites gemeinsam genutzt wurden. Das, mit dem wir uns vorerst befasst haben (ein großer Vorteil des "Metafactory" -Ansatzes besteht darin, dass dies geändert werden kann, ohne dass Benutzerklassendateien neu kompiliert werden müssen), war das einfachste und leistungsstärkste. Wir werden weiterhin die relative Leistung zwischen den Optionen überwachen, während sich die VM weiterentwickelt, und wenn eine der anderen schneller ist, werden wir wechseln.
Brian Goetz
2
Nebenbei bemerkt, es wird nicht garantiert, dass selbst die MethodHandlein der zugrunde liegenden binären Schnittstelle verwendeten s kanonisch sind . Die Meta-Factory kann also nicht erkennen, ob zwei Referenzen auf dieselbe Methode abzielen, indem sie nur die Referenzen vergleicht. Es musste eine eingehendere Analyse durchführen, wenn es versuchte, dieselbe Implementierung für äquivalente Methodenreferenzen zurückzugeben.
Holger
4
Keine Änderungen für Java 9.
Brian Goetz
7

Um labmdas zu vergleichen, lasse ich normalerweise die Schnittstelle erweitern Serializableund vergleiche dann die serialisierten Bytes. Nicht sehr schön, funktioniert aber in den meisten Fällen.

KIC
quelle
Gleiches gilt für den Hashcode von Lambdas, nicht wahr? Ich meine, ein Lambda in ein Byte-Array zu serialisieren (mit Hilfe von ByteArrayOutputStream und ObjectOutputStream) und es durch Arrays.hash (...) zu hashen.
mmirwaldt
6

Ich sehe keine Möglichkeit, diese Informationen aus der Schließung selbst zu erhalten. Die Verschlüsse liefern keinen Zustand.

Sie können jedoch Java-Reflection verwenden, wenn Sie die Methoden überprüfen und vergleichen möchten. Natürlich ist das keine sehr schöne Lösung, wegen der Leistung und der Ausnahmen, die zu fangen sind. Aber auf diese Weise erhalten Sie diese Meta-Informationen.

F. Böller
quelle
1
+1 Reflexion ermöglicht es mir, die aufgerufene Methode zu erhalten this, arg$1aber nicht zu vergleichen. Möglicherweise muss ich den Bytecode lesen, um festzustellen, ob er identisch ist.
Peter Lawrey