Lambda-Ausdrücke und funktionale Schnittstellen: Tipps und Best Practices

Lambda-Ausdrücke und funktionale Schnittstellen: Tipps und bewährte Methoden

1. Überblick

Nachdem Java 8 eine breite Anwendung erreicht hat, tauchen für einige seiner Hauptmerkmale Muster und Best Practices auf. In diesem Tutorial werden funktionale Interfaces und Lambda-Ausdrücke genauer betrachtet.

Weitere Lektüre:

Warum müssen in Lambdas verwendete lokale Variablen endgültig oder effektiv endgültig sein?

Erfahren Sie, warum Java erfordert, dass lokale Variablen bei der Verwendung in einem Lambda endgültig sind.

Read more

Java 8 - Leistungsstarker Vergleich mit Lambdas

Elegante Sortierung in Java 8 - Lambda-Ausdrücke gehen direkt an syntaktischem Zucker vorbei und bringen leistungsstarke funktionale Semantik in Java.

Read more

2. Bevorzugen Sie Standardfunktionsschnittstellen

Funktionale Schnittstellen, die im Paketjava.util.functionenthalten sind, erfüllen die Anforderungen der meisten Entwickler bei der Bereitstellung von Zieltypen für Lambda-Ausdrücke und Methodenreferenzen. Jede dieser Schnittstellen ist allgemein und abstrakt, sodass sie sich leicht an nahezu jeden Lambda-Ausdruck anpassen lassen. Entwickler sollten dieses Paket untersuchen, bevor sie neue funktionale Schnittstellen erstellen.

Betrachten Sie eine SchnittstelleFoo:

@FunctionalInterface
public interface Foo {
    String method(String string);
}

und eine Methodeadd() in einigen KlassenUseFoo, die diese Schnittstelle als Parameter verwendet:

public String add(String string, Foo foo) {
    return foo.method(string);
}

Um es auszuführen, würden Sie schreiben:

Foo foo = parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", foo);

Wenn Sie genauer hinschauen, werden Sie feststellen, dassFoo nichts anderes als eine Funktion ist, die ein Argument akzeptiert und ein Ergebnis liefert. Java 8 bietet eine solche Schnittstelle bereits inFunction<T,R> aus dem Paketjava.util.function.

Jetzt können wir die SchnittstelleFoo vollständig entfernen und unseren Code ändern in:

public String add(String string, Function fn) {
    return fn.apply(string);
}

Um dies auszuführen, können wir schreiben:

Function fn =
  parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", fn);

3. Verwenden Sie die Annotation@FunctionalInterface

Kommentieren Sie Ihre Funktionsschnittstellen mit@FunctionalInterface.. Zunächst scheint diese Annotation nutzlos zu sein. Auch ohne sie wird Ihre Schnittstelle so lange als funktionsfähig behandelt, wie es nur eine abstrakte Methode gibt.

Stellen Sie sich ein großes Projekt mit mehreren Schnittstellen vor - es ist schwierig, alles manuell zu steuern. Eine Schnittstelle, die funktionell gestaltet wurde, konnte versehentlich durch Hinzufügen anderer abstrakter Methoden geändert werden, wodurch sie als funktionelle Schnittstelle unbrauchbar wurde.

Bei Verwendung der Annotation@FunctionalInterfacelöst der Compiler jedoch einen Fehler aus, wenn versucht wird, die vordefinierte Struktur einer Funktionsschnittstelle zu beschädigen. Es ist auch ein sehr nützliches Werkzeug, um Ihre Anwendungsarchitektur für andere Entwickler verständlicher zu machen.

Also benutze dies:

@FunctionalInterface
public interface Foo {
    String method();
}

statt nur:

public interface Foo {
    String method();
}

4. Verwenden Sie Standardmethoden in Funktionsschnittstellen nicht zu häufig

Sie können der funktionalen Schnittstelle problemlos Standardmethoden hinzufügen. Dies ist für den Funktionsschnittstellenvertrag akzeptabel, solange es nur eine abstrakte Methodendeklaration gibt:

@FunctionalInterface
public interface Foo {
    String method();
    default void defaultMethod() {}
}

Funktionsschnittstellen können durch andere Funktionsschnittstellen erweitert werden, wenn ihre abstrakten Methoden dieselbe Signatur haben. Zum Beispiel:

@FunctionalInterface
public interface FooExtended extends Baz, Bar {}

@FunctionalInterface
public interface Baz {
    String method();
    default void defaultBaz() {}
}

@FunctionalInterface
public interface Bar {
    String method();
    default void defaultBar() {}
}

Genau wie bei regulären Schnittstellen kann es problematisch sein, verschiedene funktionale Schnittstellen mit derselben Standardmethode zu erweitern. Angenommen, die SchnittstellenBar undBaz haben beide eine StandardmethodedefaultCommon().. In diesem Fall wird ein Fehler bei der Kompilierung angezeigt:

interface Foo inherits unrelated defaults for defaultCommon() from types Baz and Bar...

Um dies zu beheben, sollte die MethodedefaultCommon()in der SchnittstelleFooüberschrieben werden. Sie können diese Methode natürlich auch benutzerdefiniert implementieren. Wenn Sie jedoch eine der Implementierungen der übergeordneten Schnittstellen verwenden möchten (z. B. über dieBaz-Schnittstelle), fügen Sie dem Hauptteil derdefaultCommon()-Methode die folgende Codezeile hinzu:

Baz.super.defaultCommon();

Aber sei vorsichtig. Adding too many default methods to the interface is not a very good architectural decision. Dies sollte als Kompromiss angesehen werden, der nur bei Bedarf zum Aktualisieren vorhandener Schnittstellen verwendet werden kann, ohne die Abwärtskompatibilität zu beeinträchtigen.

5. Instanziieren Sie funktionale Schnittstellen mit Lambda-Ausdrücken

Mit dem Compiler können Sie eine innere Klasse verwenden, um eine funktionale Schnittstelle zu instanziieren. Dies kann jedoch zu sehr ausführlichem Code führen. Sie sollten Lambda-Ausdrücke bevorzugen:

Foo foo = parameter -> parameter + " from Foo";

über eine innere Klasse:

Foo fooByIC = new Foo() {
    @Override
    public String method(String string) {
        return string + " from Foo";
    }
};

The lambda expression approach can be used for any suitable interface from old libraries. Es kann für Schnittstellen wieRunnable,Comparator usw. verwendet werden. However, thisdoesn’t mean that you should review your whole older codebase and change everything.

6. Vermeiden Sie Überladungsmethoden mit funktionalen Schnittstellen als Parameter

Verwenden Sie Methoden mit unterschiedlichen Namen, um Kollisionen zu vermeiden. Schauen wir uns ein Beispiel an:

public interface Processor {
    String process(Callable c) throws Exception;
    String process(Supplier s);
}

public class ProcessorImpl implements Processor {
    @Override
    public String process(Callable c) throws Exception {
        // implementation details
    }

    @Override
    public String process(Supplier s) {
        // implementation details
    }
}

Auf den ersten Blick erscheint dies vernünftig. Aber jeder Versuch, eine der Methoden vonProcessorImplauszuführen:

String result = processor.process(() -> "abc");

endet mit einem Fehler mit der folgenden Meldung:

reference to process is ambiguous
both method process(java.util.concurrent.Callable)
in com.example.java8.lambda.tips.ProcessorImpl
and method process(java.util.function.Supplier)
in com.example.java8.lambda.tips.ProcessorImpl match

Um dieses Problem zu lösen, haben wir zwei Möglichkeiten. The first is to use methods with different names:

String processWithCallable(Callable c) throws Exception;

String processWithSupplier(Supplier s);

The second is to perform casting manually. Dies ist nicht bevorzugt.

String result = processor.process((Supplier) () -> "abc");

7. Behandeln Sie Lambda-Ausdrücke nicht als innere Klassen

Trotz unseres vorherigen Beispiels, in dem wir die innere Klasse im Wesentlichen durch einen Lambda-Ausdruck ersetzt haben, unterscheiden sich die beiden Konzepte in einer wichtigen Hinsicht: Umfang.

Wenn Sie eine innere Klasse verwenden, wird ein neuer Bereich erstellt. Sie können lokale Variablen aus dem einschließenden Bereich ausblenden, indem Sie neue lokale Variablen mit demselben Namen instanziieren. Sie können auch das Schlüsselwortthis in Ihrer inneren Klasse als Referenz auf ihre Instanz verwenden.

Lambda-Ausdrücke arbeiten jedoch mit einem einschließenden Gültigkeitsbereich. Sie können Variablen innerhalb des Lambda-Körpers nicht aus dem einschließenden Bereich ausblenden. In diesem Fall verweist das Schlüsselwortthis auf eine umschließende Instanz.

In der KlasseUseFoo haben Sie beispielsweise eine Instanzvariablevalue:

private String value = "Enclosing scope value";

Fügen Sie dann in eine Methode dieser Klasse den folgenden Code ein und führen Sie diese Methode aus.

public String scopeExperiment() {
    Foo fooIC = new Foo() {
        String value = "Inner class value";

        @Override
        public String method(String string) {
            return this.value;
        }
    };
    String resultIC = fooIC.method("");

    Foo fooLambda = parameter -> {
        String value = "Lambda value";
        return this.value;
    };
    String resultLambda = fooLambda.method("");

    return "Results: resultIC = " + resultIC +
      ", resultLambda = " + resultLambda;
}

Wenn Sie die MethodescopeExperiment() ausführen, erhalten Sie das folgende Ergebnis:Results: resultIC = Inner class value, resultLambda = Enclosing scope value

Wie Sie sehen, können Sie durch Aufrufen vonthis.value im IC von seiner Instanz aus auf eine lokale Variable zugreifen. Im Fall des Lambda erhalten Sie mit dem Aufruf vonthis.valueZugriff auf die Variablevalue, die in der KlasseUseFoodefiniert ist, nicht jedoch auf die Variablevalue, die in der Klasse definiert ist Lambdas Körper.

8. Halten Sie Lambda-Ausdrücke kurz und selbsterklärend

Verwenden Sie nach Möglichkeit einzeilige Konstruktionen anstelle eines großen Codeblocks. Denken Sie daran,lambdas should be anexpression, not a narrative. Trotz seiner prägnanten Syntaxlambdas should precisely express the functionality they provide.

Dies ist hauptsächlich eine Stilberatung, da sich die Leistung nicht drastisch ändern wird. Im Allgemeinen ist es jedoch viel einfacher, solchen Code zu verstehen und damit zu arbeiten.

Dies kann auf viele Arten erreicht werden - schauen wir uns das genauer an.

8.1. Vermeiden Sie Codeblöcke in Lambdas Körper

Im Idealfall sollten Lambdas in einer Codezeile geschrieben werden. Bei diesem Ansatz ist das Lambda eine selbsterklärende Konstruktion, die angibt, welche Aktion mit welchen Daten ausgeführt werden soll (bei Lambdas mit Parametern).

Wenn Sie einen großen Codeblock haben, ist die Funktionalität des Lambda nicht sofort klar.

Gehen Sie in diesem Sinne wie folgt vor:

Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
    String result = "Something " + parameter;
    //many lines of code
    return result;
}

Anstatt von:

Foo foo = parameter -> { String result = "Something " + parameter;
    //many lines of code
    return result;
};

However, please don’t use this “one-line lambda” rule as dogma. Wenn die Lambda-Definition zwei oder drei Zeilen enthält, ist es möglicherweise nicht sinnvoll, diesen Code in eine andere Methode zu extrahieren.

8.2. Vermeiden Sie die Angabe von Parametertypen

In den meisten Fällen kann ein Compiler den Typ der Lambda-Parameter mit Hilfe vontype inference auflösen. Daher ist das Hinzufügen eines Typs zu den Parametern optional und kann weggelassen werden.

Mach das:

(a, b) -> a.toLowerCase() + b.toLowerCase();

an Stelle von:

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

8.3. Vermeiden Sie Klammern um einen einzelnen Parameter

Die Lambda-Syntax erfordert Klammern nur für mehr als einen Parameter oder wenn überhaupt kein Parameter vorhanden ist. Aus diesem Grund ist es sicher, den Code ein wenig zu verkürzen und Klammern auszuschließen, wenn nur ein Parameter vorhanden ist.

Also mach das:

a -> a.toLowerCase();

an Stelle von:

(a) -> a.toLowerCase();

8.4. Vermeiden Sie Rücksendungen und Klammern

Die AnweisungenBraces undreturn sind in einzeiligen Lambda-Körpern optional. Dies bedeutet, dass sie der Übersichtlichkeit und Übersichtlichkeit halber weggelassen werden können.

Mach das:

a -> a.toLowerCase();

an Stelle von:

a -> {return a.toLowerCase()};

8.5. Verwenden Sie Methodenreferenzen

Selbst in unseren vorherigen Beispielen rufen Lambda-Ausdrücke häufig nur Methoden auf, die bereits an anderer Stelle implementiert sind. In dieser Situation ist es sehr nützlich, eine andere Java 8-Funktion zu verwenden:method references.

Also, der Lambda-Ausdruck:

a -> a.toLowerCase();

könnte ersetzt werden durch:

String::toLowerCase;

Dies ist nicht immer kürzer, macht den Code jedoch besser lesbar.

9. Verwenden Sie "Effektiv endgültige" Variablen

Der Zugriff auf eine nicht endgültige Variable in Lambda-Ausdrücken führt zu einem Fehler bei der Kompilierung. But it doesn’t mean that you should mark every target variable as final.

Nach dem Konzept „effectively final“ behandelt ein Compiler jede Variable alsfinal,, solange sie nur einmal zugewiesen wird.

Es ist sicher, solche Variablen in Lambdas zu verwenden, da der Compiler ihren Status kontrolliert und sofort nach jedem Versuch, sie zu ändern, einen Fehler beim Kompilieren auslöst.

Der folgende Code wird beispielsweise nicht kompiliert:

public void method() {
    String localVariable = "Local";
    Foo foo = parameter -> {
        String localVariable = parameter;
        return localVariable;
    };
}

Der Compiler informiert Sie darüber, dass:

Variable 'localVariable' is already defined in the scope.

Dieser Ansatz sollte den Prozess der Thread-sicheren Ausführung von Lambda vereinfachen.

10. Objektvariablen vor Mutation schützen

Einer der Hauptzwecke von Lambdas ist die Verwendung beim parallelen Rechnen - was bedeutet, dass sie wirklich hilfreich sind, wenn es um Thread-Sicherheit geht.

Das „effektiv endgültige“ Paradigma hilft hier sehr, aber nicht in jedem Fall. Lambdas kann den Wert eines Objekts nicht vom eingeschlossenen Bereich ändern. Im Fall von veränderlichen Objektvariablen kann ein Zustand innerhalb von Lambda-Ausdrücken geändert werden.

Betrachten Sie den folgenden Code:

int[] total = new int[1];
Runnable r = () -> total[0]++;
r.run();

Dieser Code ist legal, da die Variabletotal"effektiv endgültig" bleibt. Aber wird das Objekt, auf das es verweist, nach der Ausführung des Lambda denselben Zustand haben? No!

Behalten Sie dieses Beispiel als Erinnerung bei, um Code zu vermeiden, der unerwartete Mutationen verursachen kann.

11. Fazit

In diesem Tutorial haben wir einige Best Practices und Fallstricke in den Lambda-Ausdrücken und Funktionsschnittstellen von Java 8 gesehen. Trotz des Nutzens und der Leistungsfähigkeit dieser neuen Funktionen handelt es sich lediglich um Werkzeuge. Jeder Entwickler sollte bei der Verwendung darauf achten.

Das vollständigesource code für das Beispiel ist inthis GitHub project verfügbar - dies ist ein Maven- und Eclipse-Projekt, sodass es importiert und unverändert verwendet werden kann.