Ist es in Java 8 stilistisch besser, Methodenreferenzausdrücke oder Methoden zu verwenden, die eine Implementierung der Funktionsschnittstelle zurückgeben?

11

Java 8 fügte das Konzept der funktionalen Schnittstellen sowie zahlreiche neue Methoden hinzu, mit denen funktionale Schnittstellen verwendet werden können. Instanzen dieser Schnittstellen können mithilfe von Methodenreferenzausdrücken (z. B. SomeClass::someMethod) und Lambda-Ausdrücken (z (x, y) -> x + y. B. ) prägnant erstellt werden .

Ein Kollege und ich haben unterschiedliche Meinungen darüber, wann es am besten ist, das eine oder andere Formular zu verwenden (wobei "am besten" in diesem Fall wirklich auf "am besten lesbar" und "am besten im Einklang mit Standardpraktiken" hinausläuft, da sie im Grunde genommen anders sind Äquivalent). Dies betrifft insbesondere den Fall, dass alle der folgenden Punkte zutreffen:

  • Die betreffende Funktion wird nicht außerhalb eines einzelnen Bereichs verwendet
  • Wenn Sie der Instanz einen Namen geben, wird die Lesbarkeit verbessert (im Gegensatz dazu, dass die Logik einfach genug ist, um auf einen Blick zu sehen, was passiert).
  • Es gibt keine anderen Programmiergründe, warum ein Formular dem anderen vorzuziehen wäre.

Meine derzeitige Meinung zu diesem Thema ist, dass das Hinzufügen einer privaten Methode und das Verweisen auf diese als Methodenreferenz der bessere Ansatz ist. Es sieht so aus, als ob die Funktion so konzipiert wurde, dass sie verwendet werden soll, und es scheint einfacher zu sein, über Methodennamen und Signaturen zu kommunizieren, was vor sich geht (z. B. "boolean isResultInFuture (Result result)" bedeutet eindeutig, dass es einen boolean zurückgibt). Dadurch wird die private Methode auch wiederverwendbarer, wenn eine zukünftige Erweiterung der Klasse dieselbe Prüfung verwenden möchte, jedoch den Wrapper für die funktionale Schnittstelle nicht benötigt.

Mein Kollege bevorzugt eine Methode, die die Instanz der Schnittstelle zurückgibt (z. B. "Predicate resultInFuture ()"). Für mich scheint es nicht ganz so zu sein, wie die Funktion verwendet werden sollte, es fühlt sich etwas klobiger an und es scheint schwieriger zu sein, Absichten wirklich durch Benennung zu kommunizieren.

Um dieses Beispiel konkret zu machen, ist hier derselbe Code, der in den verschiedenen Stilen geschrieben wurde:

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

vs.

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

Gibt es offizielle oder halboffizielle Dokumentationen oder Kommentare dazu, ob ein Ansatz bevorzugter, besser auf die Absichten der Sprachdesigner abgestimmt oder besser lesbar ist? Gibt es, abgesehen von einer offiziellen Quelle, klare Gründe, warum einer der bessere Ansatz wäre?

M. Justin
quelle
1
Lambdas wurden der Sprache aus einem bestimmten Grund hinzugefügt, und es sollte Java nicht verschlimmern.
@Snowman Das versteht sich von selbst. Ich gehe daher davon aus, dass zwischen Ihrem Kommentar und meiner Frage eine implizite Beziehung besteht, die ich nicht ganz verstehe. Was ist der Punkt, den Sie versuchen zu machen?
M. Justin
2
Ich gehe davon aus, dass der Punkt, den er vorbringt, darin besteht, dass Lambdas, wenn sie den Status Quo nicht verbessert hätten, sich nicht die Mühe gemacht hätten, sie einzuführen.
Robert Harvey
2
@ RobertHarvey Sicher. Bis zu diesem Zeitpunkt wurden jedoch sowohl Lambdas als auch Methodenreferenzen gleichzeitig hinzugefügt, sodass der Status Quo zuvor noch nicht erreicht wurde. Auch ohne diesen speziellen Fall gibt es Zeiten, in denen Methodenreferenzen noch besser wären. Zum Beispiel eine bestehende öffentliche Methode (zB Object::toString). Meine Frage ist also mehr, ob es in dieser bestimmten Art von Instanz, die ich hier darlege, besser ist, als ob es Instanzen gibt, in denen eine besser ist als die andere, oder umgekehrt.
M. Justin

Antworten:

8

In Bezug auf die funktionale Programmierung diskutieren Sie und Ihr Kollege einen punktfreien Stil , insbesondere eine eta-Reduzierung . Betrachten Sie die folgenden zwei Aufgaben:

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

Diese sind betrieblich äquivalent, und der erste wird als spitzer Stil bezeichnet, während der zweite als punktfreier Stil bezeichnet wird. Der Begriff "Punkt" bezieht sich auf ein benanntes Funktionsargument (das aus der Topologie stammt), das im letzteren Fall fehlt.

Im Allgemeinen ist für alle Funktionen F ein Wrapper-Lambda, das alle angegebenen Argumente aufnimmt und sie unverändert an F übergibt und dann das Ergebnis von F unverändert zurückgibt, identisch mit F selbst. Das Entfernen des Lambda und die direkte Verwendung von F (vom punktuellen zum punktfreien Stil oben) wird als eta-Reduktion bezeichnet , ein Begriff, der aus dem Lambda-Kalkül stammt .

Es gibt punktfreie Ausdrücke, die nicht durch Eta-Reduktion erzeugt werden. Das klassischste Beispiel hierfür ist die Funktionszusammensetzung . Bei einer Funktion von Typ A bis Typ B und einer Funktion von Typ B bis Typ C können wir sie zu einer Funktion von Typ A bis Typ C zusammensetzen:

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

Nehmen wir nun an, wir haben eine Methode Result deriveResult(Foo foo). Da ein Prädikat eine Funktion ist, können wir ein neues Prädikat erstellen, das zuerst deriveResultdas abgeleitete Ergebnis aufruft und dann testet:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

Obwohl die Implementierung von composepointful style mit Lambdas verwendet, ist die Verwendung von compose zum Definieren isFoosResultInFuturepunktfrei, da keine Argumente erwähnt werden müssen.

Punktfreie Programmierung wird auch als implizite Programmierung bezeichnet und kann ein leistungsstarkes Werkzeug sein, um die Lesbarkeit zu verbessern, indem sinnlose (Wortspiel beabsichtigte) Details aus einer Funktionsdefinition entfernt werden. Java 8 unterstützt die punktfreie Programmierung nicht annähernd so gründlich wie funktionalere Sprachen wie Haskell, aber eine gute Faustregel ist, immer eine Eta-Reduktion durchzuführen. Es ist nicht erforderlich, Lambdas zu verwenden, die sich nicht von den Funktionen unterscheiden, die sie einschließen.

Jack
quelle
1
Ich denke, mein Kollege und ich diskutieren mehr über diese beiden Aufgaben: Predicate<Result> pred = this::isResultInFuture;und Predicate<Result> pred = resultInFuture();wo resultInFuture () a zurückgibt Predicate<Result>. Dies ist zwar eine gute Erklärung dafür, wann eine Methodenreferenz im Vergleich zu einem Lambda-Ausdruck verwendet werden soll, deckt diesen dritten Fall jedoch nicht vollständig ab.
M. Justin
4
@ M.Justin: Die resultInFuture();Zuordnung ist identisch mit der von mir vorgestellten Lambda-Zuordnung, außer in meinem Fall ist Ihre resultInFuture()Methode inline. Dieser Ansatz hat also zwei Indirektionsebenen (von denen keine etwas bewirkt) - die Lambda-Konstruktionsmethode und das Lambda selbst. Noch schlimmer!
Jack
5

Keine der aktuellen Antworten befasst sich tatsächlich mit dem Kern der Frage, ob die Klasse eine private boolean isResultInFuture(Result result)Methode oder eine private Predicate<Result> resultInFuture()Methode haben soll. Während sie weitgehend gleichwertig sind, gibt es jeweils Vor- und Nachteile.

Der erste Ansatz ist natürlicher, wenn Sie die Methode zusätzlich zum Erstellen einer Methodenreferenz direkt aufrufen. Wenn Sie diese Methode beispielsweise einem Unit-Test unterziehen, ist es möglicherweise einfacher, den ersten Ansatz zu verwenden. Ein weiterer Vorteil des ersten Ansatzes besteht darin, dass sich die Methode nicht darum kümmern muss, wie sie verwendet wird, sondern sie von ihrer Aufrufstelle entkoppelt. Wenn Sie die Klasse später so ändern, dass Sie die Methode direkt aufrufen, zahlt sich diese Entkopplung auf sehr geringe Weise aus.

Der erste Ansatz ist auch überlegen, wenn Sie diese Methode an verschiedene funktionale Schnittstellen oder verschiedene Schnittstellenverbindungen anpassen möchten. Beispielsweise möchten Sie möglicherweise, dass die Methodenreferenz vom Typ ist Predicate<Result> & Serializable, was beim zweiten Ansatz nicht die Flexibilität bietet, die Sie erreichen können.

Andererseits ist der zweite Ansatz natürlicher, wenn Sie Operationen höherer Ordnung für das Prädikat ausführen möchten. Das Prädikat enthält mehrere Methoden, die nicht direkt in einer Methodenreferenz aufgerufen werden können. Wenn Sie diese Methoden verwenden möchten, ist der zweite Ansatz überlegen.

Letztendlich ist die Entscheidung subjektiv. Wenn Sie jedoch nicht beabsichtigen, Methoden für Predicate aufzurufen, würde ich den ersten Ansatz bevorzugen.

Stellen Sie Monica wieder her
quelle
Operationen höherer Ordnung für das Prädikat sind weiterhin verfügbar, indem die Methodenreferenz gewirkt wird:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
Jack
4

Ich schreibe keinen Java-Code mehr, aber ich schreibe beruflich in einer funktionalen Sprache und unterstütze auch andere Teams, die funktionale Programmierung lernen. Lambdas haben einen empfindlichen Sweet Spot der Lesbarkeit. Der allgemein akzeptierte Stil für sie ist, dass Sie sie verwenden, wenn Sie sie inline können. Wenn Sie jedoch das Bedürfnis haben, sie einer Variablen zur späteren Verwendung zuzuweisen, sollten Sie wahrscheinlich nur eine Methode definieren.

Ja, die Leute weisen Variablen in Tutorials ständig Lambdas zu. Dies sind nur Tutorials, die Ihnen ein gründliches Verständnis ihrer Funktionsweise vermitteln. Sie werden in der Praxis nur auf relativ seltene Weise verwendet (z. B. bei der Auswahl zwischen zwei Lambdas mit einem if-Ausdruck).

Das heißt, wenn Ihr Lambda kurz genug ist, um nach dem Inlining leicht lesbar zu sein, wie im Beispiel aus dem Kommentar von @JimmyJames, dann machen Sie es:

people = sort(people, (a,b) -> b.age() - a.age());

Vergleichen Sie dies mit der Lesbarkeit, wenn Sie versuchen, Ihr Beispiel-Lambda zu integrieren:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

Für Ihr spezielles Beispiel würde ich es persönlich bei einer Pull-Anfrage markieren und Sie bitten, es aufgrund seiner Länge auf eine benannte Methode zu ziehen. Java ist eine ärgerlich ausführliche Sprache, daher unterscheiden sich die eigenen sprachspezifischen Praktiken möglicherweise mit der Zeit von anderen Sprachen. Halten Sie Ihre Lambdas jedoch bis dahin kurz und inline.

Karl Bielefeldt
quelle
2
Betreff: Ausführlichkeit - Die neuen funktionalen Ergänzungen verringern zwar nicht viel Ausführlichkeit, ermöglichen jedoch Möglichkeiten, dies zu tun. Zum Beispiel können Sie definieren: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)mit der offensichtlichen Implementierung und dann können Sie Code wie people = sort(people, (a,b) -> b.age() - a.age());folgt schreiben: Dies ist eine große Verbesserung IMO.
JimmyJames
Das ist ein großartiges Beispiel für das, worüber ich gesprochen habe, @JimmyJames. Wenn Lambdas kurz sind und eingefügt werden können, sind sie eine große Verbesserung. Wenn sie so lang werden, dass Sie sie trennen und ihnen einen Namen geben möchten, ist es besser, nur eine Methode zu definieren. Vielen Dank, dass Sie mir geholfen haben zu verstehen, wie meine Antwort falsch ausgelegt wurde.
Karl Bielefeldt
Ich denke, deine Antwort war in Ordnung. Ich bin mir nicht sicher, warum es abgelehnt wird.
JimmyJames