Lambda-Ausdrücke, funktionale Programmierung, Streams

Lambda-Ausdrücke

Voll mein Typ: Lambda-Ausdruck verschieden typisieren

Weise den folgenden Lambda-Ausdrücken (mindestens) zwei verschiedenen funktionalen Schnittstellen zu. (Die Methoden stammen aus der Klasse String)

() -> System.out.println("lambda!");
() -> "lambda!"
s -> s.toLowerCase() s -> s.contains("-")
(s,t) -> s.contains(t)
(s,t) -> s.concat(t)
(s,ch) -> s.indexOf(ch)

Zum folgenden Lambda-Ausdruck existiert keine funktionale Schnittstelle aus der Standard-API:

(s,ch,fromIndex) -> s.indexOf(ch,fromIndex)

Realisiere zwei funktionale Schnittstellen, denen wir diesen Lambda-Ausdruck zuweisen können. Verwende Generics, um von den speziellen Typen unabhängig zu werden.

Methoden-Referenzen

Suche zu den folgenden Lambda-Ausdrücken eine passende funktionale Schnittstelle und schreiben den Lambda-Ausdruck als Methodenreferenz. Ist zum Beispiel gegeben

s -> System.out.println(s)

so lautete ein möglicher funktionaler Schnittstellentyp Consumer.

Consumer<String> var = s -> System.out.println(s);

Wie sieht nun die Methodenreferenz aus?

Finde eine Schreibeweise für die Folgenden:

s -> Integer.parseInt(s)
arr -> Arrays.sort(arr)
s -> s.toLowerCase()
sb -> sb.reverse()
s -> s.isEmpty()
(s,t) -> s.startsWith(t)
(s, cs) -> s.contains(cs)

Alle Variablennamen beziehen sich auf die Typen String (s), StringBuilder (sb) bzw. CharSequence (cs).

Entwerfe für die folgenden Lambda-Ausdrücke eine funktionale Schnittstelle und schreibe den Lambda-Ausdruck als Methodenreferenz:

(s,cs1,cs2) -> s.replace(cs1, cs2) ;
(s1, s2, in) -> s1.split(s2, in)
() -> new ArrayList<>();
s -> new Integer(s);
s -> new Integer(s);
sb -> new String(sb);
s -> new String(s);
size -> new String[size];
size -> new int[size];
(b,cs) -> new String(b, cs);

Die folgenden Lambdaausdrucke verwenden eine Variable außerhalb ihres Kontexts (capturing). Suche eine passende funktionale Schnittstelle und schreibe den Lambda-Ausdruck als Methodenreferenz:

  1. List<String> list = Arrays.asList("qqq", "www", "eee", "rrr");
    () -> list.toString();
    s -> list.add(s);
  2. String st = "abcde";
    () -> st.length();
  3. Set<String> set = new HashSet<>();
    set.addAll(Arrays.asList("leo","bale","hanks"));
    s ->set.contains(s);

Comparator-Schnittstelle

Personen vergleichen

Implementiere eine einfache Klasse Person mit String firstname, lastname. (Eine Person hat einen Vornamen und einen Nachnamen.) Gib der Person zusätzlich ein Alter age als int.

Schreibe einen Comparator, damit Personen nach Nachnamen verglichen werden. Nutze zur Implementierung:

  1. eine lokale Klasse
  2. eine anonyme Klasse
  3. einen Lambda-Ausdruck

Wo können Generics verwenden werden, wo nicht?

Erweitere den Comparator, dass bei gleichen Nachnamen zusätzlich nach dem Vornamen verglichen wird.

Comparator-Methoden

Studiere die API-Dokumentation beim Comparator zu den folgenden drei Methoden:

  1. Lege ein Array von Strings an, wobei die Strings nur aus Ziffern bestehen. Erzeuge daraus eine List. Erzeuge mit der statischen Methode Comparator.comparingInt(...)
    mit einem Lambda-Ausdruck einen Comparator für Strings und sortiere damit die Liste mit der sort()-Methode aus dem Interface List.
  2. Erzeuge aus dem Array eine zweite Liste und sortieren die Elemente abwärts mit der Collections.sort(...)-Methode unter Verwendung des Comparators aus Aufgabenteil 1.
  3. Erzeuge ein weiteres Arrays, das neben den Ziffernstrings mehrere null-Referenzen enthält. Lege mit Hilfe des Comparators aus dem 1. Teil und der passenden statischen Methode aus Comparator einen weiteren Comparator an, der null verarbeiten kann. Sortiere das Array wieder.

Comparatoren verketten

Studiere die API-Dokumentation zur Comparator-Methode thenComparing(Comparator<? super T> other).

Lege eine List von Personen an, in der Personen mit verschiedene Namen das gleiche Alter haben, aber mindestens einmal der gleiche Name doppelt mit verschiedenem Alter erscheint.

  1. Schreibe einen Comparator, der die Personen mit ihrem Namen vergleicht
    und sich auf compareTo(...) von String bezieht. Schreibe einen zweiten Comparator, der die Personen mit ihrem Alter vergleicht. Verwende dafür die passende statische Methode aus Integer. Sortiere zuerst nach Namen und unabhängig davon nach Alter.
  2. Sortiere zuerst nach Alter, dann nach Namen. Implementiere den zweiten Vergleich mit thenComparing(...).
  3. Sortiere umgekehrt nach Namen und anschließend nach Alter wieder mit thenComparing(...). Ändere dann die Verkettungsmethode und verwende thenComparingInt(...) anstelle von thenComparing(...).

Key-Extraktor

Variiere die vorherige Aufgabe wie folgt:

  1. Verwende für den Namensvergleich eine Methode, die einen Key-Extractor verwendet.
  2. Schreibe einen Comparator, der sich auf compareToIgnoreCase(...) aus String stützt. Verwende diesen mit der passenden Methode aus Comparator, die einen Key-Extractor verwendet und zwei Parameter hat; mache daraus einen Comparator für Person.

Für den Comparator, der das Alter vergleicht, gehe analog vor.

Erreiche die gleichen Sortierungen wie im letzten Beispiel, also erst unabhängig nach Namen und nach Alter, anschließend beide Reihenfolgen mit thenComparing(...).

Reihenfolge umdrehen

Schreiben einen Comparator, der eine bestehende Reihenfolge von Elementen (nicht notwendig sortiert) umdreht, also etwa aus 7, 5, 17, 11 dann 11, 17, 5, 7 macht.

Teste ihn mit Listen von zwei unterschiedlicher Typen. Die Reihenfolge soll man dann mit der sort()-Methode aus List umdrehen können.

Unsere forEach-Methode

Schreibe eine Klasse ArrayUtils mit einer generischen forEach(...)-Methode. Im ersten Parameter wird das Array übergeben, im zweiten Parameter ein passender Consumer, der durch das Array iteriert.

Schaue in die API-Dokumentation, ob (und wo) es eine forEach(...)-Methode vielleicht schon gibt. Wirf einen Blick auf die Implementierung.

Stream-API-Einstieg

Einfache Filter

  1. Baue eine ArrayList mit mehreren unterschiedlichen Elektrogeräten auf.
  2. Filtere alle Elektrogeräte heraus, die mehr als eine bestimmte Anzahl Watt verbrauchen.
  3. Filtere alle Feuermelder heraus.
  4. Wie viele Elektrogeräte bleiben übrig?

Erweiterung: Welchen Gesamtverbrauch haben die Geräte in der Liste? Tipp: Die mapXXX-Methode hilft.

IntStream

Neue Streams erzeugen mit range() bzw. rangeClosed() bzw. of()

Erzeuge mit den statischen Methoden range(...), rangeClosed(...)oder of(...) neue IntStream-Objekte.

Gib die Elemente mit der forEach(...)-Methode auf die Konsole aus.

Verwende zum einen eine Methodenreferenz und schreibe danach einen wiederverwendbaren IntConsumer, der den Stream in einer Zeile ausgibt.

Erzeugen von Streams aus einem Array, einfache terminierende Operationen

Erzeuge einen IntStream aus einem Array und verwende einfache terminierende Methoden wie average(), count(), max(), min(), sum().

Welche Möglichkeiten gibt es mit den OptionalXXX-Referenzen zu arbeiten, die von den Methoden geliefert werden?

Was ist die Aufgabe der Methoden getAsInt(), ifPresent(IntConsumer consumer), isPresent(), orElse(...)?

Lege einen leeren Stream an und schaue, was die OptionalXXX-Referenzen in diesem Fall liefern.

Neue Elemente über generate(...), terminieren mit toArray()

Erzeuge mit generate(...) einen "unendlichen" Stream von zufälligen Lottozahlen im Bereich von 1 bis 49. Schreiben dazu einen XXXSupplier, der zum Beispiel mit Math.random() arbeitet.

Verwende distinct() um Dubletten zu vermeiden und limit(...) um die Anzahl der Zahlen auf 6 zu begrenzen. Gib die Zahlen mit forEach(...) aus.

reduce(...)

Die Arbeitsweise von reduce(...) kann man sich an einem Beispiel klarmachen:

Vertical Processing

Erzeuge einen IntStream aus den Zahlen 1 bis 10. Filtere so, dass die ungeraden Zahlen übrigbleiben und bilde diese mit map(...) auf die Quadratzahlen ab. Gib alles mit forEach(...) aus.

Ergänze den Lambdaausdruck durch eine Konsolausgabe, die ausgibt, welche Funktion mit welchem Wert des Streams verwendet wird. Zeige damit die "vertikale" Abarbeitung eines Streams.

range(...) und map(...)

Verwende range(...) und map(...) und erzeuge damit einen IntStream mit konstantem Inhalt, z.B. 7 7 7 7 7.

Sortieren mit sort(...)

Lege ein ungeordnetes Array an, in dem gerade und ungerade Zahlen vorkommen. Zahlen dürfen doppelt auftreten.

Erzeugen daraus einen IntStream.

Setze einige intermediären Operationen in den Stream, sodass am Ende forEach(...) die Quadrate der ungeraden Zahlen ohne Dubletten sortiert ausgibt.

Jede Reihenfolge der intermediären Operationen führt letztlich zur selben Ausgabe. Probiere verschiedenen Reihenfolgen und überlege, ob dadurch Performanceunterschiede entstehen können.

Abflachen mit flatMap(...)

Erzeuge einen IntStream aus den Zahlen 1, 2, 3, 4, 5.

Schreibe einen Mapper, der zu jeder Zahl einen Stream mit folgendem Inhalt erzeugt:

Überlege, welchen Stream flatMap(...) daraus erzeugt. Gib den Ergebnis-Stream aus, um die Überlegung zu prüfen.

Erzeuge mit einem geeigneten Mapper den folgenden Stream:

Tipp: range(...) und map(...) können helfen.

Ergebnisse exportieren

Erzeuge einen IntStream und speichere die Werte in einem Set. Welche Möglichkeiten bieten sich an?

parallel(), forEach(), forEachOrdered()

Lege ein ungeordnetes Array an, z. B.:

int[] arr = { 10, 1, 9, 2, 8, 3, 7, 4, 6, 5 };

Für jeden der folgenden Aufgaben soll ein IntStream aus diesem Array angelegt werden:

Wie unterscheiden sich die Methoden?

Um den Verarbeitungsprozess besser zu verstehen führen wir einen aussagekräftigen IntConsumer für die Methoden peek(...), forEach(...) und forEachOrdered(...) ein. Implementiere einen IntConsumer, der zu jeder Zahl im Stream zusätzlich den Namen der Methode und den Namen des Threads ausgibt, der die Operation abarbeitet.

  1. Lege einen IntStream an und verarbeiten ihn mit der Reihenfolge parallel().peek(...).forEach(...).
  2. Legen einen zweiten IntStream an mit der Reihenfolge parallel().peek(...).forEachOrdered(...).

Vergleiche die Ereignisse und interpretiere die Konsolmeldungen.

Einsammeln mit collect(...)

Die collect(...)-Methode erwartet drei Parameter. Jeder dieser Parameter ist vom Typ einer generischen funktionalen Schnittstelle. Der Rückgabetyp von collect(...) ist immer der generische Typ des ersten Parameters.

Zu den Parametern:

Löse folgende Aufgaben: