JAVA-C1
Funktionale Programmierung
@! Spungmarken in allen Pages testen!
Funktionale Programmierung
Function as Value
Pure Function
Higher-Order Functions
@FunctionalInterface Annotation
Methoden-Referenzen
Bedeutung von Stream API
Optional als neuer Typ
Zusammengefasst
Funktionales Interface
@FunctionalInterface
Lambda-Expressions
Simple / Komplex
Scope
Custom-Functional-Interfaces
Custom-Functional-Interfaces
Custom-Functional-Interfaces
Herkömmliches Interface
Funktionales Interface IRechnerDouble definieren und verwenden.
Generische funktionale Interfaces
@FunctionalInterface
Beispiele mit IRechnerGenericFlexible<Integer> r = ..
Beispiel: Lambda-Rechenoperationen mit Objekten
Beispiel: IRead T, IUpdate T, I Read T, IPrint T
Predefined Functional Interfaces
Overview
Function
BiFunction
Predicate
Supplier
Consumer
Diagramm aller Predefined Functional Interfaces
Diagramm Converter Interfaces
ToIntFunction/ToIntBiFunction
DoubleBinaryOperator (EXAMPLE)
@! Sprunmarken ergänzen/testen!
plugin-and-command-pattern-architecture
Beispiel/Collection
Overview
Custom | Overview
Functional Interfaces sind ein spezieller Datentyp welcher zur Speicherung von Referenzen auf Methoden in Variablen verwendet werden kann. Auch wenn Java von sich aus einige solcher Interfaces bereitstellt, kann man sich eigene Custom Function Interfaces programmieren.
In diesem Themenblock zum Thema «Funktionale Programmierung» zeigen wir, wie man typische Rechenoperationen über Lambda-Ausdrücke formulieren und diese in eigenen funktionalen Interfaces speichern kann. Wir zeigen darüber hinaus, dass Funktionale Interfaces auch generisch programmiert werden können und welche Probleme in diesem Falle implizitem Casting entstehen können: Implizites Casting funktioniert in diesem Falle nämlich nicht.
Syntax
Custom | Beispiele
Examples
Die nachfolgenden Beispiele zeigen, wie man die immer gleiche Problemstellung für die Nutzung von Rechenarten auf Lambda-Ausdrücke auslagern kann. Bedeutung
Es werden in allen Varianten jeweils die vier Rechenarten programmiert. Die verwendeten Interfaces sind allerdings in jedem Fall andere. Im nachfolgenden Beispiel zeigen wir in verschiedenen Varianten, wie man übliche Rechenoperationen wie Addieren, Substrahieren, Multiplizieren und Dividieren in Java auch über Lambda-Expressions und Funktionale Interfaces abbilden kann.
WICHTIG: Besondere Beachtung sollte man dem Casting von Parametern widmen, denn die implizierte Typkonvertierung funktioniert bei Lambdas NICHT.
Custom | Herkömmliches Interface
Die einfachste Variante, um ein funktionales Interface erstellen zu können, besteht wie auch bei einer normalen Klasse darin, dass man den Kopf einer Methode programmiert, über welche ein Objekt später verfügen muss, wenn es denn das Interfaces erfüllen möchte.
package com.stuelken.java.tests.a7.e40401_lambda;
/**
* Funktionale Schnittstelle beziehungsweise funktionales Interface mit einer
* Methode, hier für den Datentyp {@link Integer}.
*
* @author t2m
*/
interface IRechner {
int berechnen(int a, int b);
}
Nachdem wir diese Interface als Typ für Funktionen definiert haben, können wir später im Programmcode einen Lambda-Ausdruck definieren und diesen in einer Variablen dieses Typs speichern.
Hinweis: Da das funktionale Interface die Parametertypen sowie auch den Typ des Return-Wertes angibt, werden für den eigentlichen Programmcode des Lambda-Ausdrucks keine weiteren Typen mehr angegeben.
IRechner additionInt = (a, b) -> a + b;
System.out.println("Ergebnis: " + additionInt.berechnen(5, 3));
Da die Methode nun strikt typisiert ist für Integer-Werte, würden wir für den Fall einer Methode mit Double-Werten nun ein zweites funktionales Interface benötigen.
/**
* Funktionale Schnittstelle beziehungsweise funktionales Interface mit einer
* Methode, hier für den Datentyp {@link Double}.
*
* @author t2m
*/
interface IRechnerDouble {
double berechnen(double a, double b);
}
Auch in diesem Fall können wir später Lambda-Ausdrücke dieses Typs definieren.
IRechnerDouble divisionDouble = (a, b) -> (b != 0) ? a / b : 0;
System.out.println("Ergebnis: " + divisionDouble.berechnen(5, 3));
Der Lambda-Ausdruck wird
Custom | Generisches Funktionales Interfaces
Java ermöglicht auch eine generische Variante für funktionale Interfaces. Es müssen schlichtweg generische Typparameter ergänzt werden.
/**
* Generisches funktionales Interface, das eine Berechnung mit zwei Werten des
* Typs <T> durchführt und einen Wert vom gleichen Typ zurückgibt.
*
* @author t2m
* @param <T> Der Typ sowohl für die Parameter als auch den Rückgabewert.
*/
interface IRechnerGeneric<T> {
T berechnen(T a, T b);
}
Wir können nun bei derr Deklaration einer Variablen noch den Typ angeben und uns damit entscheiden, ob wir nun mit Integer oder Double rechnen möchten.
IRechnerGeneric<Integer> substraktionInt = (a, b) -> a - b;
System.out.println("Ergebnis: " + substraktionInt.berechnen(5, 3));
IRechnerGeneric<Double> substraktionDouble = (a, b) -> a - b;
System.out.println("Ergebnis: " + substraktionDouble.berechnen((double) 5, (double) 3));
Hinweis: Bei der Verwendung von Typparametern für funktionale Interfaces ist es in Java zwingend erforderlich, dass Werte mit den korrekten Datentypen übergeben werden. Ein automatisch casting bzw. eine implizite Typkonvertierung von int für den Wert 5 zu double des Wertes 5.0 funktioniert im Gegensatz zum sonst üblichen Verfahren hier nicht.
Der Wert muss also entweder als
5 oder mit Casting
(double) 5
eingegeben werden.
Custom | Generisch erweitert
Um für die Parameter selbst auch andere Typen ermöglichen zu können, gibt es einen weiteren Weg.
Da generische Typen auch mögliche Supertypen in der Vererbung angegeben können, ist es möglich, den gemeinsamen Supertyp von Integer, Double, Float und dergleichen vom Typ Number anzugeben.
/**
* Generisches funktionales Interface, das eine Berechnung mit zwei Werten des
* Typs <T> durchführt und einen Wert vom gleichen Typ zurückgibt.
*
* @author t2m
* @param <T> Der Typ sowohl für die Parameter als auch den Rückgabewert.
*/
interface IRechnerGenericFlexible<T extends Number> {
T berechnen(T a, T b);
}
Mit double-Werten:
IRechnerGenericFlexible<Double> multiplikationDouble =
(a, b) -> a.doubleValue() * b.doubleValue();
IRechnerGenericFlexible<Double> divisionDouble =
(a,b) -> (b.doubleValue() != 0) ? a.doubleValue() / b.doubleValue() : 0;
System.out.println("Ergebnis: " + multiplikationDouble.berechnen((double) 5, (double) 3));
System.out.println("Ergebnis: " + divisionDouble.berechnen((double) 5, (double) 3));
Wichtig:
Auch hier müssen wir wegen des Double-Typs
zwingend einen double-Wert angeben, also entweder als 5.0
Literal oder aber mit Casting des 5 Literals, welches
von Java als Integer interpretiert wird.
Mit int-Werten könnten wir zwar den eingehenden Parameter in double wandeln, aber wir müssen für einen Integer-Typ zwingend das Ergebnis der Operation wieder in int casten.
IRechnerGenericFlexible<Integer> multiplikationInt =
(a, b) -> (int) (a.doubleValue() * b.doubleValue());
IRechnerGenericFlexible<Integer> divisionInt =
(a,b) -> (int) ((b != 0) ? a.doubleValue() / b.doubleValue() : 0);
System.out.println("Ergebnis: " + multiplikationInt.berechnen(5, 3));
System.out.println("Ergebnis: " + divisionInt.berechnen(5, 3));
Auch in diesem Fall ist nun für den Wert zwingend
ein 5 Literal
für den Integer-Typ erforderlich.
Examples
Custom | Beispiele für Custom Generic Functional Interfaces
Examples
Beispiel 1
Lambda-Ausdruck im Einzeiler in Variable speichern.
Im nachfolgenden Beispiel zeigen wir in verschiedenen Varianten, wie man übliche Rechenoperationen wie Addieren, Substrahieren, Multiplizieren und Dividieren in Java auch über Lambda-Expressions und Funktionale Interfaces abbilden kann.
Es werden in allen Varianten jeweils die vier Rechenarten programmiert. Die verwendeten Interfaces sind allerdings in jedem Fall andere.
package com.stuelken.java.tests.a7.e40356_lambda;
/**
* Funktionale Schnittstelle beziehungsweise funktionales Interface mit einer
* Methode, hier für den Datentyp {@link Integer}.
*
* @author t2m
*/
@FunctionalInterface
interface IRechner {
int berechnen(int a, int b);
}
/**
* Funktionale Schnittstelle beziehungsweise funktionales Interface mit einer
* Methode, hier für den Datentyp {@link Double}.
*
* @author t2m
*/
@FunctionalInterface
interface IRechnerDouble {
double berechnen(double a, double b);
}
/**
* Generisches funktionales Interface, das eine Berechnung mit zwei Werten des
* Typs <T> durchführt und einen Wert vom gleichen Typ zurückgibt.
*
* @author t2m
* @param <T> Der Typ sowohl für die Parameter als auch den Rückgabewert.
*/
@FunctionalInterface
interface IRechnerGeneric<T> {
T berechnen(T a, T b);
}
/**
* Generisches funktionales Interface, das eine Berechnung mit zwei Werten des
* Typs <T> durchführt und einen Wert vom gleichen Typ zurückgibt.
*
* @author t2m
* @param <T> Der Typ sowohl für die Parameter als auch den Rückgabewert.
*/
@FunctionalInterface
interface IRechnerGenericFlexible<T extends Number> {
T berechnen(T a, T b);
}
/**
* Klasse mit Beispielen die Definition von Lambda-Ausdrücken als alternative zu
* Rechenoperatoren.
*
* @author t2m
*
*/
public class LambdaRechnerExample {
public static void main(String[] args) {
exampleRechnenInt();
exampleRechnenDouble();
exampleRechnenGenerisch();
}
private static void exampleRechnenInt() {
/*
* Implementierungen des Interfaces mit Hilfe von Lambda-Expressions in Java
* welche entsprechend dem Interface jeweils zwei Parameter erwarten und einen
* Rückgabewert geben.
*/
System.out.println("Lambda-Expressions mit int-Werten:");
IRechner additionInt = (a, b) -> a + b;
IRechner substraktionInt = (a, b) -> a - b;
IRechner multiplikation = (a, b) -> a * b;
IRechner divisionInt = (a, b) -> (b != 0) ? a / b : 0; // Vermeidung von Division durch 0
System.out.println("Ergebnis: " + additionInt.berechnen(5, 3));
System.out.println("Ergebnis: " + substraktionInt.berechnen(5, 3));
System.out.println("Ergebnis: " + multiplikation.berechnen(5, 3));
System.out.println("Ergebnis: " + divisionInt.berechnen(5, 3));
}
private static void exampleRechnenDouble() {
/*
* Da das Interface die Typen für Parameter und Return-Wert vorgibt, müssen für
* double-Werte ein anderes Interfaces verwendet werden.
*/
System.out.println("Lambda-Expressions mit double-Werten:");
IRechnerDouble additionDouble = (a, b) -> a + b;
IRechnerDouble substraktionDouble = (a, b) -> a - b;
IRechnerDouble multiplikationDouble = (a, b) -> a * b;
IRechnerDouble divisionDouble = (a, b) -> (b != 0) ? a / b : 0;
System.out.println("Ergebnis: " + additionDouble.berechnen(5, 3));
System.out.println("Ergebnis: " + substraktionDouble.berechnen(5, 3));
System.out.println("Ergebnis: " + multiplikationDouble.berechnen(5, 3));
System.out.println("Ergebnis: " + divisionDouble.berechnen(5, 3));
}
/**
* Hinweis: Java kann int nicht automatisch zu Double konvertieren, wenn ein
* generisches Interface genutzt wird. Es muss explizit gecastet werden.
*/
private static void exampleRechnenGenerisch() {
System.out.println("Lambda-Expressions generisch mit int-Werten:");
IRechnerGeneric<Integer> additionInt = (a, b) -> a + b;
IRechnerGeneric<Integer> substraktionInt = (a, b) -> a - b;
IRechnerGeneric<Integer> multiplikationInt = (a, b) -> a * b;
IRechnerGeneric<Integer> divisionInt = (a, b) -> (b != 0) ? a / b : 0;
System.out.println("Ergebnis: " + additionInt.berechnen(5, 3));
System.out.println("Ergebnis: " + substraktionInt.berechnen(5, 3));
System.out.println("Ergebnis: " + multiplikationInt.berechnen(5, 3));
System.out.println("Ergebnis: " + divisionInt.berechnen(5, 3));
System.out.println("Lambda-Expressions generisch mit double-Werten:");
IRechnerGeneric<Double> additionDouble = (a, b) -> a + b;
IRechnerGeneric<Double> substraktionDouble = (a, b) -> a - b;
IRechnerGeneric<Double> multiplikationDouble = (a, b) -> a * b;
IRechnerGeneric<Double> divisionDouble = (a, b) -> (b != 0) ? a / b : 0;
// Ohne das Casting des liefert Java eine Fehlermeldung:
// The method berechnen(Double, Double) in the type IRechnerGeneric<Double> is
// not applicable for the arguments (int, int)
System.out.println("Ergebnis: " + additionDouble.berechnen((double) 5, (double) 3));
System.out.println("Ergebnis: " + substraktionDouble.berechnen((double) 5, (double) 3));
System.out.println("Ergebnis: " + multiplikationDouble.berechnen((double) 5, (double) 3));
System.out.println("Ergebnis: " + divisionDouble.berechnen((double) 5, (double) 3));
}
/**
* Hinweis: Java kann int nicht automatisch zu Double konvertieren, wenn ein
* generisches Interface genutzt wird. Es muss explizit gecastet werden.
*/
private static void exampleRechnenGenerischFlexible() {
System.out.println("Lambda-Expressions generisch-flexibel mit int-Werten:");
IRechnerGenericFlexible<Integer> additionInt = (a, b) -> (int) (a.doubleValue() + b.doubleValue());
IRechnerGenericFlexible<Integer> substraktionInt = (a, b) -> (int) (a.doubleValue() - b.doubleValue());
IRechnerGenericFlexible<Integer> multiplikationInt = (a, b) -> (int) (a.doubleValue() * b.doubleValue());
IRechnerGenericFlexible<Integer> divisionInt = (a,
b) -> (int) ((b != 0) ? a.doubleValue() / b.doubleValue() : 0);
System.out.println("Ergebnis: " + additionInt.berechnen(5, 3));
System.out.println("Ergebnis: " + substraktionInt.berechnen(5, 3));
System.out.println("Ergebnis: " + multiplikationInt.berechnen(5, 3));
System.out.println("Ergebnis: " + divisionInt.berechnen(5, 3));
System.out.println("Lambda-Expressions generisch-flexibel mit double-Werten:");
IRechnerGenericFlexible<Double> additionDouble = (a, b) -> a.doubleValue() + b.doubleValue();
IRechnerGenericFlexible<Double> substraktionDouble = (a, b) -> a.doubleValue() - b.doubleValue();
IRechnerGenericFlexible<Double> multiplikationDouble = (a, b) -> a.doubleValue() * b.doubleValue();
IRechnerGenericFlexible<Double> divisionDouble = (a,
b) -> (b.doubleValue() != 0) ? a.doubleValue() / b.doubleValue() : 0;
// Ohne das Casting des liefert Java eine Fehlermeldung:
// The method berechnen(Double, Double) in the type IRechnerGeneric<Double> is
// not applicable for the arguments (int, int)
System.out.println("Ergebnis: " + additionDouble.berechnen((double) 5, (double) 3));
System.out.println("Ergebnis: " + substraktionDouble.berechnen((double) 5, (double) 3));
System.out.println("Ergebnis: " + multiplikationDouble.berechnen((double) 5, (double) 3));
System.out.println("Ergebnis: " + divisionDouble.berechnen((double) 5, (double) 3));
}
}
Beispiel 2
Beispiel Lambda Calculator
Das nachfolgende eher akademische Beispiel zeigt, dass wir Rechenoperationen nicht nur mit Zahlen sondern auch mit INumber-artigen Objekten vom Typ NumberObject ebenso mit Lambda-Expressions abbilden können, indem wir Zahlenwerte für unsere Operationen in Objekten speichern und damit die Lambda-Expressions für uns eigenen INumber-Typ definieren.
INumber result = addieren.operate(a, multiplizieren.operate(b, c));
Das vollständige Beispiel.
package com.stuelken.java.tests.a7.e40366_lambda_calculator;
/**
* Funktionales Interface für Operationen, das zwei Operanden nimmt und ein Ergebnis liefert
* @author t2m
*
* @param <T> Erster Operand der Operation, bei Rechenzeichen der Wert links vom Operator
* @param <U> Zweiter Operand der Operation, bei Rechenzeichen der Wert rechts vom Operator
* @param <R>
*/
@FunctionalInterface
interface IOperation<T, U, R> {
R operate(T operand1, U operand2);
}
/**
* Interface zur Darstellung eines skalaren Zahlenwerts.
*
* Der Sinn und Zweck besteht darin, dass wir in diesem Beispiel mit Objekten
* arbeiten und nicht mit normalen Zahlen.
*
* @author t2m
*
*/
interface INumber {
double getValue();
}
/**
* Konkrete Implementierung von INumber, die einen double-Wert kapselt.
*
* @author t2m
*/
class NumberObject implements INumber {
private final double value;
/**
* Konstruktor für Kapselung von Zahlenwerten in einem Objekt.
*
* @param value Ein Zahlenwert welcher vom Prinzip her aber auch int, float und double
* sein könnte, weil bei Java bei der Zuweisung normalerweise die Werte implizit
* umwandelt.
*/
public NumberObject(double value) {
this.value = value;
}
/**
* Methode mit deren Hilfe wir an den eigentlichen Zahlenwert
* des Objekts gelangen können.
*/
@Override
public double getValue() {
return value;
}
/**
* Überschreiben der von {@link Object} geerbten toString() Methode
* weil anderenfalls statt des Strings für den Zahlenwert nur der
* Typ NumberObject gefolgt vom Hashcode ausgegeben werden würde.
*/
@Override
public String toString() {
return Double.toString(value);
}
}
/**
* CustomizableCalculator, in dem wir die Operationen als Lambda-Ausdrücke definieren und nutzen
*
* @author t2m
*
*/
public class CustomizableCalculator {
public static void main(String[] args) {
// Erzeugen der NumberObjects
INumber a = new NumberObject(2);
INumber b = new NumberObject(4);
INumber c = new NumberObject(3);
// Definition der Operationen als funktionale Interfaces
// Addition
IOperation<INumber, INumber, INumber> addieren =
(x, y) -> new NumberObject(x.getValue() + y.getValue());
// Multiplikation
IOperation<INumber, INumber, INumber> multiplizieren =
(x, y) -> new NumberObject(x.getValue() * y.getValue());
// Berechnung: 2 + 4 * 3 (Multiplikation zuerst, weil wir sie verschachteln)
// entspricht: addieren(a, multiplizieren(b, c))
INumber result = addieren.operate(a, multiplizieren.operate(b, c));
System.out.println("Ergebnis: " + result.getValue()); // Ausgabe: 14.0
}
}
Beispiel 3
CRUD Operationen
Man kann mit Hilfe von Lambda-Ausdrücken typische CRUD Operationen (create, read, update, delete) programmieren.
Erzeugen (ICreate): Man definiert eine Methode create() ohne Eingabeparameter, die ein Objekt erzeugt.
Lesen (IRead): Die Methode read(T t) nimmt ein Objekt und gibt es z. B. in unveränderter Form zurück (in diesem Beispiel ein simplen "Pass-Through").
Aktualisieren (IUpdate): Hier wird das übergebene Objekt verändert – z. B. durch Addition zu seinem internen Wert – und zurückgegeben.
Ausgeben (IPrint): Diese Methode gibt den Zustand (z. B. den Zahlenwert) des Objekts auf der Konsole aus.
INumber result = addieren.operate(a, multiplizieren.operate(b, c));
Zur besseren Lesbarkeit bekommen alle Interface ein I Zeichen vorangestellt.
package com.stuelken.java.b3.e03_lambda_crud;
/**
* Beispielklasse, die alle CRUD-Operationen mithilfe von Lambda-Ausdrücken kombiniert.
*/
public class CrudLambdaExample {
/**
* Hauptprogramm
* @param args Keine Argumente
*/
public static void main(String[] args) {
/*
* === Deklaration der Funktionen
*/
// Erzeugungs-Operation: Erstelle ein NumberObject mit einem Startwert
ICreate<NumberObject> creator = () -> new NumberObject(2.0);
// Lese-Operation: Liest den aktuellen Wert, hier trivial implementiert.
IRead<NumberObject> reader = (obj) -> obj; // Gibt das Objekt unverändert zurück
// Update-Operation: Ändert den Wert, beispielsweise durch Addition.
// Hier simulieren wir: update um 10 erhöhen.
IUpdate<NumberObject> updater = (obj) -> {
obj.setValue(obj.getValue() + 10.0);
return obj;
};
// Print-Operation: Gibt den Wert des Objekts aus.
IPrint<NumberObject> printer = (obj) -> System.out.println("Wert: " + obj.getValue());
/*
* Eigentliche Nutzung der Funktionen
*/
// Ablauf: Erstellen, Lesen, Aktualisieren und Ausgeben
NumberObject data = creator.create(); // Create
data = reader.read(data); // Read
data = updater.update(data); // Update
printer.print(data); // Print
}
}
//Funktionale Interfaces für CRUD-Operationen
@FunctionalInterface
interface ICreate<T> {
T create();
}
@FunctionalInterface
interface IRead<T> {
T read(T t);
}
@FunctionalInterface
interface IUpdate<T> {
T update(T t);
}
@FunctionalInterface
interface IPrint<T> {
void print(T t);
}
/**
* Ein einfaches Zahlenobjekt, das einen scalar-Wert kapselt.
*
* @author t2m
*/
class NumberObject {
private double value;
public NumberObject(double value) {
this.value = value;
}
public double getValue() {
return value;
}
public void setValue(double value) {
this.value = value;
}
@Override
public String toString() {
return Double.toString(value);
}
}
Output Wert: 12.0
Links
Quellen, Notes, Tags
Custom Function Interfaces, implizierte Typkonvertierung
UIO3 Es ist einfacher als Du denkst.
Stelle noch heute Deine Anfrage.
