https://delors.github.io/prog-adv-java-funktionale-programmierung/folien.de.rst.html
https://delors.github.io/prog-adv-java-funktionale-programmierung/folien.de.rst.html.pdf
Programmierparadigma, bei dem Funktionen im Mittelpunkt stehen
Vermeidet veränderliche Zustände (Mutable State)
Fördert deklarativen Code statt imperativem Code
Wichtige Konzepte
Funktionen Höherer Ordnung
Lambda-Ausdrücke
Funktionskomposition
Currying und Partielle Anwendung
Ein Ausdruck, dessen Wert eine Funktion ist.
Solche Ausdrücke sind sehr nützlich, mussten in Java bisher aber mit anonymen inneren Klassen emuliert werden.
Ein einfache Personenklasse
1class Person {2private String name;3private int age;45public Person(String name, int age) {6this.name = name;7this.age = age;8}9public String getName() { return name; }10public int getAge() { return age; }11public String toString() { return "Person[" + name + ", " + age + "]"; }12}
Sortieren von Personen nach Alter
Angenommen wir haben eine Klasse Person und eine Liste von Personen, die nach Alter sortiert werden soll. Dazu muss eine Vergleichsfunktion übergeben werden. In Java <8 kommt dazu nur ein Objekt in Frage.
1List<Person> persons = Arrays.asList(2new Person("Hugo", 55),3new Person("Amalie", 15),4new Person("Anelise", 32) );
Traditionelle Lösung
1Collections.sort(persons, new Comparator<Person>() {2public int compare(Person p1, Person p2) {3return p1.getAge() - p2.getAge();4}5});
Lösung ab Java 8+
1Collections.sort(2persons,3(p1, p2) -> { return p1.getAge() - p2.getAge(); }4);
Lösung ab Java 8 (kürzer)
1 Collections.sort(persons, (p1, p2) -> p1.getAge() - p2.getAge());
Instanzen von inneren Klassen können immer Object zugewiesen werden:
1Object actionListener = new ActionListener() {2@Override3public void actionPerformed(ActionEvent e) {4System.out.println(text);5}6};
Illegale Zuweisung:
1Object actionListener = (e) -> System.out.println(text);23// Error: The target type of this expression must be a functional interface
Zuweisung an ein funktionales Interface:
1 ActionListener actionListener = (e) -> System.out.println(text);
Funktionale Interfaces
Ein Functional Interface ist ein Interface das genau eine Methode enthält (die natürlich abstrakt ist) optional kann die Annotation @FunctionalInterface hinzugefügt werden.
Vordefinierte Funktionsinterfaces
java.util.function enthält viele vordefinierte Funktionsinterfaces, die in der funktionalen Programmierung häufig verwendet werden.
Beispiele sind:
Function<T,R>: Eine Funktion, die ein Argument vom Typ T entgegennimmt und ein Ergebnis vom Typ R zurückgibt.
Predicate<T>: Eine Funktion, die ein Argument vom Typ T entgegennimmt und ein Ergebnis vom Typ boolean zurückgibt.
Consumer<T>: Eine Funktion, die ein Argument vom Typ T entgegennimmt und kein Ergebnis zurückgibt.
Supplier<T>: Eine Funktion, die kein Argument entgegennimmt und ein Ergebnis vom Typ T zurückgibt.
Als Implementierung eines funktionalen Interfaces (als „Lambda“) können auch Methoden verwendet werden.
Neue Methoden in der Collection API
forEach(Consumer<? super T> action)
removeIf(Predicate<? super T> filter)
replaceAll(UnaryOperator<T> operator)
sort(Comparator<? super T> c)
Erste Implementierung von Funktionen höherer Ordnung
Schreiben Sie eine Klasse Tuple2<T>; d. h. eine Variante von Pair bei der beide Werte vom gleichen Typ T sein müssen. Die Klasse soll Methoden haben, um die beiden Werte zu setzen und zu lesen und weiterhin um folgende Methoden ergänzt werden:
void forEach(Consumer<...> action): Führt die Aktion für jedes Element in der Queue aus.
void replaceAll(UnaryOperator<...> operator): Ersetzt alle Elemente in der Queue durch das Ergebnis der Anwendung des Operators auf das Element.
Schreiben Sie Tests für die neuen Methoden; verwenden Sie dafür Closures bzw. Lambda-Funktionen. Stellen Sie 100% Statementcoverage sicher.
Die Java Dokumentation finden Sie hier:
Übersicht: https://docs.oracle.com/en/java/javase/24/docs/api/help-doc.html
API Dokumentation: https://docs.oracle.com/en/java/javase/24/docs/api/allclasses-index.html
(Hier der Link auf die Dokumentation der Klasse UnaryOperator: https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/function/UnaryOperator.html)
Eine Warteschlange ist eine einfache Datenstruktur bei der die Elemente in der Reihenfolge entfernt werden in der diese hinzugefügt wurden (First-in-First-out (FiFo)).
Zentrale Methoden einer Warteschlange Queue<T> sind:
void enqueue(T item) (auch boolean offer(T item) bzw. void add(T item)): Fügt ein Element hinzu.
void dequeue(T item) oder T poll(): Entfernt das älteste Element und gibt es zurück.
boolean isEmpty(): Gibt an ob die Warteschlange leer ist.
Eine verkettete Liste ist eine alternative Implementierungstechnik, die flexibler ist als ein Array und dynamisch wachsen kann. Jedes Element in einer verketteten Liste enthält eine Referenz auf das nächste Element in der Liste.
Implementierung einer Warteschlange mittels verketteter Liste
Implementieren Sie eine Warteschlange (Queue<T>) basierend auf einer verketteten Liste. Ihre Klasse LinkedQueue<T> soll das folgende Interface Queue<T> implementieren.
1public interface Queue<T> {2void enqueue(T item);3T dequeue();4boolean isEmpty();5int size();67void replaceAll(UnaryOperator<T> operator);8void forEach(Consumer<T> operator);9<X> Queue<X> map(Function<T, X> mapper);10static <T> Queue<T> empty() { TODO }11}
Erklärungen
map:Erzeugt eine neue Queue<X> bei der die Elemente der neuen Queue
das Ergebnis der Anwendung der Funktion apply des Objekts mappers auf die Element der Queue sind.
empty:Erzeugt eine leere Queue.
Schreiben Sie Testfälle, um die Implementierung zu überprüfen. Zielen Sie auf mind. 100% Statementcoverage ab.
Funktionale Programmierung
Lambdas sind Ausdrücke, die (anonyme) Funktionen repräsentieren.
Es sind sowohl Referenzen auf statische Methoden als auch Instanzmethoden und sogar Konstruktoren möglich.
Currying wird nicht direkt unterstützt, aber durch die Verwendung von Funktionskomposition und partieller Anwendung kann es simuliert werden.
Warteschlangen
Queues realisieren das Konzept einer Warteschlange bei der die Elemente, die zuerst hinzugefügt wurden, auch zuerst wieder entfernt werden (FiFo).
Eine Implementierungsstrategie für Queues ist die Verwendung einer verketteten Liste.
Streams sind umgeformte Sammlungen, die durch die Umformung für funktional-orientiere Massen-Operationen geeignet sind.
Stream<T> ist der Typ der Streams mit Objekten vom Typ T
Streams mit primitiven Daten:
IntStream
LongStream
DoubleStream
Dies Streams mit primitiven Daten arbeiten in vielen Fällen effizienter jedoch sind manche Operationen nur auf Object-Streams erlaubt. „Primitive“ Streams können mit der Methode boxed in Object-Streams umgewandelt werden.
Statische Methoden in Arrays
Die Klasse java.util.Arrays hat mehrere überladene statische stream-Methoden, mit denen Arrays in Ströme umgewandelt werden können.
Die Streams können Objekte oder primitive Daten enthalten.
Statische Methoden in Stream
Das Interface java.util.stream.Stream enthält mehrere statische Methoden mit denen Streams erzeugt werden können.
Für die Klassen der Streams mit primitiven Werten (z.B. java.util.stream.IntStream) gibt es äquivalente Methoden.
Mit of werden die übergebenen Wert in einen Stream gepackt.
Mit iterate und generate hat man eine einfache Möglichkeit unendliche Ströme zu erzeugen.
Statische range-Methoden in IntStream und LongStream
Die Interfaces java.util.stream.IntStream und java.util.stream.LongStream enthalten jeweils zwei statische range-Methoden mit denen Streams erzeugt werden können.
Nicht-statische Methoden der Collection-API
Das Interface java.util.Collection enthält die Methode stream mit der die jeweilige Kollektion in einen Stream umgewandelt werden kann.
Streams werden typischerweise in einer Pipeline-artigen Struktur genutzt:
Erzeugung
Folge von Verarbeitungs-/Transformationsschritten
Abschluss mit einer terminalen Operation
Verarbeitungsoperationen
Verarbeitungs-Operationen transformieren die Elemente eines Streams. Man unterscheidet:
zustandslose Operationen
Transformieren die Elemente jeweils völlig unabhängig von allen anderen.
zustandsbehaftete Operationen
Transformieren die Elemente abhängig von anderen.
Zustandslose Verarbeitungsoperationen
map(Function<? super T,? extends R> mapper): Transformiert jedes Element in ein anderes.
filter(Predicate<? super T> predicate): Filtert Elemente heraus.
flatMap(Function<? super T,? extends Stream<? extends R>> mapper): Transformiert jedes Element in einen Stream und fügt die Streams zusammen.
Zustandsbehaftete Verarbeitungsoperationen
distinct(): Entfernt Duplikate.
sorted(): Sortiert die Elemente.
sorted(Comparator<? super T> comparator): Sortiert die Elemente mit einem gegebenen Comparator.
limit(long maxSize): Begrenzt die Anzahl der Elemente.
skip(long n): Überspringt die ersten n Elemente.
Verarbeitungsoperationen
Eine terminale Operation hat im Gegensatz zu den Verarbeitungsoperationen keinen Stream als Ergebnis.
Terminale Operationen ohne Ergebnis
forEach(Consumer<? super T> action)
Wendet die übergebene Aktion auf alle Elemente des Streams an.
Terminale Operationen mit Ergebnis
Operationen mit Array-Ergebnis: Stream => Array
Operationen die den Stream in ein äquivalentes Array umwandeln.
Operationen mit Kollektions-Ergebnis: Stream => Kollektion
Operationen die den Stream in eine äquivalente Kollektion umwandeln.
Operationen mit Einzel-Ergebnis: Aggregierende Operationen
Operationen die den Stream zu einem einzigen Wert verarbeiten.
Terminale Operationen mit Kollektions-Ergebnis
Die Methode collect erzeugt eine Kollektion aus den Elementen des Streams.
IntStream und andere Streams mit primitiven Daten haben keine entsprechende Operation.
Das Argument von collect ist ein java.util.stream.Collector. Die Erzeugung einer Kollektion ist damit Sonderfall einer aggregierenden Operation.
Für die Erzeugung einer Kollektion verwendet man typischerweise einen vordefinierten Collector aus der Klasse java.util.stream.Collectors.
Einfache Kollektionserzeuger in Collectors sind:
toList()
toSet()
toCollection(Supplier<C> collectionFactory)
In Collectors finden sich Kollektoren mit denen Maps erzeugt werden können, die eine Gruppierung bzw. eine Partitionierung der Stream-Elemente darstellen:
static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(Function<? super T,? extends K> classifier)
Gruppiert die Elemente entsprechend einer Klassifizierungsfunktion.
static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate)
Partitioniert die Elemente entsprechend einem Prädikat.
Das Interface Stream bzw. die Interfaces für Ströme primitiver Daten (IntStream, etc.) bieten einige einfache aggregierende Funktionen für Standardoperationen auf allen Elementen des Stroms.
Das Interface Stream bieten einige einfache aggregierende Funktionen für den Test aller Elemente des Stroms mit einem übergebenen Prädikat.
Das Interface Stream bietet die Funktionen findFirst und findAny für die „Suche“ nach dem ersten bzw. irgendeinem Element in einem Stream.
Das Interface Stream bietet die Funktion
Optional<T> reduce(BinaryOperator<T> accumulator)
mit der eine Funktion auf jedes Element und das bisherige Zwischenergebnis angewendet werden kann.
Falls der erste Wert nicht der Startwert sein soll, verwendet man:
Optional<T> reduce(T identity, BinaryOperator<T> accumulator)
Es gibt einen Kollektor mit dem String-Elemente zu einem String konkateniert werden können:
static Collector<CharSequence,?,String> joining(CharSequence delimiter)
Ausgewählte Eigenschaften des Basisinterface aller Streams
Parallele und sequentielle Streams.
1package java.util.stream;23public interface BaseStream<T, S extends BaseStream<T,S>> {4/** Closes the stream, releasing any resources associated with it. */5void close();67/** Returns an equivalent stream that is parallel. */8S parallel();9/** Returns an equivalent stream that is sequential. */10S sequential();11}
13public interface Stream<T> extends BaseStream<T, Stream<T>> {14// ...15}
Die Interfacedefintion (BaseStream<T, S extends BaseStream<T,S>>) ist eine Anwendung des CRTP; d. h. des Curiously Recurring Template Patterns. Bei diesem Idiom haben wir eine Klasse X, die von einer generischen Klasse oder einem generischen Interface S abgeleitet wird, wobei die ableitende Klasse X sich selber als Typparameter verwendet. Dies erlaubt die Definition einer Fluent-API, bei der Methoden, die in der Basisklasse definiert sind, den abgeleiteten Typ zurückgeben.
Erzeugen von eigenen Streams mittels StreamSupport
Die Implementierung des Interfaces Stream<T> ist ggf. sehr aufwändig. Alternativ kann die Klasse StreamSupport verwendet werden, um auf einem Spliterator basierende Streams zu erzeugen.
1package java.util.stream;23public final class StreamSupport {45/** Creates a new sequential or parallel Stream from a Spliterator. */6static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel);78// ...9}
OptionalsInstanzen der Klasse java.util.Optional<T> (bzw. java.util.OptionalInt etc.) repräsentieren Werte die vorhanden sind oder
auch nicht.
Insbesondere java.util.Optional<T> kann/sollte anstelle von null verwendet werden, in Fällen in denen unter bestimmten Umständen kein sinnvoller Wert angegeben werden kann.
Java Streams
Schreiben Sie eine Methode int sumOfSquares(int[] a) die die Elemente des Arrays quadriert und dann die Summe berechnet.
Schreiben Sie eine Methode int sumOfSquaresEven(int[] a) die die Elemente des Arrays quadriert, und dann die Summe berechnet für alle Elemente die gerade sind.
Schreiben Sie eine Methode, die eine Liste von Strings (List<String>) in eine flache Liste von Zeichen (List<Integer>) umwandelt.
Schreiben Sie eine Methode, die die Zahlen von 1 bis Integer.MAX_VALUE addiert. Nutzen Sie IntStream.range() um die Zahlen zu iterieren. Messen Sie die Ausführungsdauer für die sequentielle und parallele Ausführung (siehe Anhang für eine entsprechende Methode zur Zeitmessung.)
Um die Ausführungsdauer Ihrer Methode zu messen, können Sie folgenden Methode verwenden:
1void time(Runnable r) {2final var startTime = System.nanoTime();3r.run();4final var endTime = System.nanoTime();5System.out.println("elapsed time: "+(endTime - startTime));6}
Ein Aufruf der Methode time könnte dann so aussehen:
1 time(() -> System.out.println(sumOfSquares(new int[]{1,2,3,4,5,6,7,8,9,0})));