Overview
Der funktionale Programmierstil lässt sich seit Java 8 durch die Einführung von Lambda-Ausdrücken umsetzen. Funktionale Interfaces bilden die Basis für einen Lambda-Ausdruck. Der Einsatz von Lambdas ist eine Alternative zur Realisierung eines SAM-Typs durch eine anonymen innere Klasse. Dadurch kann die Code-Qualität verbessert werden.
Die Programmiersprache Java folgt grundsätzlich dem objektorientierten Programmierparadigma. Das heißt, dass der Programmierstil bzw. das Code-Design den Prinzipien der Objektorientierung folgt.
Doch seit dem Release von Version 8 unterstützt Java als Ergänzung(!) zur Objektorientierung auch den Einsatz des funktionalen Programmierstils. Was genau bedeutet das?
Durch den Einsatz des funktionellen Programmierstils steht uns ein mächtiges Werkzeug zur Verfügung. Denn durch die alternative Herangehensweise können wir bestimmte Programmieraufgaben erheblich einfacher und eleganter umsetzen als mit einem objektorientierten Ansatz. Unsere Programmcode kann dadurch an Qualität gewinnen, weil der Code einfacher zu lesen, zu pflegen und auszubauen wird. Außerdem gilt: Je besser dein Code designt ist, desto einfacher kann sich deine Anwendung mit sich ändernden Anforderungen weiterentwicklen.
Wie wir die funktionale Programmierung in Java umsetzen, sehen wir uns jetzt einmal genau an!
Der funktionalen Programmierstil lässt sich seit Java 8 durch die Einführung von Lambda-Ausdrücken umsetzen.
Ein Lambda-Ausdruck ist ein Behälter für ein Stück Sourcecode und wird als ausführbare Funktion definiert.
Eine Lambda-Funktion ähnelt einer Methode, weil sie beschreibt, was vom Programm auszuführen ist. Auch hat eine Lambda-Funktion einen Body und kann Parameter haben.
Der Unterschied zu einer Methode ist aber, dass die Lambda-Funktion anonym ("namenlos") ist und vor allem keine definierende Klasse besitzt.
Die Syntax einer Lambda-Funktion hat eine spezielle Schreibweise und sieht so aus:
Schauen wir uns jetzt anhand des Event-Handlings ein konkretes Beispiel an, wie ein Lambda-Ausdruck aussieht und um wieviel einfacher er unseren Code machen kann. Um bei einem Klick auf einen JavaFX-Button einen Text auf der Konsole anzuzeigen, könnte man das Ganze "klassisch" ohne Lambda so programmieren:
Button btn = new Button("Ich bin ein Button");
// Lösung ohne Lambda
btn.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
System.out.println("Ich wurde geklickt");
}
});
Wie du siehst, ist das ganze recht umständlich mit jeder Menge Boilerplate-Code, d.h. Code, der zwar syntaktisch notwendig, aber nicht prodiktiv für die Programmlogik ist.
In diesem Beispiel übergeben wir in der Methode setOnAction() ein extra dafür instanziiertes Objekt einer anonymen inneren Klasse, die den Interface-Typ EventHandler implementiert, indem es die handle() Methode mit vollständiger Methodendeklaration überschreibt.
Es sind also viele Schritte notwendig, nur um eine kleine Konsolenausgabe zu erzeugen! Doch keine Sorge: Das muss so nicht sein 😉
Die perfekte Lösung besteht dagegen im Einsatz einer Lambda-Funktion, und die sieht für unser Beispiel so aus:
// Lösung mit Lambda
btn.setOnAction(e -> System.out.println("Ich wurde geklickt!"));
Magic, isn't it? 🤩
Der ganze Boilerplate-Code ist raus und wir haben durch den rein funktionalen Ansatz das Event-Handling auf nur eine Zeile reduzieren können: Wir schieben nur noch den direkt auszuführenden Code mit e als Parameter rein.
Wie das möglich ist, d.h. welche Mechanismen hier genau wirken, um Lambda-Ausdrücke in Java möglich zu machen, schauen wir uns im nächsten Schritt an.
Natürlich lässt wir unsere Lambda-Implementierung auch mit einer Variablen eines passenden Typs referenzieren. Die Variable verweist dann auf die Lambda-Funktion und kann an geeigneter Stelle eingesetzt werden:
EventHandler<ActionEvent> event = (e -> System.out.println("Ich wurde geklickt!"));
btn.setOnAction(event);
Das wichtigste vorweg:
Ein funktionales Interface ist ein Interface mit exakt einer abstrakten Methode (statische und default Methoden dürfen vorhanden sein). Man sagt auch SAM-Typ dazu, wobei SAM für Single Abstract Method steht.
Neben dem funktionalen Interface EventHandler, gibt es eine ganze Reihe weiterer SAM-Typen, wie z.B. Runnable, Callable<V>, Comparator<T>, ActionListener usw.
Vor Java 8 wurden funktionale Interfaces durch eine anonyme Klasse implementiert (siehe oben). Heute geht das als bessere Alternative durch den funktionalen Ansatz mit Lambda, um die abstrakte Methode des funktionalen Interfaces zu erfüllen.
Schauen wir uns nochmal den Lambda-Ausdruck von oben an:
btn.setOnAction(e -> System.out.println("Ich wurde geklickt!"));
Die Methode setOnAction() erwartet ein Objekt des Interfaces EventHandler. Woher weiß Java nun, dass unser Lambda-Ausdruck genau dieses funktionale Interface heranzieht? Er wird ja niergendwo genannt und schließlich gibt es viele funktionale Interfaces (auch benutzerdefinierte).
Die Antwort liegt im Mechanismus des Type Inference. Das bedeutet, dass der Compiler den passenden Typ aus dem Einsatzkontext heraus bestimmen kann. Da nämlich die Methode setOnAction() ein EventHandler-Objekt verlangt, ist das, wir wir ihr als Lambda-Funktion geben, durch eben diesen Kontext heraus für den Compiler gleichwertig.
Wichtig ist, dass die abstrakte Methode des SAM-Typs durch Lambda erfüllt werden kann, d.h. dass die Anzahl der Parameter und der Rückgabetyp kompatibel sind. Die abstrakte Methode des EventHandler-Interfaces hat einen Parameter und void als Rückgabetyp, insofern geht das perfekt konform mit unserer Lambda-Funktion oben.
Natürlich können wir auch eigene funktionale Interfaces als Basis für die Lamba-Nutzung programmieren. Hier ein sehr einfaches Beispiel:
package sample;
@FunctionalInterface
public interface MeinFunktionalesInterface {
public void methode();
}
Und so setzen wir den Lambda-Ausdruck für das funktionale Interface um:
MeinFunktionalesInterface x = () -> {
System.out.println("Ich liebe Lambda!");
};
Auch hier weiß der Compiler durch den Kontext, welches funktionale Interface als Basis für den Lambda-Ausdruck herangezogen wird: Denn auf die Deklaration der Variablen x vom Typ MeinFunktionalesInterface folgt ein Zuweisungsoperator und damit sollte auch ein Objekt vom Typ MeinFunktionalesInterface folgen. Da der Compiler damit rechnet, können wir ihm stattdessen auch straight einen alternativen Lambda-Ausdruck bieten.
Da die abstrakte Methode methode() keinen Parameter hat, dürfen wir (anders als im Lambda-Beispiel oben) auch keinen e-Parameter setzen, sondern müssen zur Kohärenz leere Klammern setzen. Der Body-Code System.out.println("Ich liebe Lambda!"); ist dann gleichzusetzen mit der Implementierung der abstrakten Methode methode().
Bestimmt ist dir in unserem SAM-Typ MeinFunktionalesInterface oben die Annotation @FunctionalInterface aufgefallen. Dies teilt dem Compiler mit, dass du ein funktionales Interfaces für die spätere Lambda-Nutzung erzeugen willst. Zwar ist die Annotation optional, aber wir empfehlen dir, sie in jedem Fall zu setzen. Der Grund ist, dass wenn du die Regeln für ein funktionales Interface nicht einhälst, der Compiler dir dies mitteilt. Zum Beispiel:
@FunctionalInterface
public interface MeinFunktionalesInterface { // Compiliert nicht!!
public void methode();
public void nochEineMethode();
}
Die Fehlermeldung lautet: Multiple non-overriding abstract methods found in interface sample.MeinFunktionalesInterface. Das heißt, wir haben die Regel für ein funktionales Interface verletzt, dass dieses nur eine(!) abstrakte Methode haben darf.
Beim Erstellen eines Lamba-Ausdrucks gibt es einige Schreibweisen, die zu einem noch kürzerem Code führen können.
Erstens: Falls die abstrakte Methode des funktionalen Interfaces einen Rückgabewert liefert, kann beim Lambda-Ausdruck sowohl das Schlüsselwort return als auch die eckigen Klammern ausgelassen werden:
MeinFunktionalesInterface lambda = () -> { return 10; };
// noch kürzer:
MeinFunktionalesInterface lambda = () -> 10;
Zweitens: Falls die abstrakte Methode nur einen Parameter hat, können die runden Klammern weggelassen werden:
MeinFunktionalesInterface lambda = (e) -> {System.out.println("Lambda"); };
// noch kürzer:
MeinFunktionalesInterface lambda = e -> {System.out.println("Lambda"); };
Drittens: Falls die Lambda-Funktion nur eine Anweisung im Body hat, können die eckigen Klammern ausgelassen werden:
MeinFunktionalesInterface x = e -> {System.out.println("Lambda");};
// noch kürzer:
MeinFunktionalesInterface x = e -> System.out.println("Lambda");
Java Basics
[Java einrichten] [Variablen] [Primitive Datentypen] [Operatoren] [if else] [switch-case] [Arrays] [Schleifen]
Objektorientierung
[Einstieg] [Variablen ] [Konstruktor] [Methoden] [Rekursion] [Statische Member] [Initializer] [Pass-by-value] [Objektsammlungen] [Objektinteraktion] [Objekte löschen]
Klassenbibliothek
[Allgemeines] [String ] [Math] [Wrapper] [Scanner] [java.util.Arrays] [Date-Time-API]
Vererbung
[Einstieg Vererbung] [Konstruktoren bei Vererbung ] [Der protected Zugriffsmodifikator] [Abstrakte Klassen und Methoden] [Polymorphie in Java] [Typumwandlung] [Die Klasse Object] [Die toString()-Methode] [Objekte vergleichen] [Was ist ein Interface?]