michael.eichberg@dhbw.de, Raum 149B
1.0.1
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;
45
public 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@Override
3public 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();
67
void 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;
23
public 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;
23
public 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}
Optional
sInstanzen 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})));