Tutorium 24.06

Lambda

Ein Lambda ist in Java eine kurze Schreibweise für die Implementierung genau einer abstrakten Methode eines funktionalen Interfaces.

Eigenes funktionales Interface mit Lambda
1@FunctionalInterface
2public interface Funktion2p {
3    double rechne(double x, double y);
4}
5
6Funktion2p f = (a, b) -> a + b;
Wichtige funktionale Interfaces aus Java

Interface

Bedeutung

Methode

Beispiel

Function<T, R>

Eingabe T, Ausgabe R

apply

text -> text.length()

Predicate<T>

Eingabe T, Ausgabe boolean

test

text -> text.isEmpty()

Consumer<T>

Eingabe T, keine Ausgabe

accept

text -> System.out.println(text)

Supplier<T>

keine Eingabe, Ausgabe T

get

() -> Math.random()

BiFunction<T, U, R>

zwei Eingaben, eine Ausgabe

apply

(a, b) -> a + b

UnaryOperator<T>

Eingabe und Ausgabe gleicher Typ

apply

x -> x * x

BinaryOperator<T>

zwei Eingaben gleicher Typ, Ausgabe gleicher Typ

apply

(a, b) -> a + b

Frage 1

Was ist ein Lambda-Ausdruck in Java in einem Satz?

Lösung anzeigen

Ein Lambda-Ausdruck ist eine kurze Schreibweise für die Implementierung genau einer abstrakten Methode eines funktionalen Interfaces.

Beispiel
1Funktion1p quadrat = x -> x * x;

Das Lambda beschreibt hier, was die Methode rechne(double x) tun soll.

Frage 2

Warum kann Java mit diesem Ausdruck allein noch nichts anfangen?

(a, b) -> a + b
Lösung anzeigen

Der Ausdruck hat allein keinen eindeutigen Typ. Java weiß ohne Kontext nicht:

  • Welche Datentypen haben a und b?

  • Welcher Rückgabetyp ist gemeint?

  • Welche Methode soll dadurch implementiert werden?

  • Ist das eine BiFunction, ein DoubleBinaryOperator, ein eigenes Interface oder etwas anderes?

Deshalb braucht ein Lambda immer einen Zieltyp.

Frage 3

Warum funktioniert diese Zuweisung nicht?

var f = (a, b) -> a + b;
Lösung anzeigen

var muss den Typ aus der rechten Seite ableiten. Bei einem Lambda geht das ohne Zieltyp nicht, weil das Lambda selbst Java nicht genug Informationen liefert.

Korrekt wäre zum Beispiel:

1BinaryOperator<Integer> f = (a, b) -> a + b;

oder mit eigenem Interface:

1Funktion2p f = (a, b) -> a + b;

Frage 4

Warum funktioniert diese Zuweisung?

Funktion2p f = (a, b) -> a + b;

Gegeben sei:

1@FunctionalInterface
2public interface Funktion2p {
3    double rechne(double x, double y);
4}
Lösung anzeigen

Funktion2p hat genau eine abstrakte Methode:

double rechne(double x, double y);

Der Compiler kann deshalb ableiten:

  • a und b sind double,

  • das Ergebnis muss ein double sein,

  • das Lambda implementiert die Methode rechne.

Das Lambda ist also die Implementierung dieser Methode.

Frage 5

Was bedeutet @FunctionalInterface?

1@FunctionalInterface
2public interface Funktion1p {
3    double rechne(double x);
4}
Lösung anzeigen

@FunctionalInterface bedeutet: Dieses Interface soll genau eine abstrakte Methode besitzen. Der Compiler prüft das.

Wenn man eine zweite abstrakte Methode hinzufügen würde, meldet der Compiler einen Fehler. Die Annotation ist nicht zwingend notwendig, macht die Absicht aber klar.

Frage 6

Ist folgendes Interface für ein Lambda geeignet? Begründe kurz.

1public interface Rechner {
2    double addiere(double a, double b);
3    double multipliziere(double a, double b);
4}
Lösung anzeigen

Nein. Das Interface hat zwei abstrakte Methoden. Ein Lambda kann aber nur genau eine abstrakte Methode implementieren. Java wüsste nicht, ob das Lambda addiere oder multipliziere implementieren soll.

Frage 7

Ist folgendes Interface für ein Lambda geeignet? Begründe kurz.

1public interface Aktion {
2    void ausfuehren();
3
4    default void beschreibungAusgeben() {
5        System.out.println("Eine Aktion wird ausgefuehrt.");
6    }
7}
Lösung anzeigen

Ja. Es gibt nur eine abstrakte Methode: void ausfuehren(). Die Methode beschreibungAusgeben() ist eine default-Methode und besitzt bereits eine Implementierung. Sie zählt daher nicht als abstrakte Methode.

Beispiel:

1Aktion a = () -> System.out.println("Hallo");
2a.ausfuehren();

Frage 8

Was steht bei einem Lambda links vom Pfeil und was rechts vom Pfeil?

x -> x * x
Lösung anzeigen

Links vom Pfeil steht der Parameter: x

Rechts vom Pfeil steht die Implementierung, also die Berechnung: x * x

Man kann es lesen als:

Nimm x und liefere x * x zurück.

Frage 9

Sind diese beiden Lambdas gleichwertig?

x -> x * x
(x) -> x * x
Lösung anzeigen

Ja. Bei genau einem Parameter darf man die Klammern weglassen.

Bei mehreren Parametern braucht man Klammern:

(a, b) -> a + b

Bei keinem Parameter braucht man ebenfalls Klammern:

() -> Math.random()

Frage 10

Warum braucht dieses Lambda kein return?

x -> x * x

Warum braucht dieses Lambda ein return?

1x -> {
2    System.out.println(x);
3    return x * x;
4}
Lösung anzeigen

Das erste Lambda braucht kein return, weil rechts vom Pfeil nur ein einzelner Ausdruck steht. Das Ergebnis dieses Ausdrucks wird automatisch zurückgegeben.

Das zweite Lambda braucht return, weil der Lambda-Körper aus einem Block mit geschweiften Klammern besteht. Bei einem Block muss man explizit angeben, welcher Wert zurückgegeben wird.

Frage 11

Welches funktionale Interface aus Java passt zu dieser Idee?

text -> text.length()

Also: Ein String wird in einen Integer umgewandelt.

Lösung anzeigen

Passend ist Function<String, Integer>.

1Function<String, Integer> laenge = text -> text.length();
2System.out.println(laenge.apply("Hallo"));

Die Methode heißt bei Function: apply

Frage 12

Welches funktionale Interface aus Java passt zu dieser Idee?

text -> text.length() > 10

Also: Ein String wird auf true oder false geprüft.

Lösung anzeigen

Passend ist Predicate<String>.

1Predicate<String> istLang = text -> text.length() > 10;
2System.out.println(istLang.test("Programmieren"));

Die Methode heißt bei Predicate: test

Ein Predicate ist immer eine Prüfung auf true oder false.

Frage 13

Welches funktionale Interface aus Java passt zu dieser Idee?

text -> System.out.println(text)

Also: Ein String wird verarbeitet, aber es wird kein Wert zurückgegeben.

Lösung anzeigen

Passend ist Consumer<String>.

1Consumer<String> ausgabe = text -> System.out.println(text);
2ausgabe.accept("Hallo");

Die Methode heißt bei Consumer: accept

Ein Consumer verbraucht einen Wert, gibt aber keinen Wert zurück.

Frage 14

Welches funktionale Interface aus Java passt zu dieser Idee?

() -> Math.random()

Also: Es gibt keine Eingabe, aber es wird ein Wert erzeugt.

Lösung anzeigen

Passend ist Supplier<Double>.

1Supplier<Double> zufall = () -> Math.random();
2System.out.println(zufall.get());

Die Methode heißt bei Supplier: get

Ein Supplier liefert einen Wert, bekommt aber keine Eingabe.

Frage 15

Welches funktionale Interface aus Java passt zu dieser Idee?

(a, b) -> a + b

Also: Zwei Werte werden zu einem Ergebnis verarbeitet.

Lösung anzeigen

Passend ist zum Beispiel BiFunction<Integer, Integer, Integer>.

1BiFunction<Integer, Integer, Integer> addiere = (a, b) -> a + b;
2System.out.println(addiere.apply(3, 4));

Wenn man mit double arbeitet, ist oft diese Variante besser:

1DoubleBinaryOperator addiere = (a, b) -> a + b;
2System.out.println(addiere.applyAsDouble(3.0, 4.0));

Frage 16

Was gibt dieser Code aus?

1Predicate<String> istLang = text -> text.length() > 5;
2
3System.out.println(istLang.test("Hallo"));
4System.out.println(istLang.test("Programmieren"));
Lösung anzeigen
Ausgabe
false
true

Erklärung:

  • "Hallo" hat 5 Zeichen. 5 > 5 ist false.

  • "Programmieren" hat mehr als 5 Zeichen. Das Ergebnis ist true.

Frage 17

Was gibt dieser Code aus?

1Function<String, Integer> laenge = text -> text.length();
2
3int ergebnis = laenge.apply("Java");
4System.out.println(ergebnis);
Lösung anzeigen
Ausgabe
4

Das Lambda berechnet die Länge des Strings. "Java" hat 4 Zeichen.

Frage 18

Ersetze die anonyme Klasse durch ein Lambda.

1Runnable r = new Runnable() {
2    @Override
3    public void run() {
4        System.out.println("Programm laeuft");
5    }
6};
7
8r.run();
Lösung anzeigen
1Runnable r = () -> System.out.println("Programm laeuft");
2
3r.run();

Das funktioniert, weil Runnable genau eine abstrakte Methode hat: void run()

Frage 19

Ersetze die anonyme Klasse durch ein Lambda.

1Comparator<String> nachLaenge = new Comparator<String>() {
2    @Override
3    public int compare(String a, String b) {
4        return a.length() - b.length();
5    }
6};
Lösung anzeigen
1Comparator<String> nachLaenge = (a, b) -> a.length() - b.length();

Noch etwas sauberer:

1Comparator<String> nachLaenge =
2        (a, b) -> Integer.compare(a.length(), b.length());

Oder mit Methodenreferenz:

1Comparator<String> nachLaenge = Comparator.comparingInt(String::length);

Für den Anfang ist aber die Lambda-Variante am besten verständlich.

Frage 20

Wann ist ein Lambda sinnvoll und wann eher nicht? Nenne jeweils ein Beispiel.

Lösung anzeigen

Ein Lambda ist sinnvoll, wenn eine kleine Funktion einmalig oder direkt an Ort und Stelle gebraucht wird.

1List<String> namen = List.of("Anna", "Max", "Christopher");
2namen.sort((a, b) -> a.length() - b.length());

Ein Lambda ist eher nicht sinnvoll, wenn die Logik lang, kompliziert oder fachlich wichtig ist. Dann ist eine benannte Methode meistens besser.

Lambdas sind gut für kurze, lokale Logik. Bei langer oder wichtiger Fachlogik ist eine benannte Methode meistens besser.

Collections

Collections sind Datenstrukturen, mit denen mehrere Werte gespeichert und verarbeitet werden können.

Überblick über die wichtigsten Collection-Typen

Typ

Grundidee

Typische Frage

List<E>

geordnete Sammlung, Duplikate erlaubt

In welcher Reihenfolge stehen die Elemente?

Set<E>

Sammlung ohne Duplikate

Ist dieses Element enthalten?

Map<K, V>

Zuordnung von Schlüssel zu Wert

Welcher Wert gehört zu diesem Schlüssel?

Queue<E>

Warteschlange

Wer ist als Nächstes dran?

Deque<E>

beidseitige Warteschlange

Vorne oder hinten einfügen/entfernen?

Wichtige Implementierungen

Interface

Implementierung

Eigenschaft

List

ArrayList

Standardliste, schneller Zugriff per Index

Set

HashSet

keine Duplikate, keine garantierte Reihenfolge

Set

LinkedHashSet

keine Duplikate, Einfügereihenfolge bleibt erhalten

Set

TreeSet

keine Duplikate, sortierte Reihenfolge

Map

HashMap

Standard-Map, schnelle Zuordnung

Map

LinkedHashMap

Map mit Einfügereihenfolge

Map

TreeMap

Map mit sortierten Schlüsseln

Map

EnumMap

sehr effiziente Map, wenn der Schlüssel ein enum ist

Frage 1

Du willst eine Liste von Namen speichern. Die Reihenfolge soll erhalten bleiben und Namen dürfen mehrfach vorkommen. Welche Collection passt?

Lösung anzeigen

Passend ist eine List<String>, meistens eine ArrayList<String>.

1List<String> namen = new ArrayList<>();

Grund:

  • Reihenfolge bleibt erhalten.

  • Duplikate sind erlaubt.

  • Man kann per Index zugreifen.

Frage 2

Du willst speichern, welche Studierenden bei einer Veranstaltung anwesend waren. Jede Person soll nur einmal vorkommen. Welche Collection passt?

Lösung anzeigen

Passend ist ein Set<String>, meistens ein HashSet<String>.

1Set<String> anwesend = new HashSet<>();

Grund:

  • Jede Person soll nur einmal vorkommen.

  • Ein Set verhindert Duplikate automatisch.

Frage 3

Du willst zu einer Matrikelnummer den Namen eines Studierenden speichern.

12345 -> "Anna"
67890 -> "Max"

Welche Collection passt?

Lösung anzeigen

Passend ist eine Map<Integer, String>.

1Map<Integer, String> studenten = new HashMap<>();
2studenten.put(12345, "Anna");
3studenten.put(67890, "Max");

Eine Map speichert eine Zuordnung von Schlüssel zu Wert.

Frage 4

Was ist der wichtigste Unterschied zwischen List und Set?

Lösung anzeigen
  • Eine List erlaubt Duplikate und hat eine feste Reihenfolge.

  • Ein Set erlaubt keine Duplikate.

 1List<String> liste = new ArrayList<>();
 2liste.add("Anna");
 3liste.add("Anna");
 4
 5Set<String> set = new HashSet<>();
 6set.add("Anna");
 7set.add("Anna");
 8
 9System.out.println(liste); // [Anna, Anna]
10System.out.println(set);   // [Anna]

Frage 5

Was ist der wichtigste Unterschied zwischen Set und Map?

Lösung anzeigen
  • Ein Set speichert einzelne Werte.

  • Eine Map speichert Paare aus Schlüssel und Wert.

1Set<String> namen = new HashSet<>();
2namen.add("Anna");
3
4Map<String, Integer> alter = new HashMap<>();
5alter.put("Anna", 22);

Bei der Map fragt man: Welcher Wert gehört zu diesem Schlüssel?

Frage 6

Was gibt dieser Code aus?

1List<String> namen = new ArrayList<>();
2
3namen.add("Anna");
4namen.add("Max");
5namen.add("Anna");
6
7System.out.println(namen);
Lösung anzeigen
Ausgabe
[Anna, Max, Anna]

Eine List erlaubt Duplikate und behält die Reihenfolge bei.

Frage 7

Was gibt dieser Code ungefähr aus? Warum steht "Anna" nicht zweimal darin?

1Set<String> namen = new HashSet<>();
2
3namen.add("Anna");
4namen.add("Max");
5namen.add("Anna");
6
7System.out.println(namen);
Lösung anzeigen

Mögliche Ausgabe:

[Max, Anna]

"Anna" steht nur einmal darin. Ein Set speichert jedes Element nur einmal. Das zweite add("Anna") fügt keinen neuen Eintrag hinzu.

Wichtig: Die Reihenfolge bei einem HashSet ist nicht garantiert.

Frage 8

Warum sollte man sich bei einem HashSet nicht auf die Reihenfolge der Ausgabe verlassen?

Lösung anzeigen

Bei einem HashSet ist die Reihenfolge intern nicht definiert. Sie hängt vom Hashwert der Elemente ab und kann sich ändern.

Wenn die Einfügereihenfolge wichtig ist:

Set<String> namen = new LinkedHashSet<>();

Wenn eine sortierte Reihenfolge wichtig ist:

Set<String> namen = new TreeSet<>();

Frage 9

Welche Collection passt, wenn jedes Element nur einmal vorkommen soll, aber die Einfügereihenfolge erhalten bleiben soll?

Lösung anzeigen

Passend ist LinkedHashSet.

1Set<String> namen = new LinkedHashSet<>();
2
3namen.add("Anna");
4namen.add("Max");
5namen.add("Anna");
6namen.add("Lena");
7
8System.out.println(namen);
Ausgabe
[Anna, Max, Lena]

Frage 10

Welche Collection passt, wenn jedes Element nur einmal vorkommen soll und automatisch sortiert werden soll?

Lösung anzeigen

Passend ist TreeSet.

1Set<String> namen = new TreeSet<>();
2
3namen.add("Max");
4namen.add("Anna");
5namen.add("Lena");
6namen.add("Anna");
7
8System.out.println(namen);
Ausgabe
[Anna, Lena, Max]

TreeSet entfernt Duplikate und sortiert die Elemente automatisch.

Frage 11

Was gibt dieser Code aus? Warum?

1Map<String, Integer> alter = new HashMap<>();
2
3alter.put("Anna", 22);
4alter.put("Max", 25);
5alter.put("Anna", 23);
6
7System.out.println(alter.get("Anna"));
Lösung anzeigen
Ausgabe
23

In einer Map kann jeder Schlüssel nur einmal vorkommen. Der zweite put-Aufruf mit dem Schlüssel "Anna" überschreibt den ersten Wert.

Frage 12

Was ist der Unterschied zwischen diesen beiden Operationen?

namen.add("Anna");
alter.put("Anna", 22);
Lösung anzeigen

add fügt ein einzelnes Element in eine Collection ein.

put speichert eine Zuordnung in einer Map:

Schlüssel: "Anna"
Wert:      22

Bei add geht es um einzelne Elemente. Bei put geht es um Schlüssel-Wert-Paare.

Frage 13

Was passiert, wenn man bei einer Map einen Wert mit einem bereits vorhandenen Schlüssel einfügt?

1Map<String, String> emails = new HashMap<>();
2
3emails.put("anna", "anna@example.com");
4emails.put("anna", "anna.neu@example.com");
5
6System.out.println(emails);
Lösung anzeigen
Ausgabe
{anna=anna.neu@example.com}

Der zweite put-Aufruf überschreibt den alten Wert zum gleichen Schlüssel.

Frage 14

Welche Collection würdest du verwenden, um zu zählen, wie oft jeder Name vorkommt? Begründe kurz.

1List<String> namen = List.of(
2        "Anna", "Max", "Anna", "Lena", "Max", "Anna"
3);

Erwartetes Ergebnis:

Anna -> 3
Max  -> 2
Lena -> 1
Lösung anzeigen

Man verwendet eine Map<String, Integer>.

1Map<String, Integer> counts = new HashMap<>();

Grund: Man will zu jedem Namen eine Anzahl speichern.

Name -> Anzahl

Das ist genau der typische Einsatzzweck einer Map.

Frage 15

Ergänze den Code, sodass die Namen gezählt werden.

 1List<String> namen = List.of(
 2        "Anna", "Max", "Anna", "Lena", "Max", "Anna"
 3);
 4
 5Map<String, Integer> counts = new HashMap<>();
 6
 7for (String name : namen) {
 8    // Was fehlt hier?
 9}
10
11System.out.println(counts);
Lösung anzeigen

Eine verständliche Lösung mit containsKey:

1for (String name : namen) {
2    if (counts.containsKey(name)) {
3        counts.put(name, counts.get(name) + 1);
4    } else {
5        counts.put(name, 1);
6    }
7}

Kompakter mit getOrDefault:

1for (String name : namen) {
2    counts.put(name, counts.getOrDefault(name, 0) + 1);
3}

Noch kompakter mit merge:

1for (String name : namen) {
2    counts.merge(name, 1, Integer::sum);
3}

Für Anfänger ist die containsKey-Variante oft am besten, weil man den Ablauf klar sieht.

Frage 16

Was macht getOrDefault in diesem Beispiel?

counts.put(name, counts.getOrDefault(name, 0) + 1);
Lösung anzeigen

Diese Zeile bedeutet:

  • Wenn name schon in der Map steht, nimm den bisherigen Wert.

  • Wenn name noch nicht in der Map steht, nimm stattdessen 0.

  • Addiere 1.

  • Speichere den neuen Wert wieder in der Map.

Beispiel:

counts.getOrDefault("Anna", 0)

liefert entweder den bisherigen Wert von "Anna" oder 0, falls "Anna" noch nicht vorhanden ist.

Frage 17

Was gibt dieser Code aus?

1List<String> namen = List.of("Anna", "Max", "Lena");
2
3System.out.println(namen.contains("Max"));
4System.out.println(namen.contains("Tom"));
Lösung anzeigen
Ausgabe
true
false

contains prüft, ob ein Element enthalten ist.

Frage 18

Warum ist ein Set oft besser geeignet als eine List, wenn man sehr oft prüfen möchte, ob ein Element enthalten ist?

Lösung anzeigen

Bei einer List muss Java im schlechtesten Fall alle Elemente nacheinander durchsuchen.

Ein HashSet ist für Mitgliedschaftsprüfungen typischerweise deutlich besser geeignet:

1Set<String> verboteneNamen = new HashSet<>();
2
3verboteneNamen.add("admin");
4verboteneNamen.add("root");
5verboteneNamen.add("system");
6
7if (verboteneNamen.contains(name)) {
8    System.out.println("Name ist nicht erlaubt.");
9}

Frage 19

Entferne Duplikate aus einer Liste, aber behalte die ursprüngliche Reihenfolge.

1List<String> namen = List.of(
2        "Anna", "Max", "Anna", "Lena", "Max"
3);

Erwartetes Ergebnis:

[Anna, Max, Lena]

Welche Collection hilft?

Lösung anzeigen

Passend ist ein LinkedHashSet.

1Set<String> eindeutig = new LinkedHashSet<>(namen);
2System.out.println(eindeutig);
Ausgabe
[Anna, Max, Lena]

Wenn wieder eine List gebraucht wird:

1List<String> ohneDuplikate = new ArrayList<>(new LinkedHashSet<>(namen));

Frage 20

Gruppiere Studierende nach Kurs. Gesucht ist eine Struktur wie:

Programmieren -> [Anna, Max]
Datenbanken   -> [Lena, Max]

Welche Collection-Kombination passt?

Lösung anzeigen

Passend ist Map<String, List<String>>.

1Map<String, List<String>> kurse = new HashMap<>();
2
3kurse.computeIfAbsent("Programmieren", k -> new ArrayList<>()).add("Anna");
4kurse.computeIfAbsent("Programmieren", k -> new ArrayList<>()).add("Max");
5kurse.computeIfAbsent("Datenbanken", k -> new ArrayList<>()).add("Lena");
6kurse.computeIfAbsent("Datenbanken", k -> new ArrayList<>()).add("Max");
7
8System.out.println(kurse);

Der Typ Map<String, List<String>> bedeutet:

Kursname -> Liste von Studierenden

Jeder Schlüssel führt also nicht zu einem einzelnen Wert, sondern zu einer Liste von Werten.

Frage 21

Welche Collection passt für eine Warteschlange, zum Beispiel bei Druckaufträgen?

Lösung anzeigen

Passend ist eine Queue, zum Beispiel mit ArrayDeque.

1Queue<String> druckauftraege = new ArrayDeque<>();
2
3druckauftraege.add("Dokument1.pdf");
4druckauftraege.add("Dokument2.pdf");
5
6System.out.println(druckauftraege.poll());
7System.out.println(druckauftraege.poll());
Ausgabe
Dokument1.pdf
Dokument2.pdf

Eine Queue arbeitet nach dem Prinzip: First In, First Out. Der erste Auftrag wird zuerst verarbeitet.

Frage 22

Welche Collection passt, wenn man Elemente vorne und hinten einfügen oder entfernen will?

Lösung anzeigen

Passend ist eine Deque, zum Beispiel mit ArrayDeque.

1Deque<String> namen = new ArrayDeque<>();
2
3namen.addFirst("Anna");
4namen.addLast("Max");
5
6System.out.println(namen.removeFirst());
7System.out.println(namen.removeLast());

Eine Deque kann vorne und hinten arbeiten.

Frage 23

Was ist an diesem Code problematisch?

1Map<String, Integer> lager = new HashMap<>();
2
3lager.put("Apfel", 30);
4lager.put("Banane", 12);
5
6System.out.println(lager.get("Milch") + 1);
Lösung anzeigen

lager.get("Milch") liefert null, weil "Milch" nicht in der Map steht. Dann versucht Java sinngemäß:

null + 1

Das führt zu einer NullPointerException.

Frage 24

Wie kann man das Problem aus Frage 23 mit getOrDefault lösen?

Lösung anzeigen
1Map<String, Integer> lager = new HashMap<>();
2
3lager.put("Apfel", 30);
4lager.put("Banane", 12);
5
6System.out.println(lager.getOrDefault("Milch", 0) + 1);
Ausgabe
1

Wenn "Milch" nicht vorhanden ist, wird 0 verwendet.

Frage 25

Was ist der Unterschied zwischen keySet, values und entrySet bei einer Map?

Lösung anzeigen
1Map<String, Integer> alter = new HashMap<>();
2
3alter.put("Anna", 22);
4alter.put("Max", 25);
5
6System.out.println(alter.keySet());   // alle Schlüssel
7System.out.println(alter.values());   // alle Werte
8System.out.println(alter.entrySet()); // alle Schlüssel-Wert-Paare

Mögliche Ausgabe:

[Anna, Max]
[22, 25]
[Anna=22, Max=25]

Die genaue Reihenfolge ist bei HashMap nicht garantiert.

Frage 26

Welche Schleife ist meistens besser, wenn man sowohl Schlüssel als auch Wert einer Map braucht?

Variante A:

1for (String key : map.keySet()) {
2    System.out.println(key + ": " + map.get(key));
3}

Variante B:

1for (Map.Entry<String, Integer> entry : map.entrySet()) {
2    System.out.println(entry.getKey() + ": " + entry.getValue());
3}
Lösung anzeigen

Meistens ist Variante B besser. Man bekommt Schlüssel und Wert direkt zusammen, ohne für jeden Schlüssel den Wert erneut aus der Map zu holen.

Frage 27

Sortiere Namen alphabetisch und entferne gleichzeitig Duplikate.

1List<String> namen = List.of(
2        "Max", "Anna", "Lena", "Anna", "Tom"
3);

Erwartetes Ergebnis:

[Anna, Lena, Max, Tom]

Welche Collection passt?

Lösung anzeigen

Passend ist ein TreeSet.

1Set<String> sortiert = new TreeSet<>(namen);
2System.out.println(sortiert);
Ausgabe
[Anna, Lena, Max, Tom]

TreeSet entfernt Duplikate und sortiert die Elemente. Wenn wieder eine Liste gebraucht wird:

1List<String> ergebnis = new ArrayList<>(new TreeSet<>(namen));

Frage 28

Warum sollte man meistens gegen das Interface programmieren?

List<String> namen = new ArrayList<>();

statt:

ArrayList<String> namen = new ArrayList<>();
Lösung anzeigen

Der Rest des Codes muss nur wissen: Das ist eine List. Ob intern eine ArrayList, LinkedList oder eine andere Implementierung verwendet wird, ist oft egal.

Man kann später leichter austauschen:

List<String> namen = new LinkedList<>();

ohne den restlichen Code stark zu verändern.

Verwende links meistens das Interface, rechts die konkrete Implementierung.

Mini-Aufgabe

Ein kleines Programm soll Bestellungen auswerten.

1List<String> bestellungen = List.of(
2        "Apfel", "Banane", "Apfel", "Milch", "Banane", "Apfel"
3);

Bearbeite folgende Teilaufgaben:

  1. Speichere alle bestellten Produkte in der ursprünglichen Reihenfolge.

  2. Ermittle alle unterschiedlichen Produkte (Reihenfolge egal).

  3. Ermittle alle unterschiedlichen Produkte in der Reihenfolge ihres ersten Auftretens.

  4. Ermittle, wie oft jedes Produkt bestellt wurde.

  5. Gib alle Produkte mit ihrer Anzahl aus.

Lösung anzeigen
Musterlösung
 1import java.util.*;
 2
 3public class BestellAuswertung {
 4    public static void main(String[] args) {
 5        List<String> bestellungen = List.of(
 6                "Apfel", "Banane", "Apfel", "Milch", "Banane", "Apfel"
 7        );
 8
 9        // 1. Ursprüngliche Reihenfolge speichern
10        List<String> alleBestellungen = new ArrayList<>(bestellungen);
11
12        // 2. Unterschiedliche Produkte, Reihenfolge egal
13        Set<String> unterschiedlicheProdukte = new HashSet<>(bestellungen);
14
15        // 3. Unterschiedliche Produkte, Reihenfolge des ersten Auftretens
16        Set<String> produkteInReihenfolge = new LinkedHashSet<>(bestellungen);
17
18        // 4. Anzahl pro Produkt zählen
19        Map<String, Integer> counts = new HashMap<>();
20        for (String produkt : bestellungen) {
21            counts.put(produkt, counts.getOrDefault(produkt, 0) + 1);
22        }
23
24        // 5. Ausgabe
25        System.out.println("Alle Bestellungen:");
26        System.out.println(alleBestellungen);
27
28        System.out.println("Unterschiedliche Produkte:");
29        System.out.println(unterschiedlicheProdukte);
30
31        System.out.println("Unterschiedliche Produkte in Reihenfolge:");
32        System.out.println(produkteInReihenfolge);
33
34        System.out.println("Anzahl pro Produkt:");
35        for (Map.Entry<String, Integer> entry : counts.entrySet()) {
36            System.out.println(entry.getKey() + ": " + entry.getValue());
37        }
38    }
39}

Mögliche Ausgabe:

Mögliche Ausgabe
Alle Bestellungen:
[Apfel, Banane, Apfel, Milch, Banane, Apfel]
Unterschiedliche Produkte:
[Apfel, Milch, Banane]
Unterschiedliche Produkte in Reihenfolge:
[Apfel, Banane, Milch]
Anzahl pro Produkt:
Apfel: 3
Milch: 1
Banane: 2

Hinweis: Die Reihenfolge bei HashSet und HashMap ist nicht garantiert. Die Ausgabe dort kann anders sortiert erscheinen.

Streams

Ein Stream in Java ermöglicht es, eine Folge von Elementen deklarativ zu verarbeiten. Statt einer Schleife beschreibt man, was mit den Elementen passieren soll, nicht wie.

Einfacher Stream
1List<String> namen = List.of("Anna", "Max", "Lena", "Tom");
2
3List<String> ergebnis = namen.stream()
4        .filter(n -> n.length() > 3)
5        .sorted()
6        .collect(Collectors.toList());
7
8System.out.println(ergebnis); // [Anna, Lena]
Wichtige Stream-Operationen

Operation

Beschreibung

Beispiel

.filter

Nur Elemente durchlassen, die eine Bedingung erfüllen

.filter(n -> n.length() > 3)

.map

Jedes Element in etwas anderes umwandeln

.map(String::toUpperCase)

.sorted

Elemente sortieren (natürlich oder mit Comparator)

.sorted() / .sorted((a, b) -> ...)

.collect

Ergebnis zusammenfassen, z. B. in eine Liste

.collect(Collectors.toList())

.forEach

Jedes Element verarbeiten (kein Rückgabewert)

.forEach(System.out::println)

.count

Anzahl der verbleibenden Elemente zählen

.count()

.distinct

Duplikate entfernen

.distinct()

.limit

Nur die ersten N Elemente weitergeben

.limit(3)

.noneMatch

true, wenn kein Element die Bedingung erfüllt

.noneMatch(n -> n < 0)

Frage 1

Was gibt dieses Programm aus?

 1import java.util.*;
 2
 3public class Pipeline1 {
 4
 5    public static void main(String[] args) {
 6        List<String> strings = Arrays.asList("bb", "ZZ", "aa", "PP", "zz");
 7
 8        strings
 9            .stream()
10            .sorted()
11            .map(String::toUpperCase)
12            .forEach(n -> System.out.print(n + " "));
13    }
14}
Lösung anzeigen
Ausgabe
PP ZZ AA BB ZZ

sorted() verwendet die natürliche Reihenfolge von Strings. Großbuchstaben haben niedrigere Unicode-Werte als Kleinbuchstaben (z. B. 'Z' = 90, 'a' = 97). Deshalb kommen "PP" und "ZZ" vor "aa", "bb" und "zz".

Sortiert: ["PP", "ZZ", "aa", "bb", "zz"]

Erst danach wandelt map(String::toUpperCase) alles in Großbuchstaben um. Die Reihenfolge ändert sich dabei nicht mehr.

Original

nach sorted

nach toUpperCase

"bb"

"PP"

"PP"

"ZZ"

"ZZ"

"ZZ"

"aa"

"aa"

"AA"

"PP"

"bb"

"BB"

"zz"

"zz"

"ZZ"

Frage 2

Was gibt dieses Programm aus? Erkläre jeden Schritt kurz.

 1import java.util.*;
 2
 3public class Pipeline2 {
 4
 5    public static void main(String[] args) {
 6        List<String> strings = Arrays.asList(
 7            "bbb", "ZZZ", "aaa", "PPP",
 8            "zzz", "fff", "vvv"
 9        );
10
11        boolean i = strings
12            .stream()
13            .distinct()
14            .sorted((a, b) -> a.compareToIgnoreCase(b))
15            .limit(4)
16            .map(a -> a.substring(0, 2))
17            .noneMatch(a -> a.equals("aa"));
18
19        System.out.println(i);
20    }
21}
Lösung anzeigen
Ausgabe
false

Schrittweise Erklärung:

  1. .distinct(): Alle Elemente sind bereits eindeutig — keine Änderung.

  2. .sorted((a, b) -> a.compareToIgnoreCase(b)): Sortiert ohne Unterschied zwischen Groß- und Kleinschreibung:

    ["aaa", "bbb", "fff", "PPP", "vvv", "ZZZ", "zzz"]

  3. .limit(4): Nur die ersten 4 Elemente behalten:

    ["aaa", "bbb", "fff", "PPP"]

  4. .map(a -> a.substring(0, 2)): Jedes Element auf die ersten 2 Zeichen kürzen:

    ["aa", "bb", "ff", "PP"]

  5. .noneMatch(a -> a.equals("aa")): Prüft, ob kein Element gleich "aa" ist. Da "aa" vorhanden ist, liefert noneMatch false.

Frage 3

Ergänze die zwei fehlenden Stream-Operationen, sodass nur die geraden Zahlen aus der Liste aufsteigend sortiert ausgegeben werden.

Erwartete Ausgabe:

2
4
6
 1import java.util.*;
 2
 3public class Pipeline3 {
 4
 5    public static void main(String[] args) {
 6
 7        List<Integer> intArray = Arrays.asList(7, 6, 5, 4, 3, 2, 1);
 8
 9        intArray
10            .stream()
11            ._____________________________
12            ._____________________________
13            .forEach(System.out::println);
14    }
15}
Lösung anzeigen
1intArray
2    .stream()
3    .filter(n -> n % 2 == 0)
4    .sorted()
5    .forEach(System.out::println);
  • filter(n -> n % 2 == 0) behält nur gerade Zahlen: [6, 4, 2]

  • sorted() sortiert sie aufsteigend: [2, 4, 6]

  • forEach gibt jede Zahl in einer eigenen Zeile aus.

Wichtig: erst filtern, dann sortieren — so werden nur 3 statt 7 Elemente sortiert.

Aufgaben

Aufgabe 1: Benachrichtigungssystem

Eine Anwendung verwaltet verschiedene Arten von Benachrichtigungen. Jede Benachrichtigung hat einen Empfänger und einen Text. Es gibt unterschiedliche Kanäle, zum Beispiel E-Mail und SMS.

Gegeben ist folgendes Grundgerüst:

Abstrakte Basisklasse
 1public abstract class Nachricht {
 2
 3    private final String empfaenger;
 4    private final String text;
 5
 6    public Nachricht(String empfaenger, String text) {
 7        this.empfaenger = empfaenger;
 8        this.text = text;
 9    }
10
11    public String getEmpfaenger() {
12        return empfaenger;
13    }
14
15    public String getText() {
16        return text;
17    }
18
19    public abstract String kanal();
20
21    public abstract int prioritaet();
22}
Unterklassen und funktionales Interface
 1public class EmailNachricht extends Nachricht {
 2    public EmailNachricht(String empfaenger, String text) {
 3        super(empfaenger, text);
 4    }
 5
 6    @Override
 7    public String kanal() { return "E-Mail"; }
 8
 9    @Override
10    public int prioritaet() { return 1; }
11}
12
13public class SmsNachricht extends Nachricht {
14    public SmsNachricht(String empfaenger, String text) {
15        super(empfaenger, text);
16    }
17
18    @Override
19    public String kanal() { return "SMS"; }
20
21    @Override
22    public int prioritaet() { return 2; }
23}
24
25@FunctionalInterface
26public interface NachrichtenFormat {
27    String formatiere(Nachricht n);
28}

In der main-Methode existiert folgende Liste:

Ausgangsdaten
1List<Nachricht> nachrichten = new ArrayList<>();
2
3nachrichten.add(new EmailNachricht("anna@example.com", "Termin morgen"));
4nachrichten.add(new SmsNachricht("Max", "Bitte zurueckrufen"));
5nachrichten.add(new EmailNachricht("lena@example.com", "Unterlagen erhalten"));
6nachrichten.add(new SmsNachricht("Anna", "Raum wurde geaendert"));

Aufgaben:

  1. Erkläre, warum folgender Code bei SMS-Nachrichten "SMS" und bei E-Mail-Nachrichten "E-Mail" ausgibt, obwohl die Liste den Typ List<Nachricht> hat.

    1for (Nachricht n : nachrichten) {
    2    System.out.println(n.kanal());
    3}
    
  2. Erstelle ein Lambda vom Typ NachrichtenFormat, das eine Nachricht in folgender Form formatiert:

    KANAL an EMPFAENGER: TEXT
    

    Beispiel: SMS an Max: Bitte zurueckrufen

  3. Wende das Lambda auf alle Nachrichten an und gib das Ergebnis aus.

  4. Sortiere die Liste so, dass Nachrichten mit höherer Priorität zuerst kommen (SmsNachricht.prioritaet() == 2, EmailNachricht.prioritaet() == 1).

  5. Erstelle eine Collection, die alle Empfänger enthält, aber jeden Empfänger nur einmal. Die Reihenfolge des ersten Auftretens soll erhalten bleiben.

  6. Erstelle eine Map, die zählt, wie viele Nachrichten pro Kanal vorhanden sind. Erwartete Idee: {SMS=2, E-Mail=2}

  7. Beantworte kurz:

    • Warum wird für nachrichten eine List verwendet?

    • Warum ist für eindeutige Empfänger ein Set sinnvoll?

    • Warum braucht das Lambda aus Teilaufgabe 2 ein Interface?

    • Warum wäre var format = n -> n.kanal(); nicht erlaubt?

Lösung anzeigen

Lösung 1 – Polymorphie

Obwohl die Liste den Typ List<Nachricht> hat, befinden sich darin konkrete Objekte vom Typ EmailNachricht und SmsNachricht. Beim Methodenaufruf n.kanal() entscheidet Java zur Laufzeit, welche konkrete Implementierung verwendet wird. Das nennt man Polymorphie.

Lösung 2 – Lambda erstellen

1NachrichtenFormat format =
2        n -> n.kanal() + " an " + n.getEmpfaenger() + ": " + n.getText();

Das Lambda implementiert die Methode String formatiere(Nachricht n) des funktionalen Interfaces NachrichtenFormat.

Lösung 3 – Lambda anwenden

1for (Nachricht n : nachrichten) {
2    System.out.println(format.formatiere(n));
3}
Mögliche Ausgabe
E-Mail an anna@example.com: Termin morgen
SMS an Max: Bitte zurueckrufen
E-Mail an lena@example.com: Unterlagen erhalten
SMS an Anna: Raum wurde geaendert

Lösung 4 – Sortieren nach Priorität

1nachrichten.sort((a, b) -> Integer.compare(b.prioritaet(), a.prioritaet()));

b wird mit a verglichen, damit höhere Priorität zuerst steht. Die Variante mit Integer.compare ist robuster als eine einfache Subtraktion.

Lösung 5 – Eindeutige Empfänger

Da die Reihenfolge des ersten Auftretens erhalten bleiben soll, passt ein LinkedHashSet.

1Set<String> empfaenger = new LinkedHashSet<>();
2
3for (Nachricht n : nachrichten) {
4    empfaenger.add(n.getEmpfaenger());
5}
6
7System.out.println(empfaenger);
Mögliche Ausgabe
[anna@example.com, Max, lena@example.com, Anna]

Ein HashSet würde ebenfalls Duplikate entfernen, aber keine Reihenfolge garantieren.

Lösung 6 – Nachrichten pro Kanal zählen

1Map<String, Integer> counts = new HashMap<>();
2
3for (Nachricht n : nachrichten) {
4    String kanal = n.kanal();
5    counts.put(kanal, counts.getOrDefault(kanal, 0) + 1);
6}
7
8System.out.println(counts);
Mögliche Ausgabe
{SMS=2, E-Mail=2}

Lösung 7 – Verständnisfragen

  • Eine List wird verwendet, weil die Reihenfolge erhalten bleiben soll und mehrere Nachrichten vom gleichen Typ erlaubt sind.

  • Ein Set ist sinnvoll, weil jeder Empfänger nur einmal vorkommen soll.

  • Ein Lambda braucht ein funktionales Interface, damit Java weiß, welche abstrakte Methode das Lambda implementiert.

  • var format = n -> n.kanal(); ist nicht erlaubt, weil Java den Typ von n und das gemeinte Interface ohne Zieltyp nicht ableiten kann.

Aufgabe 2: Shop-Auswertung

Ein kleiner Shop verwaltet Produkte. Produkte haben einen Namen, eine Kategorie und einen Preis. Es gibt verschiedene Produktarten.

Gegeben ist folgendes Grundgerüst:

Enum, abstrakte Klasse und Unterklassen
 1public enum Kategorie {
 2    BUCH,
 3    LEBENSMITTEL,
 4    TECHNIK
 5}
 6
 7public abstract class Produkt {
 8
 9    private final String name;
10    private final Kategorie kategorie;
11
12    public Produkt(String name, Kategorie kategorie) {
13        this.name = name;
14        this.kategorie = kategorie;
15    }
16
17    public String getName() { return name; }
18    public Kategorie getKategorie() { return kategorie; }
19
20    public abstract double preis();
21}
22
23public class Buch extends Produkt {
24    private final double preis;
25
26    public Buch(String name, double preis) {
27        super(name, Kategorie.BUCH);
28        this.preis = preis;
29    }
30
31    @Override
32    public double preis() { return preis; }
33}
34
35public class Lebensmittel extends Produkt {
36    private final double preis;
37
38    public Lebensmittel(String name, double preis) {
39        super(name, Kategorie.LEBENSMITTEL);
40        this.preis = preis;
41    }
42
43    @Override
44    public double preis() { return preis; }
45}
46
47@FunctionalInterface
48public interface RabattRegel {
49    double berechne(Produkt p);
50}

Der folgende Code soll Produkte auswerten. Er enthält aber mehrere Fehler:

Fehlerhafter Code
 1List<Produkt> warenkorb = List.of(
 2        new Buch("Java Basics", 30.0),
 3        new Lebensmittel("Apfel", 1.0),
 4        new Buch("Java Basics", 30.0)
 5);
 6
 7RabattRegel regel = p -> {
 8    if (p.preis() > 20) {
 9        p.preis() * 0.9;
10    }
11
12    p.preis();
13};
14
15Map<Kategorie, Double> summen = new EnumMap<>();
16
17for (Produkt p : warenkorb) {
18    summen.put(
19            p.getKategorie(),
20            summen.get(p.getKategorie()) + regel.berechne(p)
21    );
22}
23
24Set<Produkt> eindeutigeProdukte = new HashSet<>(warenkorb);
25
26System.out.println(eindeutigeProdukte.size());

Aufgaben:

  1. Finde mindestens vier Probleme im Code und erkläre jeweils kurz, warum sie problematisch sind.

  2. Korrigiere die Rabattregel. Produkte über 20 Euro erhalten 10 % Rabatt, alle anderen behalten ihren normalen Preis.

  3. Korrigiere die Erstellung der EnumMap.

  4. Berechne die Summe der (rabattierten) Preise pro Kategorie korrekt. Erwartete Idee: {BUCH=54.0, LEBENSMITTEL=1.0}

  5. Erkläre, warum die beiden Bücher mit dem Namen "Java Basics" durch new HashSet<>(warenkorb) nicht automatisch als Duplikate erkannt werden. Nenne eine einfache Alternative, wenn nur eindeutige Produktnamen gesucht sind.

  6. Beantworte kurz:

    • Warum ist Produkt eine abstrakte Klasse?

    • Warum ist RabattRegel ein Interface und keine Klasse?

    • Warum passt EnumMap gut zu Kategorie?

    • Warum ist für einen Warenkorb eine List sinnvoller als ein Set?

Lösung anzeigen

Lösung 1 – Fehler finden

Problem 1: Lambda-Block ohne ``return``

Der Lambda-Körper verwendet geschweifte Klammern, aber es fehlen die return-Anweisungen. Die Zeilen p.preis() * 0.9; und p.preis(); sind nur Ausdrücke, deren Ergebnis verworfen wird.

Problem 2: ``EnumMap`` braucht die Enum-Klasse

new EnumMap<>() funktioniert nicht. Eine EnumMap braucht beim Erzeugen die Klasse des Enums: new EnumMap<>(Kategorie.class).

Problem 3: ``summen.get(…)`` kann ``null`` liefern

Wenn für eine Kategorie noch kein Wert gespeichert wurde, liefert get den Wert null. Dann entsteht sinngemäß null + 27.0, was zu einer NullPointerException führt.

Problem 4: ``HashSet<Produkt>`` erkennt fachliche Duplikate nicht automatisch

Ohne überschriebene equals- und hashCode-Methoden sind zwei new Buch("Java Basics", 30.0)-Objekte für Java verschiedene Objekte, auch wenn Name und Preis identisch sind.

Lösung 2 – Lambda korrigieren

1RabattRegel regel = p -> {
2    if (p.preis() > 20) {
3        return p.preis() * 0.9;
4    }
5
6    return p.preis();
7};

Kürzer mit dem ternären Operator:

1RabattRegel regel = p -> p.preis() > 20 ? p.preis() * 0.9 : p.preis();

Lösung 3 – ``EnumMap`` korrekt erstellen

1Map<Kategorie, Double> summen = new EnumMap<>(Kategorie.class);

Die Angabe Kategorie.class sagt der EnumMap, mit welchem Enum sie arbeitet.

Lösung 4 – Summen pro Kategorie berechnen

1Map<Kategorie, Double> summen = new EnumMap<>(Kategorie.class);
2
3for (Produkt p : warenkorb) {
4    Kategorie kategorie = p.getKategorie();
5    double bisher = summen.getOrDefault(kategorie, 0.0);
6    summen.put(kategorie, bisher + regel.berechne(p));
7}
8
9System.out.println(summen);
Mögliche Ausgabe
{BUCH=54.0, LEBENSMITTEL=1.0}

Zwei Bücher à 30 Euro mit je 10 % Rabatt ergeben 2 × 27 = 54 Euro.

Lösung 5 – Eindeutige Produkte

Ohne passende equals- und hashCode-Methoden vergleicht Java Objekte nach Objektidentität. Zwei getrennt erzeugte Buch-Objekte sind daher verschiedene Objekte.

Wenn nur eindeutige Produktnamen gesucht sind, kann man ein Set<String> verwenden:

1Set<String> produktNamen = new LinkedHashSet<>();
2
3for (Produkt p : warenkorb) {
4    produktNamen.add(p.getName());
5}
6
7System.out.println(produktNamen);
Mögliche Ausgabe
[Java Basics, Apfel]

Lösung 6 – Verständnisfragen

  • Produkt ist abstrakt, weil ein allgemeines Produkt nicht direkt instanziiert werden soll. Die konkrete Preisberechnung übernehmen die Unterklassen.

  • RabattRegel ist ein Interface, damit die Regel flexibel als Lambda übergeben werden kann.

  • EnumMap passt gut, weil Kategorie ein enum ist und die Menge der möglichen Schlüssel fest definiert ist.

  • Eine List ist für den Warenkorb sinnvoll, weil gleiche Produkte mehrfach vorkommen dürfen.

  • Ein Lambda kann nicht ohne Zieltyp verwendet werden, weil Java wissen muss, welches funktionale Interface und welche Methode implementiert wird.

Aufgabe 3: Stream-Verarbeitung von Städten

Gegeben ist folgende Liste:

1List<String> staedte = List.of(
2        "Berlin", "München", "Hamburg", "Köln", "Frankfurt",
3        "Stuttgart", "Düsseldorf", "Leipzig", "Bremen", "Hannover"
4);

Bearbeite folgende Teilaufgaben mit Java Streams.

  1. Erstelle eine Liste aller Städte, deren Name mehr als 7 Zeichen hat. Sortiere das Ergebnis alphabetisch.

  2. Erstelle eine Liste aller Städtenamen in Großbuchstaben, aber nur für Städte, die mit "H" beginnen.

  3. Zähle, wie viele Städte einen Namen mit genau 6 Zeichen haben.

  4. Erstelle eine Liste der Längen aller Städtenamen, sortiert aufsteigend. (Nicht die Namen, sondern die Längen als Integer-Liste.)

  5. Erkläre, warum Variante A schneller ist als Variante B, obwohl beide dasselbe Ergebnis liefern:

     1// Variante A – erst filtern, dann sortieren
     2staedte.stream()
     3       .filter(s -> s.length() > 7)
     4       .sorted()
     5       .collect(Collectors.toList());
     6
     7// Variante B – erst sortieren, dann filtern
     8staedte.stream()
     9       .sorted()
    10       .filter(s -> s.length() > 7)
    11       .collect(Collectors.toList());
    
Lösung anzeigen

Lösung 1 – Mehr als 7 Zeichen, alphabetisch sortiert

1List<String> ergebnis = staedte.stream()
2        .filter(s -> s.length() > 7)
3        .sorted()
4        .collect(Collectors.toList());
5
6System.out.println(ergebnis);
Ausgabe
[Düsseldorf, Frankfurt, Hannover, Stuttgart]

Lösung 2 – Großbuchstaben, nur Städte mit „H“

1List<String> ergebnis = staedte.stream()
2        .filter(s -> s.startsWith("H"))
3        .map(String::toUpperCase)
4        .collect(Collectors.toList());
5
6System.out.println(ergebnis);
Ausgabe
[HAMBURG, HANNOVER]

Lösung 3 – Anzahl der Städte mit genau 6 Zeichen

1long anzahl = staedte.stream()
2        .filter(s -> s.length() == 6)
3        .count();
4
5System.out.println(anzahl);
Ausgabe
2

Städte mit genau 6 Zeichen: "Berlin" und "Bremen".

Lösung 4 – Längen aller Namen, aufsteigend

1List<Integer> laengen = staedte.stream()
2        .map(String::length)
3        .sorted()
4        .collect(Collectors.toList());
5
6System.out.println(laengen);
Ausgabe
[4, 6, 6, 7, 7, 7, 8, 9, 9, 10]

Lösung 5 – Filter vor Sort

Beide Varianten liefern dasselbe Ergebnis. Der Unterschied liegt in der Effizienz:

  • Variante A filtert zuerst. Aus 10 Städten bleiben 4 übrig. Diese 4 werden sortiert.

  • Variante B sortiert zuerst alle 10 Städte. Danach werden 6 davon verworfen.

Sortieren ist eine vergleichsweise aufwändige Operation. Je weniger Elemente sortiert werden müssen, desto schneller geht es. Deshalb ist Variante A effizienter.

Faustregel: Filter so früh wie möglich in die Stream-Kette stellen.

Aufgabe 4: Produkt-Stream mit Fehlersuche

Verwendet wird dieselbe Klassenhierarchie wie in Aufgabe 2. Gegeben sind diese Produkte:

Ausgangsdaten
1List<Produkt> produkte = new ArrayList<>(List.of(
2        new Buch("Java Basics", 29.99),
3        new Lebensmittel("Apfel", 1.49),
4        new Buch("Clean Code", 34.99),
5        new Lebensmittel("Brot", 2.89),
6        new Buch("Design Patterns", 44.99),
7        new Lebensmittel("Milch", 1.19),
8        new Buch("Refactoring", 39.99)
9));

Aufgaben:

  1. Erstelle mit einem Stream eine Liste aller Produktnamen, deren Preis unter 5,00 Euro liegt. Sortiere die Namen alphabetisch.

  2. Erstelle mit einem Stream eine Liste aller Buchpreise (nur Bücher), sortiert aufsteigend.

  3. Folgender Code soll alle Produkte mit einem Preis ≥ 30 Euro, aufsteigend nach Preis sortiert, in eine Namensliste sammeln. Er kompiliert nicht und ist außerdem langsamer als nötig. Finde beide Probleme und erkläre sie:

    1List<String> teuer = produkte.stream()
    2        .sorted((a, b) -> a.preis() - b.preis())
    3        .filter(p -> p.preis() >= 30.0)
    4        .map(p -> p.getName())
    5        .collect(Collectors.toList());
    
  4. Erstelle eine Map, die jedem Kategorienamen (als String) die Anzahl der Produkte in dieser Kategorie zuordnet. Verwende Collectors.groupingBy und Collectors.counting.

    Erwartete Ausgabe:

    {BUCH=4, LEBENSMITTEL=3}
    
Lösung anzeigen

Lösung 1 – Günstige Produkte, alphabetisch

1List<String> ergebnis = produkte.stream()
2        .filter(p -> p.preis() < 5.0)
3        .map(Produkt::getName)
4        .sorted()
5        .collect(Collectors.toList());
6
7System.out.println(ergebnis);
Ausgabe
[Apfel, Brot, Milch]

Lösung 2 – Buchpreise aufsteigend

1List<Double> buchpreise = produkte.stream()
2        .filter(p -> p.getKategorie() == Kategorie.BUCH)
3        .map(Produkt::preis)
4        .sorted()
5        .collect(Collectors.toList());
6
7System.out.println(buchpreise);
Ausgabe
[29.99, 34.99, 39.99, 44.99]

Lösung 3 – Fehler und Performance-Problem

Problem 1 – Kompilierfehler:

Der Ausdruck a.preis() - b.preis() liefert einen double-Wert. Ein Comparator<Produkt> erwartet aber int als Rückgabe der compare-Methode. Java kann double nicht automatisch in int umwandeln — der Code kompiliert daher nicht.

Korrekt:

.sorted((a, b) -> Double.compare(a.preis(), b.preis()))

Problem 2 – Reihenfolge ineffizient:

.sorted(...) steht vor .filter(...). Das bedeutet: alle 7 Produkte werden zuerst sortiert. Danach werden 4 davon herausgefiltert.

Effizienter: erst filtern (übrig bleiben 3 teure Produkte), dann nur diese sortieren.

Korrigierte Version:

1List<String> teuer = produkte.stream()
2        .filter(p -> p.preis() >= 30.0)
3        .sorted((a, b) -> Double.compare(a.preis(), b.preis()))
4        .map(Produkt::getName)
5        .collect(Collectors.toList());
6
7System.out.println(teuer);
Ausgabe
[Clean Code, Refactoring, Design Patterns]

Lösung 4 – groupingBy mit counting

1Map<String, Long> proKategorie = produkte.stream()
2        .collect(Collectors.groupingBy(
3                p -> p.getKategorie().name(),
4                Collectors.counting()
5        ));
6
7System.out.println(proKategorie);
Ausgabe
{BUCH=4, LEBENSMITTEL=3}

Collectors.groupingBy gruppiert die Elemente nach dem angegebenen Kriterium. Collectors.counting zählt die Elemente in jeder Gruppe.