- Lernen
- Informatik & Informationswissenschaften
- Programmierung
- Java-Programmierung – Klassen, Vererbung, Collections und Streams
- Lambda-Ausdrücke und Streams in Java
Generics, Lambda-Ausdrücke und Streams in Java
AnfängerEinheit 11 von 11 in Kurs: Java-Programmierung – Klassen, Vererbung, Collections und Streams
Was du lernst
- Verwende Typparameter, um Klassen, Interfaces und Methoden zu schreiben, die mit vielen Typen arbeiten, während der Compiler jede Verwendung prüft.
- Definiere und verwende generische KlassenDeklariere Klassen und Interfaces mit Typparametern, erzeuge Objekte mit Typargumenten und erkläre die Typsicherheit, die dies gegenüber dem Speichern von
Objectbietet. - Schreibe generische MethodenDeklariere Methoden mit eigenen Typparametern, rufe sie mit abgeleiteten oder expliziten Typargumenten auf und entscheide, wann eine Methode statt einer Klasse generisch sein sollte.
- Beschränke Typparameter mit Typschranken und WildcardsVerwende beschränkte Typparameter, um Methoden auf einem generischen Typ aufzurufen, erkläre, warum eine
List<Integer>keineList<Number>ist, und wähle zwischen? extendsund? super. - Beachte die Einschränkungen durch TyplöschungErkläre, wie der Compiler Typargumente löscht, welche Operationen Generics deshalb nicht erlauben und warum rohe Typen vermieden werden sollten.
- Definiere und verwende generische Klassen
- Schreibe Lambda-Ausdrücke und Methodenreferenzen für funktionale Interfaces, verwende Javas funktionale Standardinterfaces und übergib Verhalten als Wert an Methoden.
- Schreibe Lambda-Ausdrücke für funktionale InterfacesErkenne funktionale Interfaces, schreibe Lambda-Ausdrücke in Kurzform und mit Blockrumpf und erkläre, wie der Zieltyp einem Lambda-Ausdruck seine Bedeutung gibt.
- Verwende die funktionalen StandardinterfacesWähle zwischen Predicate, Function, Consumer, Supplier und verwandten Interfaces, kombiniere sie und übergib sie an Collection-Methoden wie removeIf und replaceAll.
- Fange Variablen ein und schreibe MethodenreferenzenVerwende Variablen aus dem umgebenden Code innerhalb eines Lambda-Ausdrucks, wende die Regel für effektiv finale Variablen an und ersetze einfache Lambda-Ausdrücke durch die vier Arten von Methodenreferenzen.
- Schreibe Lambda-Ausdrücke für funktionale Interfaces
- Sortiere Listen mit Comparatoren nach beliebigen Regeln, von einer selbst geschriebenen compare-Methode bis zu Verkettungen mit comparing, thenComparing und reversed.
- Schreibe einen Comparator und sortiere damitImplementiere den compare-Vertrag korrekt, sortiere Listen und Arrays mit einem Comparator und entscheide zwischen Comparable und Comparator.
- Kombiniere Comparatoren mit comparing, thenComparing und reversedErstelle Comparatoren aus Sortierschlüsseln, ergänze sekundäre Vergleichskriterien, kehre Ordnungen um, behandle null-Werte und sortiere die Einträge einer Map.
- Schreibe einen Comparator und sortiere damit
- Erstelle Pipelines mit der Stream-API, die Daten filtern, umwandeln, sortieren und reduzieren, und erkläre den Ablauf einer Pipeline.
- Baue eine Stream-Pipeline auf und verstehe ihren AblaufErzeuge Streams aus Collections, Arrays und Zahlenbereichen, unterscheide Zwischenoperationen von Terminaloperationen und erkläre die verzögerte Auswertung, die einmalige Verwendung und warum die Quelle unverändert bleibt.
- Filtere Elemente, wandle sie um und ändere ihre ReihenfolgeVerwende filter, map, mapToInt, flatMap, distinct, sorted, limit und skip und wechsle zwischen Objekt-Streams und Streams primitiver Werte.
- Reduziere einen Stream auf ein einzelnes ErgebnisBerechne mit Terminaloperationen Anzahlen, Summen, Mittelwerte, Extremwerte und Wahrheitswerte und behandle fehlende Ergebnisse mit Optional.
- Baue eine Stream-Pipeline auf und verstehe ihren Ablauf
- Sammle Stream-Ergebnisse in Listen, Mengen, Maps und Strings, gruppiere und partitioniere Daten mit Collectoren und entscheide, wann ein Stream oder eine Schleife das bessere Werkzeug ist.
- Sammle Ergebnisse in Listen, Mengen, Maps und StringsVerwende toList, Collectors.toList, toSet, toCollection, joining und toMap und gehe mit nicht modifizierbaren Ergebnissen und doppelten Schlüsseln um.
- Gruppiere und partitioniere Daten mit CollectorenTeile Daten mit groupingBy und partitioningBy in Gruppen auf und fasse jede Gruppe mit einem nachgeschalteten Collector zusammen.
- Wähle für eine vollständige Aufgabe zwischen Streams und SchleifenLöse eine Datenverarbeitungsaufgabe von Anfang bis Ende, vergleiche eine Stream-Lösung mit einer Schleife und halte Pipelines frei von Seiteneffekten.
- Sammle Ergebnisse in Listen, Mengen, Maps und Strings
Lambda-Ausdrücke und Streams in Java einfach erklärt
1Schreibe generische Klassen und Methoden
Generischer Code bewahrt eine nützliche Beziehung zwischen Eingaben, gespeicherten Werten und Ergebnissen. Eine Klasse kann diese Beziehung für ein Objekt festlegen, während eine Methode sie bei jedem Aufruf neu wählen kann. Typschranken und Wildcards ermöglichen es APIs, die benötigten Typen zu akzeptieren, ohne unsichere Schreibzugriffe zu erlauben. Der Compiler prüft diese Zusicherungen, bevor er Typparameter löscht. Das erklärt sowohl die Vorteile von Generics als auch die Laufzeitoperationen, die sie nicht unterstützen können.
1.1 Definiere und verwende generische Klassen
Ein Container sollte dir sagen, was er enthält. Mit einem Typparameter kann eine einzige Klasse Strings, Zahlen oder eigene Objekte aufnehmen, und der Compiler prüft jede Verwendung, bevor das Programm läuft.
Gib einer Klasse einen Typparameter
In class Box<T> ist T ein Typparameter: ein Name für einen Typ, den der aufrufende Code angibt. Verwende ihn für Felder, Methodenparameter und Ergebnisse.
class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}
Der Name des Konstruktors ist Box, ohne <T>. Die Deklaration hinter dem Klassennamen führt T für den Klassenrumpf ein. Erzeuge nun eine Box:
Box<String> b = new Box<>("tea");
String drink = b.get();
b.set("water");
System.out.println(drink); // tea
System.out.println(b.get()); // water
String ist das Typargument. Für diese Variable gibt get() einen String zurück und set akzeptiert einen String. Beim Lesen ist keine explizite Typumwandlung nötig. Der versuchte Aufruf b.set(42) schlägt bei der Kompilierung fehl: „incompatible types: int cannot be converted to String“. Der Fehler markiert den falschen Schreibzugriff, bevor ein späterer Lesezugriff scheitern kann.
Warum nicht Object speichern?
Ein Object-Feld kann auf einen String oder eine Ganzzahl verweisen. Code, der einen String benötigt, muss das zurückgegebene Objekt explizit umwandeln. Wurde eine Ganzzahl gespeichert, wirft diese Typumwandlung zur Laufzeit eine ClassCastException. Eine Box<String> macht die beabsichtigte Beziehung zum Teil des deklarierten Typs.
Der Diamantoperator <> in new Box<>("tea") veranlasst den Compiler, die Typargumente abzuleiten. Die Prüfungen bleiben erhalten. Lässt du die spitzen Klammern ganz weg, wie in new Box("tea"), verwendest du einen rohen Typ und verlierst Prüfungen im Zusammenhang mit dem Typparameter. Weist du eine Konstruktion mit rohem Typ einer typisierten Variablen zu, entstehen Unchecked-Warnungen; die typisierte Variable setzt ihr eigenes deklariertes Typargument bei späteren Aufrufen weiterhin durch.
Verwende Wrapperklassen und mehrere Parameter
Ein Typargument muss ein Referenztyp sein. Verwende Box<Integer>, nicht Box<int>. Java verpackt das Literal 5 beim Erzeugen von new Box<Integer>(5) als Integer und entpackt es, wenn eine Rechnung einen int benötigt. Enthält n diese Box, ergibt int next = n.get() + 1; den Wert 6.
Zwei unabhängige Typen brauchen zwei Parameter. Ein Pair<K, V> kann ein Feld K first und ein Feld V second haben. Bei einem Pair<String, Integer> geben die Getter einen String und einen Integer zurück. Übliche Namen sind T für einen Typ, E für ein Element, K und V für Schlüssel und Wert und R für ein Ergebnis.
Verwende Typparameter in Interfaces
interface Container<T> {
void put(T item);
T take();
}
Eine Klasse kann die Wahl mit class Shelf<T> implements Container<T> offenhalten oder sie mit class NameList implements Container<String> festlegen. List<E> und Comparable<T> verwenden dieselbe Idee. Eine generische Klasse bleibt eine Klasse: Box<String> und Box<Integer> beschreiben unterschiedliche Verwendungen zur Kompilierzeit, keine zwei Klassendeklarationen. Sie können einander nicht zugewiesen werden, weil sie unterschiedliche Elementtypen zusichern.
Wichtige Erkenntnisse
- Ein Typparameter benennt einen Typ innerhalb einer Deklaration; ein Typargument gibt ihn bei einer Verwendung an.
- Ein generischer Getter gibt den deklarierten Elementtyp ohne explizite Typumwandlung zurück.
- Der Diamantoperator erhält die Typprüfungen; bei einem rohen Typ entfallen generische Prüfungen.
- Verwende Wrapperklassen wie
Integerals Typargumente. - Klassen und Interfaces können mehr als einen Typparameter haben.
1.2 Schreibe generische Methoden
Manche Operationen benötigen eine Typwahl für einen Aufruf, nicht für die Lebensdauer eines Objekts. Eine generische Methode führt ihren eigenen Typparameter ein und bewahrt eine Beziehung zwischen ihrer Eingabe und ihrem Ergebnis.
Deklariere den Typ vor dem Rückgabetyp
import java.util.List;
class Lists {
public static <T> T last(List<T> list) {
return list.get(list.size() - 1);
}
public static void main(String[] args) {
int number = last(List.of(4, 8, 15));
String word = Lists.<String>last(List.of("x", "y"));
System.out.println(number); // 15
System.out.println(word); // y
}
}
Lies die Deklaration in Teilen. static macht sie zu einer Klassenmethode. <T> führt einen Typparameter ein, der zur Methode gehört. Das nächste T ist der Rückgabetyp. List<T> besagt, dass die Eingabeliste und das Ergebnis denselben Elementtyp haben.
Für den ersten Aufruf leitet Java für T den Typ Integer ab. Das Ergebnis kann dann zu int entpackt werden. Im zweiten Aufruf ist <String> ein expliziter Typzeuge, der zwischen Punkt und Methodenname steht. Lists.last(...) würde hier denselben Typ ableiten. Ein Typzeuge benötigt vor dem Punkt einen Empfänger: einen Klassennamen für eine statische Methode oder ein Objekt wie this für eine Instanzmethode.
Diese Methode erwartet eine nicht leere Liste. Bei einer leeren Liste ist size() - 1 gleich -1, sodass get eine IndexOutOfBoundsException wirft. Generics prüfen Typen; sie beweisen nicht, dass ein Index existiert.
Verknüpfe mehrere Typen
Eine generische Methode kann mehrere Parameter einführen. Wenn eine Klasse Pair<K, V> mit Konstruktor und Gettern vorhanden ist, vertauscht diese vollständige Methode die Rollen:
static <K, V> Pair<V, K> swap(Pair<K, V> p) {
return new Pair<>(p.getSecond(), p.getFirst());
}
Für ein Eingabepaar ("Ada", 36) vom Typ Pair<String, Integer> ist das Ergebnis (36, "Ada") vom Typ Pair<Integer, String>. Die Rückgabedeklaration drückt die Beziehung aus; ein Rückgabetyp Object würde sie verbergen und den aufrufenden Code zu einer expliziten Typumwandlung zwingen.
Eine Methode wie <T> int countEqual(List<T> list, T target) setzt ebenfalls Eingaben zueinander in Beziehung. Ihre Schleife kann jedes Element mit equals mit target vergleichen. Bei ["a", "b", "a", "c", "a"] und "a" ist die Anzahl 3. Die Typinferenz kann für Argumente einen gemeinsamen Obertyp wählen. Daher erzwingt die wiederholte Verwendung von T nicht immer, dass zwei Argumentausdrücke identische deklarierte Typen haben.
Entscheide, zu wem der Typparameter gehört
Eine Box<String> behält ihr Typargument für die Verwendung dieses Objekts. Lists.last wählt für jeden Aufruf ein neues Typargument. Eine gewöhnliche Hilfsklasse kann daher generische statische Methoden enthalten, ohne selbst generisch zu sein.
Eine statische Methode innerhalb von Box<T> kann das T der Klasse nicht verwenden: Die statische Methode gehört zur gemeinsam genutzten Klasse, nicht zu einer einzelnen Box. Deklariere einen eigenen Parameter, zum Beispiel static <E> Box<E> of(E value). Beginnt eine Methodendeklaration in einer gewöhnlichen Klasse mit static T first(...) ohne ein <T> für die Methode, meldet der Compiler, dass er T nicht finden kann. Ergänze die Deklaration vor dem Rückgabetyp; eine Änderung des Rückgabetyps zu Object würde die nützliche Beziehung aufgeben.
Wichtige Erkenntnisse
- Eine generische Methode deklariert ihre eigenen Typparameter vor ihrem Rückgabetyp.
- Die Typinferenz findet für jeden Aufruf getrennt statt.
- Ein Typzeuge steht nach einem Punkt und vor dem Methodennamen.
- Verwende mehrere Typparameter, wenn Eingabe und Ausgabe mehrere miteinander verknüpfte Rollen haben.
- Eine statische Methode muss ihren eigenen Typparameter deklarieren.
- Typsicherheit prüft nicht, ob eine Methode die geforderte nicht leere Eingabe erhält.
1.3 Beschränke Typparameter mit Typschranken und Wildcards
Ein unbeschränktes T unterstützt nur Operationen, die auf Object verfügbar sind. Typschranken ergänzen eine nützliche Zusicherung. Wildcards ermöglichen es einem Parameter, eine Familie generischer Typen zu akzeptieren, ohne vorzugeben, dass alle ihre Elementtypen identisch sind.
Fordere die benötigten Operationen
static <T extends Comparable<T>> T largest(List<T> list) {
T best = list.get(0);
for (T x : list) {
if (x.compareTo(best) > 0) {
best = x;
}
}
return best;
}
Die Typschranke besagt, dass ein T sich mit einem anderen T vergleichen kann. Ausgehend von 3 ersetzt der Aufruf largest(List.of(3, 11, 7)) den bisher größten Wert durch 11 und lässt ihn bei 7 unverändert. Er gibt 11 zurück. Für ["pear", "apple", "plum"] ist "plum" in natürlicher Ordnung das größte Wort. Eine leere Liste scheitert bei get(0).
extends führt auch für Interfaces Typschranken ein. <T extends Number> macht doubleValue() verfügbar. Mehrere Typschranken verwenden &, wobei eine Klasse, falls vorhanden, an erster Stelle steht: <T extends Number & Comparable<T>>.
Warum List keine List ist
Integer erweitert Number, aber List<Integer> ist kein Untertyp von List<Number>. Generische Typen sind invariant. Wäre diese Zuweisung erlaubt, könnte Code über die Referenz vom Typ List<Number> ein Double hinzufügen; der ursprüngliche Besitzer würde es dann als Integer lesen. Ein Parameter vom Typ List<Number> akzeptiert Implementierungen wie ArrayList<Number>, lehnt aber List<Integer> ab.
Lies mit extends; füge mit super hinzu
static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
static void addOneToThree(List<? super Integer> target) {
target.add(1);
target.add(2);
target.add(3);
}
? extends Number bedeutet einen unbekannten Typ, der Number oder ein Untertyp davon ist. sum akzeptiert Listen von Ganzzahlen, Double-Werten oder Zahlen. Die Methode liest jedes Element als Number. sum(List.of(1.5, 2.5)) ergibt 4.0. Sie kann 3 nicht hinzufügen: Der unbekannte Elementtyp könnte Double sein. Die Typregeln erlauben das Hinzufügen von null, auch wenn eine bestimmte Liste dies zur Laufzeit ablehnen kann.
? super Integer bedeutet Integer oder einen Obertyp davon. Die zweite Methode kann Ganzzahlen zu einer modifizierbaren List<Integer>, List<Number> oder List<Object> hinzufügen. Ein über diesen Parameter gelesener Wert hat nur den Typ Object: Vorhandene Elemente könnten andere Zahlen oder Objekte sein.
| Parameter | Sichere Operation auf Elementen |
|---|---|
List<? extends T> | Ein Element als T lesen |
List<? super T> | Ein T hinzufügen; als Object lesen |
List<T> | Mit demselben benannten Typ lesen und schreiben |
List<?> | Als Object lesen; Größe abfragen |
Die Merkhilfe lautet PECS-Regel (Producer extends, consumer super). Sie beschreibt die Richtung der Elementübertragung, nicht ob die gesamte Collection unveränderlich ist. Beispielsweise kann das Entfernen von Elementen über eine Sicht mit extends weiterhin möglich sein.
Verwende einen benannten Typparameter, wenn ein Typ mehrere Positionen verknüpfen muss, insbesondere einen Eingabe- und einen Rückgabetyp. Verwende eine Wildcard, wenn nur ein Bereich akzeptierter Eingaben wichtig ist. Collections.copy liest aus List<? extends T> und schreibt in List<? super T>; das Ziel muss bereits genügend Positionen zum Ersetzen haben.
Wichtige Erkenntnisse
- Typschranken machen die Methoden des als Schranke angegebenen Typs für
Tverfügbar. - Die Invarianz generischer Typen schützt eine Liste vor Schreibzugriffen mit dem falschen Elementtyp.
- Verwende
? extends T, um Elemente alsTzu lesen. - Verwende
? super T, um Werte vom TypThinzuzufügen; gelesene Werte haben den TypObject. - Eine Wildcard beschränkt Operationen über eine Referenz; sie macht das Objekt nicht unveränderlich.
- Verwende einen benannten Typparameter, um Eingabe- und Ergebnistypen zu verknüpfen.
Collections.copyersetzt vorhandene Positionen im Ziel.
1.4 Beachte die Einschränkungen durch Typlöschung
Generics geben dem Compiler mehr Informationen, als ein gewöhnlicher Klassentest zur Laufzeit hat. Wenn du verstehst, was nach der Kompilierung erhalten bleibt, erklären sich Warnungen zu rohen Typen und mehrere Operationen, die Java ablehnt.
Was die Typlöschung entfernt
Java prüft Typargumente und ersetzt dann Typparameter durch ihre Typschranken. Ein unbeschränktes T wird zu Object; ein T extends Number wird zu Number. Der Compiler fügt beim Lesen von Werten dort, wo nötig, explizite Typumwandlungen ein. Zur Laufzeit gibt es eine Klasse Box, daher gibt dies true aus:
System.out.println(
new Box<String>("a").getClass()
== new Box<Integer>(1).getClass()
);
Die Deklaration von Box kann die generische Klasse mit einem Wertefeld, einem Konstruktor und einem Getter sein. Beide Konstruktionen haben dieselbe Laufzeitklasse, obwohl ihre deklarierten Verwendungen unterschiedliche Prüfungen zur Kompilierzeit haben.
Operationen, die einen bekannten Laufzeittyp benötigen
Innerhalb einer generischen Klasse lässt sich new T() nicht kompilieren. Java weiß nicht, welchen Konstruktor der vom aufrufenden Code angegebene Typ hat. Nimm eine Fabrik entgegen, um den Wert zu erzeugen:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
class Factory {
static <T> T create(Supplier<T> factory) {
return factory.get();
}
public static void main(String[] args) {
List<String> words = create(ArrayList::new);
words.add("ready");
System.out.println(words); // [ready]
}
}
Der Supplier stellt das Verhalten zum Erzeugen bereit. Auch new T[10] und new List<Integer>[2] werden als generische Array-Erzeugung abgelehnt. Speichere eine Folge in einer ArrayList<T>, wenn du das Verhalten eines Arrays nicht benötigst.
Für ein Object obj kann obj instanceof List<String> das Element-Typargument nicht sicher prüfen. Verwende obj instanceof List<?>, um zu prüfen, ob es sich um eine Liste handelt, und behandle ihre Elemente dann als unbekannt. Dieser Test weist keinen String-Elementtyp nach.
Gemeinsame statische Mitglieder und Überladungen nach Typlöschung
Ein Feld wie static T shared kann das T der umgebenden Klasse nicht verwenden. Alle Parametrisierungen teilen sich das statische Mitglied. Eine statische Methode kann ein eigenes <E> einführen, unabhängig vom Klassenparameter.
Zwei Überladungen print(Set<String> s) und print(Set<Integer> s) können nicht in einer Klasse nebeneinander bestehen. Beide werden durch die Typlöschung zu einer Methode, die Set entgegennimmt, sodass ein Namenskonflikt entsteht. Gib den Operationen unterschiedliche Namen oder entwirf eine Methode, die das gemeinsame Verhalten behandelt.
Verfolge eine Unchecked-Warnung bis zum fehlerhaften Lesezugriff
List raw = new ArrayList();
raw.add("text");
raw.add(42);
for (Object value : raw) {
String text = (String) value;
System.out.println(text);
}
Dies gibt text aus, dann wirft die Typumwandlung von 42 eine ClassCastException. Die Deklaration mit rohem Typ kann eine Meldung zum rohen Typ auslösen; die Aufrufe zum Hinzufügen lösen Unchecked-Warnungen aus, weil generische Prüfungen umgangen wurden. Ersetzt du den rohen Typ durch List<String>, tritt der Fehler bei add(42) zur Kompilierzeit auf. Eine Unchecked-Warnung zeigt eine verlorene Garantie und das Risiko eines Fehlers bei der Ausführung an. Behebe die Verwendung des rohen Typs oder die unsichere Typumwandlung, statt anzunehmen, dass ein späterer Lesezugriff sicher sein wird.
Wichtige Erkenntnisse
- Die Typlöschung ersetzt Typparameter durch ihre Typschranken oder durch
Object, wenn sie unbeschränkt sind. - Parametrisierte Verwendungen teilen sich eine Laufzeitklasse.
- Verwende eine übergebene Fabrik statt
new T(). - Parametertypen nach Typlöschung können generische Überladungen nicht unterscheiden.
2Übergib Verhalten mit Lambda-Ausdrücken
Eine Methode kann sowohl Verhalten als auch Daten entgegennehmen. Funktionale Interfaces beschreiben die Form dieses Verhaltens, und Lambda-Ausdrücke liefern die auszuführende Arbeit. Die Standardinterfaces geben häufigen Aufgaben klare Namen: prüfen, umwandeln, ausführen und bereitstellen. Eingefangene Variablen ermöglichen es einer Regel, Informationen aus ihrer Umgebung zu verwenden, während Methodenreferenzen vorhandenes Verhalten benennen. Der passende Zieltyp verbindet jedes Syntaxelement mit einem gewöhnlichen Methodenaufruf.
2.1 Schreibe Lambda-Ausdrücke für funktionale Interfaces
Eine Methode kann eine Regel als Argument entgegennehmen. Ein Lambda-Ausdruck formuliert diese Regel dort, wo sie verwendet wird, während ein funktionales Interface Java mitteilt, welche Eingaben die Regel entgegennimmt und was sie zurückgibt.
Eine abstrakte Methode erfüllen
Ein funktionales Interface hat eine zu implementierende abstrakte Methode. Deklarationen, die öffentlichen Instanzmethoden von Object entsprechen, etwa equals(Object), zählen nicht mit. Default-Methoden und statische Methoden fügen keine abstrakten Aufgaben hinzu. Comparator<T> und Runnable sind funktionale Interfaces.
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
class Combine {
static int combine(int a, int b, Operation op) {
return op.apply(a, b);
}
public static void main(String[] args) {
Operation add = (a, b) -> a + b;
System.out.println(add.apply(3, 4)); // 7
System.out.println(combine(3, 4, (x, y) -> x * y)); // 12
}
}
Das Interface liefert den Methodennamen apply, die beiden Parametertypen int und den Ergebnistyp int. Der Lambda-Ausdruck liefert die Parameternamen und den Rumpf. Er erstellt eine Implementierung, die über Operation verwendet werden kann; apply wird erst aufgerufen, wenn das Programm die Methode aufruft.
combine übergibt seine beiden Zahlen immer an op. Der aufrufende Code ändert das Verhalten, indem er Addition oder Multiplikation vorgibt. Auf diese Weise erhält auch sort eine Ordnungsregel und filter eine Bedingung.
Die Syntax lesen
Ein Lambda-Ausdruck hat Parameter, -> und danach einen Ausdruck oder einen Block. Bei einem einzelnen Parameter mit abgeleitetem Typ können die Klammern entfallen: w -> w.length() > 5. Bei keinem Parameter ist () nötig, und bei zwei oder mehr Parametern sind Klammern nötig, wie in (a, b) -> a * b. Explizite Typen sind erlaubt: (int a, int b) -> a + b.
Ein Ausdrucksrumpf gibt sein Ergebnis automatisch zurück. Ein Block, der einen Wert liefert, muss ihn zurückgeben:
Operation max = (a, b) -> {
if (a > b) {
return a;
}
return b;
};
System.out.println(max.apply(3, 9)); // 9
Jeder Pfad durch diesen Block gibt einen int zurück. Geschweifte Klammern um a + b ohne return ergeben keinen gültigen Block, der einen Wert zurückgibt.
Der Zieltyp legt die Bedeutung fest
Der Zieltyp ist das funktionale Interface, das bei einer Zuweisung, einem Argument oder einer Rückgabe erwartet wird. Der Text x -> x * 2 kann zu Function<Integer, Integer> oder UnaryOperator<Integer> passen. Seine Parameter- und Ergebnistypen stammen aus diesem Zieltyp. Object o = () -> 1; schlägt fehl, weil Object keine funktionale Methode vorgibt.
@FunctionalInterface veranlasst den Compiler, die Interface-Definition zu prüfen. Das schützt sinnvollerweise davor, dass später jemand eine zweite unterschiedliche abstrakte Methode hinzufügt. Ohne die Annotation kann ein solches Interface weiterhin deklariert werden, aber ein Lambda-Ausdruck kann seine beiden voneinander unabhängigen Anforderungen nicht implementieren.
Ein Lambda-Ausdruck kann auch mit lokalen Namen in Konflikt geraten. Wenn die umgebende Methode bereits int a deklariert, kann sie in diesem Gültigkeitsbereich keinen Lambda-Parameter namens a deklarieren. Wähle einen anderen Parameternamen. Im Vergleich zu einer anonymen Klasse, die Comparator<String> implementiert, erspart ein Lambda-Ausdruck die wiederholte Klassen- und Methodendeklaration; die Berechnung der Ordnung muss weiterhin den Comparator-Vertrag erfüllen.
Wichtige Erkenntnisse
- Ein Lambda-Ausdruck liefert die Implementierung der abstrakten Methode eines funktionalen Interfaces.
- Das Zielinterface bestimmt die Eingabetypen, den Ergebnistyp und den Methodennamen.
- Ausdrucksrümpfe geben einen Wert direkt zurück; Blöcke, die einen Wert zurückgeben, benötigen
return. @FunctionalInterfaceprüft die Interface-Definition.- Ein Lambda-Parameter kann eine umgebende lokale Variable nicht erneut deklarieren.
2.2 Verwende die funktionalen Standardinterfaces
Java hat bereits Namen für die häufigen Formen von Verhalten. Wähle ein Interface anhand der Anzahl der Eingaben und der Art des Ergebnisses und rufe dann seine funktionale Methode auf, um das Verhalten auszuführen.
Eine Form für Eingabe und Ausgabe wählen
| Interface | Funktionale Methode | Aufgabe |
|---|---|---|
Predicate<T> | boolean test(T t) | Eine Bedingung prüfen |
Function<T, R> | R apply(T t) | Einen Wert umwandeln |
Consumer<T> | void accept(T t) | Eine Aktion ausführen |
Supplier<T> | T get() | Ohne Eingabe einen Wert liefern |
Diese Interfaces stehen in java.util.function. Für Strings passt w -> w.length() > 5 zu einem Predicate, String::length zu einer Function, die Integer zurückgibt, und System.out::println zu einem Consumer. Ein Supplier wie () -> new ArrayList<String>() erzeugt bei jeder Ausführung von get() eine neue Liste. Eine von filter verwendete Bedingung benötigt Predicate<T>, auch wenn eine Function<T, Boolean> in anderem Code ebenfalls einen booleschen Wert berechnen könnte.
UnaryOperator<T> ist eine Funktion von T auf dasselbe T. BinaryOperator<T> verbindet zwei Werte vom Typ T zu einem T. BiFunction<T, U, R>, BiPredicate<T, U> und BiConsumer<T, U> nehmen zwei Eingaben entgegen. Primitive Spezialisierungen vermeiden das Verpacken: IntPredicate.test(int), IntBinaryOperator.applyAsInt(int, int) und ToIntFunction<T>.applyAsInt(T).
Regeln in einer festgelegten Reihenfolge kombinieren
import java.util.function.Function;
import java.util.function.Predicate;
class Rules {
public static void main(String[] args) {
Predicate<String> isLong = w -> w.length() > 5;
Predicate<String> longB = isLong.and(w -> w.startsWith("b"));
System.out.println(longB.test("banana")); // true
System.out.println(isLong.negate().test("kiwi")); // true
Function<Integer, Integer> twice = x -> x * 2;
Function<Integer, Integer> inc = x -> x + 1;
System.out.println(twice.andThen(inc).apply(5)); // 11
System.out.println(twice.compose(inc).apply(5)); // 12
}
}
and verlangt beide Bedingungen, or mindestens eine und negate kehrt die Antwort um. Predicate.not(p) negiert ebenfalls ein Predicate. andThen führt zuerst den Empfänger aus; compose führt zuerst sein Argument aus. Hier berechnet die erste Verkettung 10 und dann 11, während die zweite 6 und dann 12 berechnet.
Eine Regel an eine Methode übergeben
static <T> List<T> keep(List<T> list, Predicate<T> test) {
List<T> kept = new ArrayList<>();
for (T x : list) {
if (test.test(x)) kept.add(x);
}
return kept;
}
keep(List.of(4, 9, 12, 15), n -> n % 3 == 0) ergibt [9, 12, 15]. Dank T lässt sich die Methode für verschiedene Elementtypen wiederverwenden; das Predicate macht das Auswahlverhalten wiederverwendbar.
Für eine modifizierbare Liste [3, 8, 1, 10, 6] lässt removeIf(n -> n < 5) die Liste [8, 10, 6] übrig. replaceAll(n -> n * 2) ergibt danach [16, 20, 12]. Diese Methoden ändern die Liste; forEach akzeptiert einen Consumer für Aktionen wie die Ausgabe. Kopiere ein Ergebnis von List.of(...) in eine ArrayList, bevor du ändernde Operationen verwendest.
Für Maps nimmt forEach((key, value) -> ...) einen BiConsumer entgegen. merge(word, 1, Integer::sum) speichert 1 für ein neues Wort und erhöht eine vorhandene Anzahl. computeIfAbsent(key, k -> new ArrayList<>()) erzeugt einen Wert für einen fehlenden oder auf null abgebildeten Schlüssel und gibt den resultierenden Wert zurück. Diese Methoden halten die Regel für Abfrage und Aktualisierung zusammen.
Wichtige Erkenntnisse
- Wähle ein funktionales Interface anhand der Form seiner Eingabe und seines Ergebnisses.
andThenführt zuerst den Empfänger aus;composeführt zuerst sein Argument aus.removeIfprüft ein Predicate;replaceAllwendet einen unären Operator an.mergeverbindet einen vorhandenen Map-Wert mit einem übergebenen Wert.computeIfAbsentkann einen fehlenden Map-Wert erstellen und speichern.- Spezialisierungen für primitive Datentypen arbeiten direkt mit primitiven Werten.
2.3 Fange Variablen ein und schreibe Methodenreferenzen
Ein Lambda-Ausdruck kann Informationen aus seiner umgebenden Methode verwenden. Wenn sein Rumpf nur eine vorhandene Methode aufruft, drückt eine Methodenreferenz dasselbe Verhalten oft direkter aus.
Eine lokale Variable einfangen
int limit = 10;
Predicate<Integer> small = n -> n < limit;
System.out.println(small.test(7)); // true
Der Lambda-Ausdruck verwendet limit durch Einfangen. Eine eingefangene lokale Variable muss final oder effektiv final sein: Ihr wird einmal ein Wert zugewiesen und sie erhält danach keine Neuzuweisung. Fügt man irgendwo nach dieser ersten Zuweisung limit = 20; hinzu, lässt sich das Einfangen nicht mehr kompilieren, selbst wenn die Neuzuweisung vor dem Lambda-Ausdruck steht.
Die Regel gilt für die lokale Variable, nicht für den gesamten Zustand ihres Objekts. Dies lässt sich kompilieren:
StringBuilder log = new StringBuilder();
Runnable action = () -> log.append("ran");
action.run();
System.out.println(log); // ran
log verweist weiterhin auf denselben Builder. Auch das Aktualisieren eines Felds des umgebenden Objekts ist erlaubt, aber versteckte Änderungen können es erschweren, das Verhalten nachzuvollziehen.
Der Versuch, numbers.forEach(n -> total += n) mit einem lokalen int total = 0 zu verwenden, schlägt fehl, weil er die eingefangene Variable ändert. Verwende eine Schleife, wenn die Aufgabe eine Aktualisierung ist, oder berechne ein Ergebnis direkt mit numbers.stream().mapToInt(n -> n).sum().
Die vier Referenzformen lesen
Eine Methodenreferenz verwendet :: ohne Argumentliste. Das funktionale Interface als Zieltyp liefert die Eingaben und das erwartete Ergebnis.
| Art | Referenz | Entsprechender Lambda-Ausdruck |
|---|---|---|
| Statische Methode | Integer::parseInt | s -> Integer.parseInt(s) |
| Gebundene Instanzmethode | System.out::println | x -> System.out.println(x) |
| Ungebundene Instanzmethode | String::length | s -> s.length() |
| Konstruktor | ArrayList::new | () -> new ArrayList<>() |
Bei der gebundenen Referenz steht das Empfängerobjekt bereits fest: der PrintStream, auf den System.out verweist. Bei der ungebundenen Referenz wird das erste Argument des funktionalen Interfaces zum Empfängerobjekt. So entspricht String::compareToIgnoreCase dem Ausdruck (a, b) -> a.compareToIgnoreCase(b): Der erste String ist das Empfängerobjekt und der zweite das Methodenargument.
Die Referenz auf einen Zieltyp abstimmen
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
class References {
public static void main(String[] args) {
Function<String, Integer> parse = Integer::parseInt;
Function<String, Integer> length = String::length;
Supplier<List<String>> fresh = ArrayList::new;
System.out.println(parse.apply("42")); // 42
System.out.println(length.apply("stream")); // 6
System.out.println(fresh.get()); // []
}
}
Auch eine Konstruktorreferenz kann Argumente entgegennehmen. Student::new passt zu einer Function<String, Student>, wenn ein passender Konstruktor einen String entgegennimmt. Der Zieltyp bestimmt, welche Konstruktorüberladung ausgewählt wird.
Student::getName verweist auf einen Instanz-Getter für jeden übergebenen Studierenden; ein statischer Getter ist nicht nötig. Schreibe einen Lambda-Ausdruck, wenn zusätzliche Arbeit anfällt, wie in s -> s.getPoints() * 2 oder w -> w.length() > 5. String::length() zu schreiben ist ungültig: Die Referenz benennt Verhalten, ohne es an dieser Stelle aufzurufen.
Wichtige Erkenntnisse
- Eine eingefangene lokale Variable muss final oder effektiv final sein.
- Eine finale Referenz kann weiterhin auf ein veränderliches Objekt verweisen.
- Eine gebundene Referenz legt ihr Empfängerobjekt fest; eine ungebundene Referenz bezieht es aus dem ersten Argument.
- Konstruktorreferenzen verwenden
ClassName::new. - Methodenreferenzen haben keine Aufrufklammern.
3Ordne Objekte mit Comparator
Für dieselben Objekte können mehrere sinnvolle Ordnungen nötig sein: Namen alphabetisch, Punkte absteigend oder Gruppen mit einem sekundären Vergleichskriterium für die Punktzahl. Comparatoren halten diese Regeln von der natürlichen Ordnung der Klasse getrennt. Ein korrekter Vergleich muss die Beziehungen zwischen Vorzeichen und Reihenfolge konsistent einhalten. Erzeugermethoden machen die Schlüssel sichtbar, aber die Position einer Umkehrung und die Behandlung von Gleichständen oder Nullwerten bestimmen weiterhin das Ergebnis.
3.1 Schreibe einen Comparator und sortiere damit
Die natürliche Ordnung eines Objekts ist nicht immer die Ordnung, die eine Aufgabe benötigt. Mit einem separaten Comparator<T> kannst du dieselben Objekte nach unterschiedlichen Regeln sortieren, ohne ihre Klasse zu ändern.
Das Vorzeichen von compare lesen
compare(a, b) gibt eine negative Zahl zurück, wenn a zuerst kommt, null, wenn beide gleich eingeordnet werden, und eine positive Zahl, wenn a nach b kommt. Nur das Vorzeichen zählt. Ein Comparator gibt keinen booleschen Wert zurück.
Comparable<T> gehört zur Klasse und liefert über compareTo ihre natürliche Ordnung. Ein Comparator ist ein separater Wert, der an eine Sortiermethode übergeben wird. Strings haben eine natürliche Zeichenordnung, aber ein Comparator kann sie stattdessen nach Länge sortieren.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class SortWords {
public static void main(String[] args) {
List<String> words = new ArrayList<>(List.of("ccc", "a", "bb"));
Comparator<String> byLength =
(a, b) -> Integer.compare(a.length(), b.length());
words.sort(byLength);
System.out.println(words); // [a, bb, ccc]
}
}
Der Comparator ermittelt die Längen und vergleicht die Zahlen. words.sort ändert die modifizierbare Liste und gibt void zurück; die Methode gibt keine neue Liste zurück. Das Sortieren eines Ergebnisses von List.of(...) wirft eine UnsupportedOperationException, daher erstellt das Beispiel zunächst eine ArrayList-Kopie.
Zahlen sicher vergleichen
Verwende Integer.compare oder Double.compare. Eine Subtraktion kann das Vorzeichen durch Überlauf umkehren: 2_000_000_000 - (-2_000_000_000) ergibt als int den Wert -294.967.296. Dadurch steht die größere Zahl bei aufsteigender Ordnung fälschlich zuerst. Integer.compare(2_000_000_000, -2_000_000_000) gibt korrekt 1 zurück.
Auch die explizite Umwandlung einer Differenz von Double-Werten in int verliert Informationen. (int) (1.2 - 0.7) ist 0, obwohl der erste Wert größer ist. Double.compare(1.2, 0.7) meldet ein positives Ergebnis.
Die Ordnung konsistent halten
Das Vertauschen der Eingaben muss das Vorzeichen des Vergleichs umkehren. Wenn a vor b und b vor c steht, muss a vor c stehen. Werte, deren Vergleich null ergibt, müssen gegenüber jedem anderen Wert gleich eingeordnet werden. Ein Verstoß gegen diese Regeln kann eine falsche Reihenfolge ergeben oder dazu führen, dass die Sortierung eine IllegalArgumentException mit „Comparison method violates its general contract!“ wirft. Gibt man nur 0 oder 1 zurück, lassen sich nicht beide Richtungen ausdrücken.
Javas Listensortierungen und Comparator-basierte Sortierungen von Objekt-Arrays sind stabil: Bei Gleichständen bleibt die ursprüngliche Reihenfolge erhalten. Für die Studierenden Ada 85, Ben 72, Chen 85, Dara 90 und Eli 64 ergibt das Sortieren nach aufsteigender Punktzahl Eli, Ben, Ada, Chen, Dara. Ada bleibt vor Chen, weil beide gleich viele Punkte haben und Ada zuerst vorkam.
Die Beispiele mit Studierenden verwenden diese Klasse und eine modifizierbare Liste:
class Student {
private final String name;
private final String group;
private final int points;
Student(String name, String group, int points) {
this.name = name;
this.group = group;
this.points = points;
}
String getName() { return name; }
String getGroup() { return group; }
int getPoints() { return points; }
}
List<Student> students = new ArrayList<>(List.of(
new Student("Ada", "A", 85), new Student("Ben", "B", 72),
new Student("Chen", "A", 85), new Student("Dara", "B", 90),
new Student("Eli", "A", 64)));
students.sort(Comparator.comparingInt(Student::getPoints));
students.forEach(s -> System.out.println(s.getName()));
// Eli, Ben, Ada, Chen, Dara, each on a separate line
Einen Sortieraufruf wählen
list.sort(cmp) und Collections.sort(list, cmp) ändern eine modifizierbare Liste. list.sort(null) fordert die natürliche Ordnung an. Arrays.sort(array, cmp) akzeptiert ein Objekt-Array wie Integer[]; für ein int[] gibt es keine Überladung mit Comparator.
Die natürliche String-Ordnung vergleicht UTF-16-Codeeinheiten. Die Großbuchstaben A–Z stehen vor den Kleinbuchstaben a–z, sodass aus ["banana", "Cherry", "apple"] die Liste [Cherry, apple, banana] wird. String.CASE_INSENSITIVE_ORDER ergibt für diese Wörter [apple, banana, Cherry]. Wähle die Textregel ausdrücklich, wenn die Groß- und Kleinschreibung die Reihenfolge nicht bestimmen soll.
Wichtige Erkenntnisse
- Ein Comparator gibt ein Vergleichsergebnis mit Vorzeichen zurück, keinen booleschen Wert.
Comparableliefert die natürliche Ordnung; ein Comparator liefert eine separate Ordnung.- Verwende numerische Vergleichsmethoden, um Überlauf und Abschneiden zu vermeiden.
- Stabile Sortierungen erhalten bei Gleichständen die ursprüngliche Reihenfolge.
- Das Sortieren einer Liste ändert eine modifizierbare Liste und gibt
voidzurück. - Arrays primitiver Werte akzeptieren keinen Comparator.
3.2 Kombiniere Comparatoren mit comparing, thenComparing und reversed
Eine Ordnung nach mehreren Schlüsseln lässt sich am leichtesten als Folge von Entscheidungen lesen: Vergleiche den Hauptschlüssel und vergleiche einen zweiten Schlüssel nur dann, wenn der erste Vergleich einen Gleichstand ergibt. Comparator-Erzeugermethoden drücken diese Entscheidungen direkt aus.
Einen Schlüssel extrahieren und Gleichstände auflösen
Für einen Student mit getName(), getGroup() und getPoints() ordnet Comparator.comparing(Student::getName) nach Namen. comparingInt(Student::getPoints) extrahiert einen int, ohne ihn zu verpacken.
Comparator<Student> ranking =
Comparator.comparingInt(Student::getPoints)
.reversed()
.thenComparing(Student::getName);
Dies ordnet hohe Punktzahlen zuerst und bei gleichen Punkten die Namen aufsteigend. Für Ada (A, 85), Ben (B, 72), Chen (A, 85), Dara (B, 90), Eli (A, 64) ergibt sich Dara, Ada, Chen, Ben, Eli. Der Namensvergleich wird nur bei einem Punktegleichstand ausgeführt.
reversed auf der richtigen Ebene platzieren
reversed() kehrt alles zuvor Erstellte um. Würde man die Rangfolge nach thenComparing umkehren, würden auch die Namen innerhalb jedes Punktegleichstands umgekehrt. Um Gruppen aufsteigend und Punkte innerhalb jeder Gruppe absteigend zu ordnen, kehre nur den Punkte-Comparator um:
Comparator<Student> byGroup =
Comparator.comparing(Student::getGroup)
.thenComparing(
Comparator.comparingInt(Student::getPoints).reversed());
Das Ergebnis ist Ada, Chen, Eli, Dara, Ben: Gruppe A steht vor B; innerhalb von A steht 85 vor 64. Würde man stattdessen .reversed() ans Ende setzen, würden gegenüber der vorherigen Verkettung sowohl die Gruppen- als auch die Punktreihenfolge umgekehrt.
Regeln für Text und null wählen
Comparator.naturalOrder() folgt Comparable; reverseOrder() kehrt diese Ordnung um. Gib einen Schlüssel-Comparator an, wenn ein Schlüssel eine besondere Behandlung benötigt, wie in Comparator.comparing(Student::getName, String.CASE_INSENSITIVE_ORDER).
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class SortNullable {
public static void main(String[] args) {
List<String> words = new ArrayList<>();
words.add("b");
words.add(null);
words.add("a");
words.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(words); // [null, a, b]
}
}
Umschließe den Vergleich auf der Ebene, auf der null vorkommt. nullsLast um den gesamten Student-Comparator behandelt null-Studentenobjekte. Eine Funktion zum Extrahieren des Namens, die null zurückgibt, benötigt einen Comparator mit Nullbehandlung für den Namensschlüssel selbst. Comparatoren für die natürliche Ordnung und gewöhnliche Schlüssel-Comparatoren behandeln null-Schlüssel nicht automatisch.
Map-Einträge sortieren und Typinferenz lösen
Eine HashMap sichert keine Begegnungsreihenfolge zu. Verarbeite ihre Einträge als Stream und wähle ausdrücklich eine Ordnung:
stock.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(System.out::println);
Für rice=12, oats=3 und flour=7 gibt dies rice=12, flour=7, oats=3 in getrennten Zeilen aus. Es ordnet die Einträge im Stream, ohne stock neu zu ordnen. Ergänze thenComparing(Map.Entry.comparingByKey()), wenn Werte mit Gleichstand eine vorhersehbare Schlüsselreihenfolge benötigen.
Das explizite <String, Integer> unterstützt die Typinferenz über den verketteten Aufruf hinweg. Ebenso kann Comparator.comparing(s -> s.getName()).reversed() für s den Typ Object ableiten. Verwende Student::getName oder (Student s) -> s.getName(), um den fehlenden Typ anzugeben. Ein benannter Comparator kann beim Sortieren von Listen, mit den Stream-Operationen sorted, min und max oder mit einer geordneten Map oder Menge wiederverwendet werden.
Wichtige Erkenntnisse
comparingextrahiert einen Schlüssel; Erzeugermethoden für primitive Typen vermeiden das Verpacken numerischer Schlüssel.thenComparingwird nur verwendet, wenn der vorherige Vergleich einen Gleichstand ergibt.reversed()kehrt den bereits erstellten Comparator um.- Behandle Nullwerte dort, wo sie auftreten: bei ganzen Objekten oder extrahierten Schlüsseln.
- Das Sortieren eines Streams von Einträgen ordnet seine Quell-Map nicht neu.
4Verarbeite Collections mit Streams
Eine Stream-Pipeline wandelt eine Collection in eine Folge von Verarbeitungsschritten um. Verfolge die Werte und ihre Typen, während ein Filter auswählt, eine Abbildung sie ändert und eine Terminaloperation das Ergebnis erstellt. Verzögerte Ausführung bedeutet, dass manche Elemente möglicherweise nie durchlaufen werden, während das Sortieren die gesamte Eingabe benötigt. Numerische Streams und optionale Werte machen Ergebnistypen deutlich, auch für den Fall, dass keine Werte übrig bleiben oder eine numerische Summe zu groß ist.
4.1 Baue eine Stream-Pipeline auf und verstehe ihren Ablauf
Ein Stream beschreibt, wie eine Folge von Werten verarbeitet wird. Er liest aus einer Quelle, leitet Elemente durch Zwischenoperationen und endet mit einer Terminaloperation, die ein Ergebnis liefert oder eine Aktion ausführt.
Die drei Teile erkennen
import java.util.List;
class CountPasses {
public static void main(String[] args) {
List<Integer> points = List.of(72, 45, 90, 58, 81);
long passed = points.stream()
.filter(p -> p >= 60)
.count();
System.out.println(passed); // 3
System.out.println(points); // [72, 45, 90, 58, 81]
}
}
points.stream() ist die Quelle. filter ist eine Zwischenoperation: Sie gibt einen weiteren Stream zurück. count ist die Terminaloperation: Sie gibt einen long zurück. Die Werte 72, 90 und 81 bestehen den Filter. Das Lesen über den Stream hat 45 und 58 nicht aus points entfernt.
Eine Quelle wählen
Collections stellen stream() bereit. Eine Map stellt Streams über entrySet(), keySet() oder values() bereit. Arrays.stream(array) verarbeitet Array-Elemente als Stream; ein int[] erzeugt einen IntStream. Stream.of("a", "b", "c") liefert aufgezählte Werte.
IntStream.range(1, 5) enthält 1, 2, 3, 4, wobei das Ende ausgeschlossen ist. rangeClosed(1, 5) schließt 5 ein und ergibt die Summe 15. Stream.iterate(1, n -> n * 2) hat keinen festen Endpunkt; .limit(5).toList() sammelt [1, 2, 4, 8, 16]. Ein begrenztes Ergebnis ist beim Verarbeiten dieser unendlichen Folge unerlässlich.
Die verzögerte Ausführung verfolgen
Zwischenoperationen werden verzögert ausgewertet. Das bloße Schreiben von names.stream().map(String::toUpperCase); führt die Umwandlungen nicht aus. Eine Terminaloperation startet die Verarbeitung, und Elemente werden nur bei Bedarf verarbeitet.
String first = java.util.stream.Stream.of("one", "two", "three", "four")
.filter(w -> w.length() > 3)
.map(String::toUpperCase)
.findFirst()
.orElse("none");
System.out.println(first); // THREE
Die ersten beiden Wörter bestehen den Filter nicht. "three" besteht ihn, wird zu "THREE" und erfüllt findFirst. Das letzte Wort wird nicht benötigt. Das ist Kurzschlussauswertung: Die Antwort steht fest, bevor die ganze Quelle gelesen wurde.
sorted benötigt die gesamte Eingabe, bevor es den ersten sortierten Wert ausgeben kann. Einige andere zustandsbehaftete Operationen, etwa ein sequenzielles distinct, können Werte ausgeben, sobald Eingaben eintreffen. Die Position einer Operation in der Verkettung beeinflusst, wie viel Arbeit nötig ist.
Für ein weiteres Ergebnis einen neuen Stream erstellen
Nach einer Terminaloperation ist der Stream verbraucht. Die erneute Verwendung dieses Streams wirft häufig eine IllegalStateException mit „stream has already been operated upon or closed“. Rufe für eine weitere Pipeline erneut points.stream() auf. Eine Collection ist wiederverwendbarer Speicher; ein Stream ist ein Verarbeitungspfad.
Das Erstellen eines Streams kopiert seine Elemente nicht. Halte die Quelle während der Verarbeitung unverändert. map erzeugt abgebildete Werte; es schreibt sie nicht in die Liste zurück. Für beabsichtigte Änderungen an einer Liste drücken Methoden wie replaceAll und removeIf diese Aufgabe direkt aus. Verwende eine Operation pro Zeile, damit Verarbeitungsreihenfolge und Ergebnistyp sichtbar bleiben.
Wichtige Erkenntnisse
- Eine Pipeline hat eine Quelle, Zwischenoperationen und eine Terminaloperation.
- Zwischenoperationen werden verzögert ausgewertet.
- Kurzschlussauswertung kann das Lesen des Rests einer Quelle vermeiden.
sortedmuss die gesamte Eingabe erfassen, bevor es sortierte Werte ausgibt.- Ein verbrauchter Stream muss durch einen neuen Stream aus der Quelle ersetzt werden.
- Das Erstellen eines Streams kopiert seine Quelle nicht und schreibt sie auch nicht um.
4.2 Filtere Elemente, wandle sie um und ändere ihre Reihenfolge
Verfolge nach jeder Operation sowohl die Werte als auch ihren Typ: Ein späterer Lambda-Ausdruck erhält das aktuelle Stream-Element, nicht das ursprüngliche Quellobjekt.
Vor dem Umwandeln auswählen
filter behält Elemente, die ein Prädikat erfüllen. map ersetzt jedes Element durch einen berechneten Wert. Bei Studierenden mit Gettern für Namen und Punkte muss die Auswahl nach Punkten vor der Abbildung auf Namen erfolgen:
List<String> names = students.stream()
.filter(s -> s.getPoints() >= 80)
.map(Student::getName)
.sorted()
.toList();
Für Ada 85, Ben 72, Chen 85, Dara 90 und Eli 64 ergibt dies [Ada, Chen, Dara]. Vor map ist der Elementtyp Student. Danach ist der Typ String, sodass kein Punkte-Getter mehr verfügbar ist.
Zu einem Stream primitiver Werte wechseln
Stream<Integer> enthält Referenzen auf Wrapperobjekte und hat keine Methode sum(). mapToInt(Integer::intValue) erzeugt einen IntStream, der numerische Operationen bereitstellt. mapToInt(String::length) misst entsprechend Strings und liefert ihre Längen als primitive Werte.
import java.util.List;
import java.util.stream.IntStream;
class Windows {
public static void main(String[] args) {
List<Integer> window = IntStream.rangeClosed(1, 10)
.skip(2)
.limit(3)
.boxed()
.toList();
System.out.println(window); // [3, 4, 5]
}
}
skip(2) entfernt 1 und 2 aus dem Stream. limit(3) behält die nächsten drei Werte. Kehrt man diese Aufrufe um, bleibt nur 3 übrig. boxed() führt zurück zu Stream<Integer>, sodass die Elemente als Objekte gesammelt werden können. mapToObj erzeugt andere Objekte aus primitiven Werten; Streams von long- und double-Werten haben entsprechende Umwandlungsoperationen.
Duplikate entfernen und eine Ordnung wählen
distinct verwendet die Gleichheit von Objekten. Aus ["map", "filter", "map", "sorted", "filter"] wird [map, filter, sorted], und sorted() ergibt danach [filter, map, sorted]. Implementiere für eigene Klassen konsistente Methoden equals und hashCode, wenn übereinstimmende Feldwerte als Duplikate gelten sollen.
sorted() verwendet die natürliche Ordnung. Gib für eine andere Ordnung oder für Objekte ohne natürliche Ordnung einen Comparator an. Das Sortieren eines geordneten Streams ist stabil. Sortiert man die fünf Studierenden nach absteigender Punktzahl und nimmt dann zwei, werden Dara und Ada ausgewählt: Ada steht bei ihrer gleichen Punktzahl in der ursprünglichen Liste vor Chen. Ein Aufruf von limit(2) vor dem Sortieren würde nur die ursprünglichen ersten beiden Studierenden betrachten.
Verschachtelte Folgen abflachen
List<Integer> all = List.of(
List.of(1, 2), List.of(3), List.of(4, 5, 6))
.stream()
.flatMap(List::stream)
.toList();
System.out.println(all); // [1, 2, 3, 4, 5, 6]
Jede innere Liste wird zu einem Stream, und flatMap fügt ihre Elemente zu einem Stream zusammen. Ersetzt man hier flatMap durch map, ergibt sich ein Stream<Stream<Integer>>: ein Stream-Objekt pro innerer Liste, ohne Abflachung.
peek gibt einen Wert unverändert weiter und kann ihn zur Fehlersuche ausgeben. Seine Aktion wird nur für tatsächlich durchlaufene Elemente ausgeführt, und eine Implementierung kann eine nicht benötigte Stufe überspringen, zum Beispiel bei der Optimierung von count. Verwende es nicht, um notwendige Arbeit auszuführen. Verwende für einen einzelnen größten Wert max(comparator), statt jedes Element zu sortieren und eines zu entnehmen.
Wichtige Erkenntnisse
filterbehält den Elementtyp bei;mapkann ihn ändern.- Streams primitiver Werte bieten numerische Operationen, die
Stream<Integer>nicht hat. - Die Reihenfolge von
skip,limitund Sortierung ändert die ausgewählten Elemente. distinctrichtet sich nach der Gleichheit; das Sortieren geordneter Streams erhält bei Gleichständen die Reihenfolge.flatMapflacht Streams ab, die aus den Eingabeelementen erzeugt werden.peekdient der Untersuchung, nicht notwendigen Programmauswirkungen.
4.3 Reduziere einen Stream auf ein einzelnes Ergebnis
Wähle die Terminaloperation anhand des benötigten Ergebnisses: eine Anzahl, eine Zahl, ein Wahrheitswert, ein Element oder eine allgemeine Verknüpfung. Eine leere Eingabe ist immer dann relevant, wenn kein Ergebniswert ausgewählt werden kann.
Eine Zahl berechnen
count() gibt einen long zurück. sum() auf einem Stream primitiver Werte gibt eine Zahl des primitiven Typs des Streams zurück. Für die Punkte [72, 45, 90, 58, 81] ergibt das Filtern nach mindestens 60 und anschließende Summieren 243. Der Mittelwert dieser drei Werte ist 81,0.
import java.util.List;
class PointResults {
public static void main(String[] args) {
List<Integer> points = List.of(72, 45, 90, 58, 81);
int sum = points.stream().filter(p -> p >= 60)
.mapToInt(Integer::intValue).sum();
double average = points.stream().filter(p -> p >= 60)
.mapToInt(Integer::intValue).average().orElse(0.0);
System.out.println(sum); // 243
System.out.println(average); // 81.0
}
}
Die zweite Pipeline verwendet einen neuen Stream. Eine leere Summe ist 0, aber ein Mittelwert einer leeren Eingabe hat keinen Wert. average() gibt OptionalDouble zurück, und orElse(0.0) liefert einen gewählten Ersatzwert. Dieser Standardwert ist eine Entscheidung im Programm, nicht der mathematische Mittelwert einer leeren Menge.
Eine int-Summe kann überlaufen. Das Summieren zweier Werte von 2.000.000.000 ergibt als int den Wert -294.967.296. Wandle vor dem Addieren um: mapToInt(...).asLongStream().sum() ergibt 4.000.000.000 als long.
Einen Wert oder keinen behandeln
Ein Optional<T> enthält einen Wert oder keinen Wert. min, max, findFirst, findAny und eine Reduktion ohne neutrales Element geben bei Objekt-Streams optionale Werte zurück. points.stream().max(Integer::compare) ergibt Optional[90]; bei der Ausgabe des Containers erscheint auch der umgebende Text.
Verwende orElse für einen Ersatzwert, orElseGet für einen Ersatz-Supplier, ifPresent für eine Aktion oder map, um einen vorhandenen Wert umzuwandeln. Filtert man nach einem Studierenden mit mehr als 95 Punkten, bildet den ersten Treffer auf einen Namen ab und wendet orElse("nobody") an, ergibt sich für die Daten der fünf Studierenden "nobody". Der Aufruf von get() oder orElseThrow() auf einem leeren optionalen Wert wirft eine NoSuchElementException.
Eine Ja-oder-Nein-Frage stellen
anyMatch fragt, ob mindestens ein Wert die Bedingung erfüllt, allMatch, ob jeder Wert sie erfüllt, und noneMatch, ob keiner sie erfüllt. Sie können enden, sobald die Antwort feststeht. Bei leerer Eingabe ist anyMatch falsch, während allMatch und noneMatch wahr sind: Es gibt keinen Wert, der eine der beiden allgemeinen Aussagen verletzt.
Mit einem neutralen Element verknüpfen
Stream.of(3, 5, 7).reduce(0, Integer::sum) akkumuliert 0, 3, 8, 15. Für die Multiplikation beginne mit 1 und verwende (a, b) -> a * b, um 105 zu erhalten. Ein neutrales Element lässt einen Wert bei der Operation unverändert; 10 ist kein neutrales Element der Addition und würde die Summe fälschlich auf 25 verschieben. Der Akkumulator muss assoziativ sein, damit die Gruppierung das Ergebnis nicht ändert.
Ohne neutrales Element ergibt reduce(Integer::max) den Wert Optional[7] und bei leerer Eingabe einen leeren optionalen Wert. forEach führt hingegen eine Aktion aus und gibt void zurück; es eignet sich zur Ausgabe. Verwende eine Reduktion oder einen Collector, wenn die Aufgabe darin besteht, einen Ergebniswert zu erzeugen.
Wichtige Erkenntnisse
- Anzahlen verwenden
long; numerische Stream-Ergebnisse richten sich nach ihrem primitiven Typ. - Eine leere Summe ist null; ein Mittelwert oder Extremwert einer leeren Eingabe hat keinen Wert.
- Verwende die Operationen eines optionalen Werts für den leeren Fall, bevor du seinen Wert ausliest.
- Bei leerer Eingabe sind
allMatchundnoneMatchwahr;anyMatchist falsch. - Eine Reduktion benötigt ein neutrales Element und einen assoziativen Akkumulator.
- Wandle vor dem Summieren in
longum, wenn die Summe der Werte den Wertebereich vonintüberschreitet.
5Sammle Stream-Ergebnisse und verwende sie weiter
Ein nützliches Stream-Ergebnis kann eine Liste, Menge, Map, ein String oder eine Zusammenfassung für jede Gruppe sein. Der Collector bestimmt, was gespeichert wird und was bei wiederholten Schlüsseln geschieht. Gruppierung verbindet Auswahlregeln mit nachgeschalteten Zusammenfassungen; Partitionierung ergibt zwei boolesche Gruppen. In einer vollständigen Aufgabe kann dann die resultierende Map abgefragt werden. Der Vergleich dieser Pipeline mit einer Schleife hilft dir, lesbaren Code zu wählen und den Aufbau von Ergebnissen von Änderungen an externem Zustand zu trennen.
5.1 Sammle Ergebnisse in Listen, Mengen, Maps und Strings
Ein Collector wandelt einen Stream in eine nützliche Ergebnisstruktur um. Entscheide, was das Ergebnis unterstützen muss: Duplikate, eine Ordnung, spätere Änderungen oder doppelte Map-Schlüssel. Verschiedene Collectoren geben unterschiedliche Zusicherungen.
Eine Liste oder Menge wählen
stream.toList() gibt eine nicht modifizierbare Liste in Begegnungsreihenfolge zurück. Das Hinzufügen oder Entfernen von Elementen wirft eine UnsupportedOperationException. collect(Collectors.toList()) sammelt ebenfalls in einer Liste, garantiert aber keine bestimmte Implementierung oder Modifizierbarkeit. Fordere eine Liste, die du ändern willst, ausdrücklich an:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class Gather {
public static void main(String[] args) {
List<String> words = Stream.of("a", "b", "a")
.collect(Collectors.toCollection(ArrayList::new));
words.add("c");
System.out.println(words); // [a, b, a, c]
System.out.println(Stream.of("a", "b", "a")
.collect(Collectors.toSet()).size()); // 2
}
}
toSet entfernt Duplikate und sichert keine Reihenfolge zu. toCollection(TreeSet::new) fordert eine sortierte Menge an. Eine Liste behält doppelte Elemente; wähle eine Menge, wenn das Ergebnis eindeutige Werte darstellt.
Text zusammenfügen
Collectors.joining(", ") fügt Strings mit einem Trennzeichen zusammen, ohne ein abschließendes Trennzeichen. Die Form mit drei Argumenten ergänzt außerdem ein Präfix und ein Suffix:
String names = Stream.of("Ada", "Chen", "Dara")
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(names); // [Ada, Chen, Dara]
Die Elemente müssen Werte vom Typ CharSequence sein, normalerweise Strings. Bilde Studierende mit Student::getName oder Zahlen mit String::valueOf ab, bevor du sie zusammenfügst. Das Zusammenfügen gibt einen String zurück, keine Liste, die nur ähnlich ausgegeben wird.
Map-Schlüssel und Werte wählen
Collectors.toMap(keyFunction, valueFunction) berechnet aus jedem Element einen Eintrag. Bei den fünf Studierenden Ada (A, 85), Ben (B, 72), Chen (A, 85), Dara (B, 90), Eli (A, 64) ergeben sich beim Zuordnen der Namen zu Punkten fünf Einträge wie Ada=85 und Eli=64.
Bei Gruppenschlüsseln ist es anders: Mehrere Studierende haben dieselbe Gruppe. Ein toMap(Student::getGroup, Student::getPoints) mit zwei Argumenten wirft eine IllegalStateException, wenn ein Schlüssel erneut auftritt, selbst wenn beide Werte 85 sind. Eine Map benötigt einen Ergebniswert pro Schlüssel, und diese Form gibt keine Regel vor, um ihn auszuwählen.
Gib ein drittes Argument an, um doppelte Schlüssel zusammenzuführen:
Map<String, Integer> totals = students.stream()
.collect(Collectors.toMap(
Student::getGroup, Student::getPoints, Integer::sum));
Gruppe A erhält 85 + 85 + 64 = 234; Gruppe B erhält 72 + 90 = 162. Ersetzt du Integer::sum durch Math::max, bleiben die höchsten Punktzahlen pro Gruppe erhalten, A=85 und B=90. Verwende die Regel, die zur Aufgabe passt, und keinen beliebigen ersten Wert.
toMap sichert keine bestimmte Map-Reihenfolge zu. Ein viertes Argument kann eine solche anfordern, zum Beispiel TreeMap::new für sortierte Schlüssel. toMap(Student::getName, Student::getPoints, (a, b) -> a, TreeMap::new) behält bei doppelten Namen den ersten Wert und sortiert nach Namen. Ein Collector erspart auch das Erstellen einer externen Liste und deren Änderung in forEach; das Ergebnis wird von der Operation aufgebaut, die zum Sammeln vorgesehen ist.
Wichtige Erkenntnisse
- Die von
Stream.toList()zurückgegebene Liste ist nicht modifizierbar. Collectors.toList()garantiert keine modifizierbare Implementierung.- Verwende
toCollection, wenn ein bestimmter Collection-Typ benötigt wird. joiningfügt Text mit optionalem Trennzeichen, Präfix und Suffix zusammen.toMapmit zwei Argumenten lehnt doppelte Schlüssel ab, auch wenn die doppelten Werte gleich sind.- Eine Zusammenführungsfunktion löst wiederholte Map-Schlüssel auf.
- Fordere eine Map-Fabrik an, wenn deren Ordnung wichtig ist.
5.2 Gruppiere und partitioniere Daten mit Collectoren
Ein gruppiertes Ergebnis beantwortet für jeden Schlüssel eine Frage. Eine Partition beantwortet eine Ja-oder-Nein-Frage mit zwei Gruppen. Ein nachgeschalteter Collector bestimmt, was jede Gruppe speichert.
Nach einem berechneten Schlüssel gruppieren
groupingBy(classifier) erzeugt eine Map, die jedem vorkommenden Schlüssel eine Liste seiner Elemente zuordnet. Gruppiert man ["fig", "kiwi", "pear", "plum", "apple", "mango", "banana"] nach Wortlänge, erhält die Länge 4 die Liste [kiwi, pear, plum]. Für fehlende Längen gibt es keinen Eintrag; eine Abfrage gibt null zurück. Verwende daher getOrDefault(key, List.of()), wenn eine leere Liste nützlich ist.
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
class WordGroups {
public static void main(String[] args) {
List<String> words = List.of(
"fig", "kiwi", "pear", "plum", "apple", "mango", "banana");
Map<Integer, Long> counts = words.stream()
.collect(Collectors.groupingBy(
String::length, TreeMap::new, Collectors.counting()));
System.out.println(counts); // {3=1, 4=3, 5=2, 6=1}
}
}
Die Klassifikationsfunktion wählt ganzzahlige Schlüssel. TreeMap::new ordnet diese Schlüssel. counting() ersetzt die Liste jeder Gruppe durch ihre Größe als Long. Der Ergebnistyp ist daher Map<Integer, Long>, keine Map von Ganzzahlen auf Ganzzahlen.
Ändern, was eine Gruppe speichert
Für die Studierenden Ada (A, 85), Ben (B, 72), Chen (A, 85), Dara (B, 90), Eli (A, 64) bietet das Gruppieren nach Student::getGroup diese Möglichkeiten:
| Nachgeschalteter Collector | Wert unter A | Werttyp |
|---|---|---|
counting() | 3 | Long |
summingInt(Student::getPoints) | 234 | Integer |
averagingInt(Student::getPoints) | 78.0 | Double |
mapping(Student::getName, Collectors.toList()) | [Ada, Chen, Eli] | List<String> |
Ein nachgeschalteter Collector arbeitet innerhalb jeder Gruppe. Für A ist der Mittelwert 234 dividiert durch 3, also 78,0; der Mittelwert von B ist 81,0. maxBy und minBy speichern ein Optional, das den Extremwert der jeweiligen Gruppe enthält. Einfaches groupingBy sichert keinen bestimmten Map-Typ und keine Schlüsselreihenfolge zu; die Form mit Map-Fabrik ermöglicht eine ausdrückliche Wahl.
In zwei Antworten aufteilen
Map<Boolean, List<String>> partition = students.stream()
.collect(Collectors.partitioningBy(
s -> s.getPoints() >= 80,
Collectors.mapping(Student::getName, Collectors.toList())));
Die Liste für true ist [Ada, Chen, Dara], die Liste für false ist [Ben, Eli]. Beide booleschen Schlüssel sind vorhanden, auch wenn eine Seite keine Studierenden enthält. Mit nachgeschaltetem counting() enthalten leere Seiten 0 als Long. groupingBy mit einer booleschen Klassifikationsfunktion erstellt dagegen nur Schlüssel, die vorkommen.
Die erstellte Map verarbeiten
Ein gruppiertes Ergebnis ist eine gewöhnliche Map. Durchlaufe sie mit forEach((group, value) -> ...) oder verarbeite ihre Einträge als Stream. Für die Mittelwerte A=78,0 und B=81,0 gibt averages.entrySet().stream().max(Map.Entry.comparingByValue()) ein Optional<Map.Entry<String, Double>> mit B=81.0 zurück. Der optionale Wert umschließt den Eintrag mit dem höchsten Wert; er ist nicht der einfache Wert 81,0. In einem separaten Schritt kann dieser Eintrag auf seinen Schlüssel oder Wert abgebildet und eine leere Map behandelt werden.
Wichtige Erkenntnisse
- Eine Klassifikationsfunktion wählt Gruppenschlüssel; einfaches Gruppieren speichert Listen von Elementen.
- Ein nachgeschalteter Collector ändert die Form des Ergebnisses jeder Gruppe.
countingliefertLong; Collectoren für Mittelwerte liefernDouble.- Eine Partition enthält immer beide booleschen Schlüssel.
- Ein fehlender Gruppenschlüssel gibt bei der Abfrage
nullzurück, keine leere Gruppe.
5.3 Wähle für eine vollständige Aufgabe zwischen Streams und Schleifen
Eine vollständige Datenaufgabe hat oft zwei Stufen: zuerst die Eingabe zusammenfassen, dann die Zusammenfassung abfragen. Wähle Streams, wenn sich diese Stufen natürlich als Standardoperationen lesen lassen, und Schleifen, wenn eine explizite Steuerung die Arbeit klarer macht.
Messwerte mitteln und dann Städte auswählen
Angenommen, ein Reading hat getCity() und getCelsius(). Die Messwerte in Grad Celsius sind Oslo 4,5, Lima 19,0, Oslo 6,0, Cairo 27,5 und Lima 21,0.
class Reading {
private final String city;
private final double celsius;
Reading(String city, double celsius) {
this.city = city;
this.celsius = celsius;
}
String getCity() { return city; }
double getCelsius() { return celsius; }
}
Erstelle die Eingabeliste vor jeder der beiden Implementierungen:
List<Reading> readings = List.of(
new Reading("Oslo", 4.5), new Reading("Lima", 19.0),
new Reading("Oslo", 6.0), new Reading("Cairo", 27.5),
new Reading("Lima", 21.0));
Map<String, Double> averages = readings.stream()
.collect(Collectors.groupingBy(
Reading::getCity,
TreeMap::new,
Collectors.averagingDouble(Reading::getCelsius)));
List<String> warmCities = averages.entrySet().stream()
.filter(e -> e.getValue() > 15)
.map(Map.Entry::getKey)
.toList();
Das erste Ergebnis ist {Cairo=27.5, Lima=20.0, Oslo=5.25}. Der Mittelwert von Oslo ist (4.5 + 6.0) / 2, also 5,25. Der von Lima ist 20,0. Das zweite Ergebnis ist [Cairo, Lima]; die TreeMap sorgt für eine aufsteigende Reihenfolge der Städte. Filtere die Mittelwerte nach dem Gruppieren: Filterst du zuerst einzelne Messwerte, beantwortest du eine andere Frage.
Für den höchsten einzelnen Messwert verwende readings.stream().max(Comparator.comparingDouble(Reading::getCelsius)). Das Ergebnis ist ein optionaler Wert mit dem Messwert aus Cairo, nicht definitionsgemäß die Stadt mit dem höchsten Mittelwert. Bei anderen Daten können diese Aufgaben unterschiedliche Antworten haben.
Dieselbe Berechnung mit Schleifen ausdrücken
Map<String, List<Double>> byCity = new TreeMap<>();
for (Reading r : readings) {
byCity.computeIfAbsent(r.getCity(), k -> new ArrayList<>())
.add(r.getCelsius());
}
Map<String, Double> averages = new TreeMap<>();
for (Map.Entry<String, List<Double>> e : byCity.entrySet()) {
double sum = 0;
for (double value : e.getValue()) sum += value;
averages.put(e.getKey(), sum / e.getValue().size());
}
System.out.println(averages);
// {Cairo=27.5, Lima=20.0, Oslo=5.25}
Dies sind alternative Implementierungen desselben Ergebnisses; jede Gruppe ist nicht leer, weil sie durch einen Messwert erzeugt wurde. Der Stream macht das Gruppieren und Mitteln in einem Collector deutlich. Die Schleifen zeigen die Zwischenlisten und die Summe.
Das Verarbeitungsverhalten vorhersehbar halten
Ein Lambda-Ausdruck in einer Pipeline sollte aus seiner Eingabe rechnen, ohne externen Zustand zu ändern. Für die Punkte [72, 45, 90, 58, 81] erzeugt filter(p -> p >= 60).toList() direkt [72, 90, 81]. Fügt man innerhalb von forEach Werte zu einer externen Liste hinzu, verbirgt sich diese Aufgabe des Ergebnisaufbaus in einer Aktion.
Ändere eine ArrayList als Quelle nicht innerhalb ihrer Pipeline: Das Hinzufügen von Elementen aus einem forEach heraus kann eine ConcurrentModificationException werfen. Sammle neue Elemente zuerst separat und füge sie nach Abschluss der Verarbeitung hinzu. Eine eingefangene finale Referenz kann nach Javas Typregeln eine Zustandsänderung erlauben; das macht eine solche Änderung innerhalb eines Streams nicht geeignet.
Den klareren Kontrollfluss wählen
Verwende eine Schleife, wenn du benachbarte Indizes, eine komplizierte Abbruchbedingung oder mehrere aufeinander abgestimmte Aktualisierungen benötigst. Verwende Streams für eine gut lesbare Auswahl, Umwandlung und Sammlung. Die Stream-Syntax allein garantiert keine Geschwindigkeit. Benenne ein langes Prädikat oder einen langen Comparator und teile eine lange Verkettung an einem aussagekräftigen Zwischenergebnis auf. Untersuche zur Fehlersuche mit peek eine Stufe oder sammle sie in einer Liste und gib diese aus; fahre mit einem neuen Stream dieser Liste fort, statt eine verbrauchte Pipeline wiederzuverwenden.
Wichtige Erkenntnisse
- Gruppiere vor dem Filtern, wenn die Bedingung Gruppenmittelwerte betrifft.
- Ein größter einzelner Messwert und ein größter Gruppenmittelwert beantworten unterschiedliche Fragen.
- Ein Collector drückt die Aufgabe des Ergebnisaufbaus direkt aus.
- Halte das Verhalten der Pipeline frei von externen Aktualisierungen und Änderungen an der Quelle.
- Verwende Schleifen, wenn Indizes oder eine abgestimmte Steuerung die Aufgabe klarer machen.
- Untersuche eine als Liste gespeicherte Stufe und verwende für die Weiterarbeit einen neuen Stream.
Lambda-Ausdrücke und Streams in Java: Beispiele
1Erstelle eine typsichere, wiederverwendbare API
17 Schritte
- 1EinführungDie Typbeziehung erhalten
Ein wiederverwendbarer Container muss sich merken, was der aufrufende Code hineinlegen und zurückerhalten darf. Ein Typparameter trägt diese Beziehung durch eine Deklaration.
Du wirst eine Box erstellen, die Typwahl auf eine Methode verlagern und dann prüfen, was sich ändert, wenn ein Parameter eine Wildcard verwendet. Frage dich bei jedem Schritt, was der Compiler über einen Wert weiß, bevor das Programm läuft.
- 2ErklärungDie Box deklarieren
Tbenennt innerhalb dieser Klasse den vom aufrufenden Code gewählten Typ:class Box<T> { private T value; Box(T value) { this.value = value; } T get() { return value; } void set(T value) { this.value = value; } }Das Feld, die Eingabe des Konstruktors, das Ergebnis des Getters und die Eingabe des Setters verwenden alle dasselbe
T. Die Deklaration führt einen Platzhalter ein; ein bestimmter gespeicherter Typ wurde noch nicht gewählt. - 3BeispielEinen konkreten Typ verwenden
Erzeuge eine Box und lies ihren Wert:
Box<String> b = new Box<>("tea"); String drink = b.get(); b.set("water");Stringgibt das Typargument an. Der Getter gibt einen String ohne explizite Typumwandlung zurück, und der Setter akzeptiert Strings. Der Diamantoperator rechts veranlasst Java, die Argumente abzuleiten. Er entfernt die Prüfungen nicht; ließe man die spitzen Klammern ganz weg, entstünde eine Verwendung als roher Typ. - 4HerausforderungDen abgelehnten Schreibzugriff vorhersagen
Die generische
Box<T>hatvoid set(T value). Lässt sich nachBox<Integer> b = new Box<>(8);der Aufrufb.set("nine")kompilieren? Erkläre, wo der Fehler auftritt.---reveal---
Der Schreibzugriff schlägt fehl, weil diese Referenz einen
Integererwartet, keinenString. Ein späterer Lesezugriff ist nicht nötig, um den Fehler zu entdecken. Das Literal 8 kann alsIntegerverpackt werden; Typargumente selbst müssen weiterhin Referenztypen sein, daher istBox<int>ungültig. - 5VerbindungFür jeden Aufruf einen Typ wählen
Eine Hilfsmethode kann ihren eigenen Typparameter einführen:
static <T> T last(List<T> list) { return list.get(list.size() - 1); }Das erste
<T>deklariert den Parameter, und das nächsteTbenennt den Ergebnistyp. Ein Aufruf mit[4, 8, 15]gibt denInteger15 zurück; ein Aufruf mit["x", "y"]gibt den String"y"zurück. Die Methode benötigt unabhängig von ihren Typprüfungen eine nicht leere Liste. - 6TippEinen Typzeugen nach dem Punkt schreiben
Normalerweise leitet Java das Typargument der Methode aus der übergebenen Liste ab. Ein Aufruf kann es auch angeben:
String word = Lists.<String>last(words);Der Typzeuge folgt auf den Punkt und steht vor
last. Eine statische Methode innerhalb einer generischen Klasse benötigt weiterhin einen eigenen Parameter:static <E> Box<E> of(E value). DasTder Klasse gehört zu parametrisierten Instanzverwendungen, während die statische Methode zur gemeinsam genutzten Klasse gehört. - 7HerausforderungEingabe und Ausgabe verknüpfen
Eine Hilfsmethode soll ein Element aus einer
List<E>zurückgeben, ohne dass der aufrufende Code eine explizite Typumwandlung schreiben muss. Jemand schreibtstatic E first(List<E> xs). Welche Deklaration fehlt?---reveal---
Ergänze
<E>vor dem Rückgabetyp:static <E> E first(List<E> xs). Dies führt einen zur Methode gehörenden Parameter ein und verknüpft ihre Eingabeelemente mit ihrem Ergebnis.Objectals Ergebnis zu schreiben würde diese nützliche Beziehung verlieren, statt den Fehler zu beheben. - 8ErklärungVergleiche verfügbar machen
Ein unbeschränktes
That keine bekannte OperationcompareTo. Eine Typschranke kann diese Zusicherung liefern:<T extends Comparable<T>>Eine Methode
largest(List<T>)kann dann einT bestverwalten, jedesxmitbestvergleichen und den Wert ersetzen, wennx.compareTo(best) > 0gilt. Für 3, 11 und 7 sind die jeweils besten Werte 3, dann 11, dann 11. Der Rückgabetyp bleibt der eigene Elementtyp der Liste. - 9KernaussageEine Integer-Liste schützen
Ein
Integerist einNumber, aber eineList<Integer>ist keineList<Number>.Wäre diese Listenzuweisung erlaubt, könnte Code über die Referenz auf die Zahlenliste ein
Doublehinzufügen. Der ursprüngliche Besitzer würde es dann alsIntegerlesen. Die Invarianz generischer Typen verhindert die Zuweisung, damit sowohl Lese- als auch Schreibzugriffe ihre Zusicherungen einhalten.ArrayList<Number>wird alsList<Number>akzeptiert, weil das Element-Typargument weiterhin übereinstimmt. - 10BeispielIn die sichere Richtung übertragen
Eine extends-Wildcard erlaubt es dem Code, einen bekannten oberen Typ zu lesen; eine super-Wildcard erlaubt das Einfügen eines bekannten unteren Typs.
static <T> void append( List<? super T> dst, List<? extends T> src) { dst.addAll(src); }Bei einer modifizierbaren
List<Number>als Ziel und einerList<Integer>als Quelle kann Java fürTden TypIntegerwählen. Jedes Quellelement ist für das Ziel akzeptabel, ohne zu behaupten, dass beide Listen identische Element-Typargumente haben. - 11HerausforderungDen sicheren Lesezugriff wählen
Ein Parameter hat den Typ
List<? super Integer>. Kann sein erstes Element direkt einerInteger-Variablen zugewiesen werden? Welcher Typ ist stattdessen garantiert?---reveal---
Nur
Objectist garantiert. Die tatsächliche Liste kann eineList<Number>oderList<Object>sein und bereits andere Werte als Ganzzahlen enthalten. Über diesen Typ kann sicher eine Ganzzahl hinzugefügt werden, aber die Schreiberlaubnis beschreibt nicht jedes bereits gespeicherte Element. - 12ErklärungPrüfung und Laufzeitklassen trennen
Die Typlöschung ersetzt Typparameter durch ihre Typschranken und verwendet
Object, wenn keine Typschranke angegeben ist. Java hat die generischen Verwendungen bereits geprüft und fügt beim Lesen benötigte explizite Typumwandlungen ein.Eine
Box<String>und eineBox<Integer>teilen sich dieselbe LaufzeitklasseBox. Es gibt keine separate Integer-Box-Klasse, die ein Laufzeittest erkennen könnte. Generics bewahren Typbeziehungen bei der Prüfung des Quellcodes. Deshalb entfernen sie viele sichtbare explizite Typumwandlungen, ohne die Klasse für jedes Argument zu spezialisieren. - 13BeispielDas Erzeugungsverhalten bereitstellen
Ein Typparameter kann nicht mit
new T()instanziiert werden. Bitte stattdessen den aufrufenden Code um eine Fabrik:static <T> T create(Supplier<T> factory) { return factory.get(); } List<String> xs = create(ArrayList::new);Die Referenz wählt über den Supplier-Zieltyp einen Listenkonstruktor. Generische Arrays wie
new T[10]werden ebenfalls abgelehnt; eine Collection wieArrayList<T>erfüllt meist die Speicheraufgabe. - 14TippDie Warnung zum rohen Typ verfolgen
Eine rohe
Listkann"text"und 42 aufnehmen und dabei Unchecked-Warnungen für die Aufrufe ausgeben. Eine spätere explizite Typumwandlung(String)schlägt fehl, wenn sie 42 erreicht.Ändert man die Deklaration zu
List<String>, verlagert sich das Problem auf das ungültige Hinzufügen. Eine Unchecked-Warnung bedeutet, dass der Compiler eine Garantie verloren hat; für manche Daten kann die Ausführung trotzdem erfolgreich sein. Prüfe die Verwendung des rohen Typs oder die explizite Typumwandlung, die die Warnung verursacht hat, denn der letztlich fehlschlagende Lesezugriff kann weit entfernt sein. - 15HerausforderungDie Überladung nach Typlöschung prüfen
Eine Klasse deklariert
show(Set<String> xs)undshow(Set<Integer> xs). Warum kann Java die Element-Typargumente nicht verwenden, um eine Überladung auszuwählen?---reveal---
Beide Parametertypen werden durch die Typlöschung zu
Set, sodass die Deklarationen dieselbe Signatur nach Typlöschung haben. Gib ihnen unterschiedliche Namen oder eine gemeinsame Operation. Ein Laufzeittest vonObjectaufList<String>hat eine verwandte Einschränkung: Prüfe aufList<?>, ohne dessen Element-Typargument zu behaupten. - 16HerausforderungEine wiederverwendbare Übertragungs-API prüfen
Eine API verwendet eine Quellliste mit rohem Typ, wandelt jedes Element explizit in
Numberum und hängt es an eine Zahlenliste an. Sie soll Ganzzahlen und Double-Werte als Quellen akzeptieren. Welcher Quellparameter entfernt diese Typumwandlung und erhält zugleich sichere numerische Lesezugriffe?---reveal---
Verwende
List<? extends Number>. Jedes Element kann alsNumbergelesen werden, und sowohl Integer- als auch Double-Listen passen.List<Number>würde diese engeren Listenargumente ablehnen. Das Ziel sollte modifizierbar sein; sein generischer Typ allein kann erfolgreiche Schreibzugriffe nicht garantieren. - 17ZusammenfassungEine API anhand ihrer Zusicherungen lesen
Bestimme beim Prüfen einer generischen Deklaration, wo jeder Typparameter eingeführt wird, welche Positionen er verbindet und welche Operationen seine Typschranke bereitstellt.
Markiere bei einer Übertragung die Quelle als Erzeuger und das Ziel als Verbraucher. Suche bei einem Erzeugungsschritt nach übergebenem Konstruktionsverhalten. Frage dich bei einer Warnung oder abgelehnten Operation, ob generische Informationen umgangen wurden oder ob die Operation zur Laufzeit einen Elementtyp benötigen würde.
2Verhalten zu einem Argument machen
14 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungMit der Form des Verhaltens beginnen
- 2ErklärungDie zu implementierende Methode finden
- 3BeispielBei Bedarf einen Block für eine Entscheidung verwenden
- 4HerausforderungDas fehlende Ergebnis ergänzen
- 5ErklärungVier häufige Aufgaben zuordnen
- 6BeispielAuf die Reihenfolge der Komposition achten
- 7VerbindungCollections die Regel ausführen lassen
- 8HerausforderungEntfernen und Auswählen unterscheiden
- 9ErklärungEine Variable ohne Neuzuweisung einfangen
- 10BeispielDas Empfängerobjekt aus der Referenz bestimmen
- 11HerausforderungPrüfen, was final schützt
- 12BeispielMit einer Aktualisierungsregel für Maps zählen
- 13HerausforderungZieltypisierung und Einfangen verbinden
- 14ZusammenfassungEinen Aufruf durch die Regel verfolgen
3Eine Ordnung Schritt für Schritt aufbauen
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungDie benötigte Ordnung benennen
- 2ErklärungEin Vergleichsergebnis lesen
- 3BeispielSubtraktion bei numerischen Vergleichen vermeiden
- 4BeispielGleiche Punktzahlen in Quellreihenfolge belassen
- 5HerausforderungNatürliche und aufgabenspezifische Ordnung trennen
- 6ErklärungEine Regel nur für Gleichstände ergänzen
- 7KernaussageNur die beabsichtigten Entscheidungen umkehren
- 8TippNullwerte auf der passenden Ebene behandeln
- 9HerausforderungDie vollständige Umkehrung lesen
- 10VerbindungDie Einträge ordnen, nicht die Map
- 11HerausforderungMap-Einträge mit Gleichstand vorhersehbar ordnen
- 12ZusammenfassungDen Vergleichsbaum prüfen
4Werte durch einen Stream verfolgen
16 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungWerte und Typen gemeinsam verfolgen
- 2ErklärungDas Terminalergebnis finden
- 3BeispielNach dem ersten Treffer anhalten
- 4HerausforderungEine weitere Berechnung ausführen
- 5BeispielStudierende vor dem Abbilden auf Namen auswählen
- 6VerbindungZu numerischen Operationen wechseln
- 7BeispielVor dem Entfernen von Duplikaten abflachen
- 8HerausforderungDie Reihenfolge der Operationen lesen
- 9BeispielEine Summe und einen Mittelwert berechnen
- 10ErklärungEinen fehlenden von einem vorhandenen Wert unterscheiden
- 11HerausforderungEin fehlendes Maximum behandeln
- 12KernaussageAllgemeine Prüfungen bei leerer Eingabe prüfen
- 13BeispielEinen neutralen Startwert wählen
- 14TippVor dem Überlauf den Wertebereich erweitern
- 15HerausforderungAbflachen, Auswahl und ein Ergebnis verbinden
- 16ZusammenfassungDie Randfälle des Ergebnisses prüfen
5Eine nützliche Datenzusammenfassung erstellen
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungVom gewünschten Ergebnis rückwärts arbeiten
- 2ErklärungSpätere Änderungen ausdrücklich verlangen
- 3BeispielKollisionen mit einer Zusammenführungsregel lösen
- 4HerausforderungEinen Wert pro wiederholtem Schlüssel wählen
- 5VerbindungWerte als Text zusammenfügen
- 6ErklärungEinen Schlüssel und dann ein Gruppenergebnis wählen
- 7BeispielInnerhalb der Studierendengruppen zusammenfassen
- 8KernaussageBeide Seiten einer Partition erhalten
- 9HerausforderungEine leere Seite vorhersagen
- 10BeispielZuerst Mittelwerte der Städte berechnen
- 11BeispielDie erstellte Zusammenfassung abfragen
- 12TippDen Kontrollfluss bei Bedarf sichtbar machen
- 13HerausforderungDie Quelle während der Verarbeitung schützen
- 14HerausforderungDie Gruppe mit dem höchsten Wert sicher finden
- 15ZusammenfassungDie gesamte Datenaufgabe überprüfen
Lambda-Ausdrücke und Streams in Java: Übungsaufgaben
Set 1: Wende die Grundlagen an10 Übungsfragen
Frage 1 von 10
Box<T> hat T get() und void set(T value). Was ist bei Box<String> label = new Box<>("north"); der deklarierte Ergebnistyp von label.get()?Antwort zeigenAntwort ausblenden
String ersetzt den Klassenparameter T im deklarierten Ergebnis des Getters. Der Compiler behandelt den Aufruf daher so, dass er einen String zurückgibt, ohne dass der aufrufende Code eine explizite Typumwandlung schreiben muss. Die Typlöschung macht das Ergebnis auf Quellcodeebene nicht zu Object; die Prüfung zur Kompilierzeit verwendet weiterhin das Argument.Frage 2 von 10
Antwort zeigenAntwort ausblenden
E vor ihrem Rückgabetyp ein und wiederholt es für die Eingabeelemente und das Ergebnis. Dadurch erhält jeder Aufruf ein eigenes abgeleitetes Typargument. Das bloße Schreiben von E an den Parameter- und Rückgabepositionen deklariert es nicht, daher schlägt die Version ohne <E> in einer gewöhnlichen Hilfsklasse fehl.Frage 3 von 10
doubleValue() aufzurufen. Sie muss sowohl List<Integer> als auch List<Double> akzeptieren. Welcher Parametertyp passt?Antwort zeigenAntwort ausblenden
Number, sodass jeder gelesene Wert als Number behandelt werden kann. List<Number> kann keine List<Integer> akzeptieren, weil generische Typen invariant sind. Eine super-Wildcard würde allgemeinere Elementtypen erlauben, aber gelesene Werte hätten nur den Typ Object.Frage 4 von 10
Function<Integer, Integer> soll ihre Eingabe mit einem Blockrumpf verdoppeln. Welcher Lambda-Ausdruck passt?Antwort zeigenAntwort ausblenden
return x * 2; liefert das von der Funktion verlangte ganzzahlige Ergebnis. Ein Lambda-Ausdruck mit Ausdrucksrumpf könnte einfach x -> x * 2 verwenden, aber beim Hinzufügen geschweifter Klammern bleibt diese automatische Rückgabe nicht erhalten.Frage 5 von 10
int limit = 4; List<String> notes = new ArrayList<>(); Predicate<String> p = s -> { notes.add(s); return s.length() > limit; }; limit++;. Welche Korrektur erfüllt diese Anforderungen weiterhin?Antwort zeigenAntwort ausblenden
Frage 6 von 10
compare(a, b)?Antwort zeigenAntwort ausblenden
Frage 7 von 10
names.stream().filter(s -> s.length() > 2) und macht nichts mit dem zurückgegebenen Stream. Wann werden die Predicate-Aufrufe ausgeführt?Antwort zeigenAntwort ausblenden
filter ist eine Zwischenoperation und legt eine verzögert ausgewertete Verarbeitungsstufe fest. Hier startet keine Terminaloperation den Durchlauf, daher wird das Predicate nicht aufgerufen. Das Erstellen eines Streams kopiert oder filtert die Quellliste nicht sofort, und ein späteres unabhängiges Lesen der Liste führt diese Pipeline nicht aus.Frage 8 von 10
reduce(0, (a, b) -> a * b) berechnet. Was verursacht das falsche Ergebnis?Antwort zeigenAntwort ausblenden
Frage 9 von 10
add unterstützen. Welcher Terminalausdruck stellt ausdrücklich eine ArrayList bereit?Antwort zeigenAntwort ausblenden
toCollection(ArrayList::new) fordert eine bekannte modifizierbare Listenimplementierung an. Stream.toList() ist nicht modifizierbar, und Collectors.toList() garantiert keine Modifizierbarkeit, selbst wenn eine bestimmte Implementierung zufällig Änderungen erlaubt. Ein Mengen-Collector ändert außerdem die Struktur des Ergebnisses und den Umgang mit Duplikaten.Frage 10 von 10
ArrayList<String> words ruft forEach(w -> words.add(w + "!")) auf. Wie sollte das Programm sicher eine umgewandelte Kopie jedes ursprünglichen Wortes anhängen?Antwort zeigenAntwort ausblenden
ArrayList, während ihr Stream sie liest, beeinträchtigt den Durchlauf und kann eine ConcurrentModificationException werfen. Eine andere Referenz zeigt weiterhin auf dieselbe Quelle; sie trennt die Lese- und Schreibstufen nicht.Set 2: Verfolge Typen und Ergebnisse10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Box<Integer> enthält 12. Das Programm weist int next = box.get() + 3; zu. Wie kann das kompiliert werden und 15 ergeben?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
pick gehört zur Klasse Items und deklariert <T> T pick(List<T> xs). Wie wird bei einem Aufruf ein explizites Typargument String geschrieben?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
T bei der Typlöschung innerhalb einer unbeschränkten generischen Methode?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
int run(int x), eine Default-Methode reset() und eine statische Methode help(). Kann x -> x + 2 es implementieren?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
String entgegen und entscheidet, ob er mit ".csv" endet. Welches Standardinterface passt zur von filter erwarteten Bedingung?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
comparing(Item::getGroup).thenComparingInt(Item::getScore).reversed() sortiert. Welche Namensreihenfolge ergibt sich?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
IntStream.rangeClosed(2, 9).skip(3).limit(2).boxed().toList()?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Stream.<Integer>empty() die getrennten Aufrufe auf jeweils neuen Streams anyMatch(n -> n > 0), allMatch(n -> n > 0) und noneMatch(n -> n > 0) in dieser Reihenfolge?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
toMap(Item::getCode, Item::getCount) mit zwei Argumenten trifft auf zwei Elemente mit dem Code "R", beide mit der Anzahl 3. Was geschieht?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
partitioningBy(x -> x > 10, Collectors.counting()) gesammelt. Welches Ergebnis ist garantiert?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 3: Wähle eine sichere Implementierung10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Pair<String, Integer> p = new Pair<>("zone", 4); der Ausdruck p.getFirst() und welchen Wert hat p.getSecond() + 2?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
List<? super Integer> sink deklariert. Welcher spezifischste Typ ist für sink.get(0) garantiert?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
show(Set<Long> xs) als auch show(Set<String> xs). Warum schlägt die Kompilierung fehl?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Badge hat einen Konstruktor Badge(String text). Eine Methode akzeptiert Function<String, Badge> factory. Welche Referenz erzeugt dasselbe Verhalten wie s -> new Badge(s)?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
["z", null, "a"]. Sie wird mit Comparator.nullsLast(Comparator.reverseOrder()) sortiert. Welche Reihenfolge ergibt sich?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
s wird für s.count() und danach für s.toList() verwendet. Wie sollte das Programm beide Ergebnisse aus einer wiederverwendbaren Liste data erhalten?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Stream.of("mint", "ash", "mint", "elm").distinct().map(String::length).sorted().toList()?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 4: Verfolge die vollständige Aufgabe10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Slot<T> benötigt eine statische Fabrik, die aus einem beliebigen Wert einen typisierten Slot erzeugt. Welche Deklaration vermeidet die Verwendung des Klassenparameters aus einem statischen Kontext?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
addAll einen beliebig gewählten Elementtyp aus einer Quellliste in eine modifizierbare Zielliste. Sie soll Ganzzahlen entweder in eine Zahlenliste oder in eine Objektliste kopieren und dabei typsicher bleiben. Welche Signatur passt?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
boolean check(String s) und boolean equals(Object other) und hat keine weiteren abstrakten Methoden. Kann ein Lambda-Ausdruck es implementieren, und warum?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
String::compareToIgnoreCase, wenn sie als Comparator<String> verwendet wird?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
(int) (a - b). Was geschieht beim Vergleich von 3,6 mit 3,1?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
List.of(List.of("red"), List.of("blue", "gold")).stream().map(List::stream)?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
"reed", "oak", "fern", "elm", "cedar". Welche Map wird nach dem Gruppieren nach Länge mit counting() in einer TreeMap ausgegeben?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 5: Übertrage die Ideen auf neue Aufgaben10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
List<? super Integer> dst, List<? extends Number> src und versucht dst.add(src.get(0)). Sie muss Quellen mit Integer- oder Double-Werten und modifizierbare Ziele mit Number- oder Object-Werten akzeptieren. Wie sollten ihre Parameter geändert werden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
triple.andThen(shift).apply(4) für Function<Integer, Integer> triple = x -> x * 3; und shift = x -> x + 2; zurück?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
HashMap<String, Integer> enthält oak=6, ash=6, elm=2. Welcher Comparator für Einträge garantiert absteigende Werte und bei Gleichständen aufsteigende Schlüssel?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Stream.of("ash", "cedar", "elm", "pine").filter(w -> w.length() > 3).map(String::length).findFirst()Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
[2, 5], [] und [5, 8]. Was gibt groups.stream().flatMap(List::stream).distinct().filter(n -> n > 3).mapToInt(n -> n).sum() zurück?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
values.stream().filter(n -> n > -8).max(Integer::compare).map(n -> n * 2).orElse(0) zurück?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Sale hat ein Team und eine ganzzahlige Stückzahl. Ignoriere Verkäufe mit null oder negativer Stückzahl, berechne den Mittelwert jedes Teams aus seinen verbleibenden Verkäufen und gib das Team mit dem höchsten Mittelwert oder bei leerer Eingabe „none“ zurück. Nimm an, dass es keine gleichen Mittelwerte gibt. Welche Pipeline passt? Die Collector-Methoden sind statisch importiert.Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
"an", "oak", "to", "elm" auf der true-Seite von partitioningBy(w -> w.length() == 2, mapping(String::toUpperCase, toList()))?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
Assoziativität
Ausdrucksrumpf
x + 1 ermöglicht es x -> x + 1, die erhöhte Ganzzahl direkt zurückzugeben.Autoboxing
5 an einen Konstruktor zu übergeben, der einen Integer erwartet.B
Begegnungsreihenfolge
findFirst den ersten passenden Namen in einer Quellliste auswählt.BinaryOperator
BinaryOperator<Integer> wie Integer::sum kann Anzahlen für einen wiederholten Map-Schlüssel zusammenführen.Blockrumpf
return a; und return b;, um eine Maximumoperation zu implementieren.C
Collection-Fabrik
ArrayList::new fordert ausdrücklich eine Liste an, die späteres Hinzufügen erlaubt.Collector
forEach zu ändern.Comparator
compare für zwei Eingabewerte ein negatives Ergebnis, null oder ein positives Ergebnis zurückgibt.Comparator<String> kann Wörter nach Länge ordnen, ohne ihre natürliche String-Ordnung zu ändern.Comparator mit Nullbehandlung
nullsFirst oder nullsLast umschließt.Comparator-Umkehrung
reversed() um, einschließlich vorhandener sekundärer Vergleichskriterien.Consumer
accept gibt keinen Wert zurück.Consumer<String> kann jedes Wort ausgeben, wenn er an forEach einer Collection übergeben wird.D
Diamantoperator
<> in einem generischen Konstruktoraufruf. Er veranlasst den Compiler, Typargumente abzuleiten und dabei die Typprüfungen beizubehalten.new ArrayList<>() erhält die Typisierung der Liste aus ihrem Kontext.distinct
distinct entfernt anhand der Objektgleichheit doppelte Elemente aus einem Stream und erhält bei einem geordneten Stream die Begegnungsreihenfolge.distinct vor dem Abbilden von Wörtern auf Längen an, können im Ergebnis dennoch wiederholte Längen bleiben.Doppelter Schlüssel
toMap mit zwei Argumenten auch dann fehlschlagen, wenn beide zugeordneten Werte 85 sind.E
Effektiv final
final einfangen kann.Einfangen von Variablen
limit zu vergleichen.F
filter
filter behält Stream-Elemente, für die ihr Predicate wahr ist, und verwirft andere Werte, wobei der Elementtyp erhalten bleibt.filter nach Punkten muss vor dem Abbilden von Studierenden auf Namen ausgeführt werden, wenn er den Punkte-Getter benötigt.flatMap
flatMap wandelt jede Eingabe in einen Stream um und verbindet die Elemente dieser Streams zu einer Ausgabefolge.flatMap mit List::stream wandelt verschachtelte Integer-Listen in einen Stream von Ganzzahlen um.Function
apply eine Eingabe vom Typ T in ein Ergebnis vom Typ R umwandelt.Function<String, Integer> kann vor dem Gruppieren jedes Wort in seine Länge umwandeln.Funktionales Interface
Object entsprechen, zählen nicht mit.Operation ermöglicht es dem aufrufenden Code, seine Berechnung mit einem Lambda-Ausdruck anzugeben.Funktionskomposition
andThen und compose legen entgegengesetzte Aufrufreihenfolgen fest.twice.andThen(inc) verdoppelt und erhöht dann, sodass aus 5 der Wert 11 wird.G
Ganzzahlüberlauf
Gebundene Methodenreferenz
System.out::println gibt über dieses bestimmte Ausgabeobjekt aus.Generische Klasse
Box<T> unterstützt mit einer Deklaration eine String-Box und eine Integer-Box.Generische Methode
last gibt für eine String-Liste einen String und für eine Integer-Liste eine Ganzzahl zurück.Generisches Interface
Container<T> kann die Eingabe von put mit dem Ergebnis von take verknüpfen.groupingBy
groupingBy erstellt eine Map, die vorkommende Schlüssel der Klassifikationsfunktion Listen oder nachgeschalteten Zusammenfassungen der jeweils passenden Elemente zuordnet.groupingBy-Collector mit averagingInt speichert pro Studierendengruppe einen Mittelwert vom Typ double.I
Indexbasierte Schleife
list.get(i) mit list.get(i - 1) vergleichen und dabei zwei Zähler aktualisieren.IntStream
int-Werte mit numerischen Operationen wie Summe und Mittelwert sowie Umwandlungen zurück zu Objekten.IntStream aus mapToInt kann Punkte summieren, ohne zuerst Wrapperwerte zu sammeln.Invarianz
List<Integer> als List<Number> und schützt die Liste vor Schreibzugriffen mit Double-Werten.J
joining
joining fügt Zeichenfolgen zu einem String zusammen, optional mit Text zwischen den Elementen und um das gesamte Ergebnis.joining-Collector mit einem Komma als Trennzeichen erzeugt nach dem letzten Namen kein abschließendes Komma.K
Klassifikationsfunktion
String::length legt alle Wörter mit vier Buchstaben unter demselben ganzzahligen Schlüssel ab.Konstruktorreferenz
ClassName::new, um Verhalten zur Objekterzeugung bereitzustellen; die Argumente werden durch das Zielinterface bestimmt.Student::new kann mit einem passenden Konstruktor aus einem Namen ein Studierendenobjekt erzeugen.Kurzschlussauswertung
anyMatch, true zurückzugeben, sobald ein Element das Predicate erfüllt.L
Lambda-Ausdruck
->.(a, b) -> a * b liefert die Multiplikationsregel für eine Operation.limit
limit behält höchstens die angeforderte Anzahl von Elementen vom Anfang eines geordneten Streams, sodass spätere Eingaben übersprungen werden können.limit vor dem Sortieren wählt vom ursprünglichen Anfang aus, nicht die insgesamt höchsten Werte.M
map
map ersetzt jedes Eingabeelement durch ein berechnetes Ergebnis und kann so den Elementtyp des Streams ändern.map mit String::length wandelt Wörter in Integer-Wrapperwerte um, nicht in einen Stream primitiver Werte.Map-Eintrag
mapping
mapping wandelt Elemente um, bevor er ihre Ergebnisse an einen anderen Collector weitergibt, häufig innerhalb einer Gruppe oder Partition.mapping-Collector kann innerhalb jeder Gruppe die Namen der Studierenden extrahieren, bevor eine Liste gesammelt wird.Mehrfache Typschranken
&, sodass ein Typargument jede Schranke erfüllen muss. Eine Klasse steht, falls vorhanden, an erster Stelle.<T extends Number & Comparable<T>> verlangen numerisches Verhalten und Vergleichsverhalten.Methodenreferenz
:: und passt zu den Eingaben und dem Ergebnis eines funktionalen Interfaces als Zieltyp.Integer::parseInt stellt einer Funktion von String zu Integer Parsen als Verhalten bereit.N
Nach oben beschränkte Wildcard
? extends Number bezeichnet einen unbekannten Typ, der der angegebene Typ oder einer seiner Untertypen ist.Nach unten beschränkte Wildcard
? super Integer bezeichnet einen unbekannten Typ, der der angegebene Typ oder einer seiner Obertypen ist.List<Number> als Ziel zu akzeptieren.Nachgeschalteter Collector
counting() wandelt eine Map mit Gruppenlisten in eine Map mit Anzahlen vom Typ Long um.Natürliche Ordnung
Comparable.compareTo definiert und die verwendet wird, wenn kein separater Comparator angegeben ist."Cherry" vor das kleingeschriebene "apple".Neutrales Element
Nicht modifizierbare Liste
Stream.toList() wirft eine Ausnahme, wenn Code versucht, ein weiteres Element hinzuzufügen.Nichtinterferenz
ArrayList hinzufügt.O
Obere Typschranke
<T extends Number> erlaubt Aufrufe von doubleValue() auf einem T.Optional
Optional aus einem gefilterten Maximum kann leer sein, sodass ein Ersatzwert den fehlenden Fall behandelt.OptionalDouble
OptionalDouble aus average() kann über orElse einen gewählten Ersatzwert 0.0 liefern.P
partitioningBy
partitioningBy teilt Elemente anhand eines Predicate auf und gibt immer beide booleschen Schlüssel zurück, auch wenn eine Seite leer ist.partitioningBy-Collector kann eine leere Liste für true zurückgeben, wenn kein Studierender den vom Predicate geprüften Schwellenwert erreicht.PECS-Regel
peek
peek gibt Elemente unverändert weiter und führt für durchlaufene Werte eine Untersuchungsaktion aus; ihre Aufrufe können übersprungen werden.peek zur Ausgabe von Werten ist für erforderliche Arbeit unzuverlässig, weil die Ausführung seine Stufe auslassen kann.Pipeline
stream über filter zu count liefert die Anzahl der Treffer als long-Wert.Predicate
test einen boolean zurückgibt.removeIf übergebenes Predicate<String> bestimmt passende Wörter, die gelöscht werden sollen.Prüfoperation
anyMatch, aber true für allMatch.R
Reduktion
Roher Typ
List xs kann einen fehlerhaften Schreibzugriff erlauben, den eine List<String> ablehnen würde.S
Schlüsselextraktor
Student::getPoints lässt sich durch comparingInt ein numerischer Comparator für Studierende erstellen.Seiteneffekt
Sekundäres Vergleichskriterium
Signatur nach Typlöschung
show(Set) ist bei den versuchten Überladungen mit Set<String> und Set<Long> gleich.skip
skip verwirft die angeforderte Anzahl anfänglicher Elemente eines geordneten Streams und gibt die übrigen Elemente weiter.skip(2) auf dem Bereich 1 bis einschließlich 10 ist der nächste ausgegebene Wert 3.Spezialisierung für primitive Datentypen
ToIntFunction<String> liefert Längen direkt an mapToInt.Stabile Sortierung
Statischer Kontext
<E> ein, statt das T der Klasse zu verwenden.Stream
points kann die Werte zählen, die eine Prüfung bestehen, und die Liste dabei unverändert lassen.Supplier
get ohne Argumente einen Wert seines Ergebnistyps bereitstellt.Supplier<List<String>> mit ArrayList::new kann bei jedem Aufruf eine neue Liste bereitstellen.T
Terminaloperation
count gibt einen long zurück und lässt denselben Stream für eine Wiederverwendung nicht verfügbar.Transitivität
Typargument
String sorgt dafür, dass ein Getter von Box<String> Text ohne explizite Typumwandlung zurückgibt.Typinferenz
last(words) einen String, wenn words eine Liste von Strings ist.Typlöschung
Object; der Compiler fügt notwendige explizite Typumwandlungen ein.Box.Typparameter
T gibt dem Feld und dem Getter einer Box denselben vom aufrufenden Code gewählten Typ.Typzeuge
Lists.<String>last(words) gibt die Typwahl der Methode ausdrücklich an.U
UnaryOperator
UnaryOperator<String> kann über replaceAll einer modifizierbaren Liste jedes Wort in Großbuchstaben umwandeln.Unbeschränkte Wildcard
? ohne Typschranke. Sie steht für ein unbekanntes Argument und erlaubt das Lesen von Elementen nur als Object.List<?> ermöglicht es einer Anzeigemethode, die Listengröße abzufragen, ohne ihren Elementtyp zu kennen.Unchecked-Warnung
add auf einer rohen Liste weist auf einen möglichen späteren Lesezugriff mit falschem Typ hin.Ungebundene Methodenreferenz
String::length ruft length() auf jedem übergebenen String auf.V
Verzögerte Auswertung
findFirst, anzuhalten, sobald ein passendes Wort gefunden und umgewandelt wurde.W
Wrapperklasse
Integer macht List<Integer> gültig, während List<int> abgelehnt wird.Z
Zieltyp
Predicate<String> macht s -> s.isEmpty() zu einer booleschen Prüfung eines Strings.Zusammenführungsfunktion
Math::max behält die größte Menge, wenn ein Bestandsschlüssel zweimal vorkommt.Zwischenergebnis
Zwischenoperation
filter wartet auf eine Terminaloperation, bevor sie Elemente prüft.- 1Java-Programme kompilieren und starten
- 2Bedingungen und Schleifen in Java
- 3Java-Methoden und Problemzerlegung
- 4Listen, Arrays und Strings in Java
- 5Java-Klassen und Objekte
- 6Objektreferenzen in Java
- 7HashMaps und Collections in Java
- 8Vererbung in Java
- 9Ausnahmen und Dateien in Java
- 10Modultests in Java
- 11Lambda-Ausdrücke und Streams in Java
Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.