michael.eichberg@dhbw.de, Raum 149B
1.0
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. Stellen Sie 100% Statementcoverage sicher.
Implementierung einer Warteschlange mittels verketteter Liste
Implementieren Sie eine Warteschlange (Queue<T>
) basierend auf einer verketteten Liste. Die Klasse Queue<T>
soll folgendes Interface 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 mapper sind.
empty
:Erzeugt eine leere Queue.
Schreiben Sie Testfälle, um die Implementierung zu überprüfen. Zielen Sie auf mind. 100% Statementcoverage ab.
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 (void
).
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)
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.