Tutorium 20.05

Wiederholung

Jede Collection beantwortet eine andere Art von Frage.

Überblick über typische Collections

Problem

Typische Collection

Gedanke

Werte in einer Reihenfolge speichern

List

Reihenfolge ist wichtig, Duplikate sind erlaubt.

Werte nur einmal speichern

Set

Duplikate sollen automatisch vermieden werden.

Werte über einen Schlüssel finden

Map

Ein Schlüssel verweist auf einen Wert.

Elemente in Eingangsreihenfolge verarbeiten

Queue

First in, first out.

Letzte Aktion zuerst rückgängig machen

Deque

Last in, first out.

Werte sortiert speichern

TreeSet oder TreeMap

Elemente werden automatisch sortiert gehalten.

Diese Fragen sollten bei jeder Aufgabe gestellt werden:

  1. Sind Duplikate erlaubt?

  2. Muss die Reihenfolge erhalten bleiben?

  3. Muss ich Werte über einen Schlüssel finden?

  4. Muss ich zählen oder gruppieren?

  5. Muss ich schnell prüfen, ob ein Wert existiert?

  6. Muss die Ausgabe sortiert sein?

Eine List merkt sich Reihenfolge. Ein Set verhindert Duplikate. Eine Map ordnet Schlüssel Werten zu. Eine Queue modelliert Warteschlangen. Eine Deque modelliert Verlauf, Undo oder Stack-Verhalten.

Aufgabe 1

Entscheide für jedes Szenario, welche Collection sinnvoll wäre. Es geht zunächst nicht um Programmcode, sondern nur um die Modellierung.

Szenario A

Die Namen von Studierenden sollen in der Reihenfolge gespeichert werden, in der sie sich angemeldet haben.

Lösung anzeigen

Sinnvoll ist eine List.

Namen in Anmeldereihenfolge speichern
1List<String> students = new ArrayList<>();

Begründung:

  • Die Reihenfolge ist wichtig.

  • Duplikate sind theoretisch möglich.

  • Es wird kein Zugriff über einen Schlüssel benötigt.

Szenario B

Für einen Newsletter sollen E-Mail-Adressen gespeichert werden. Jede Adresse darf nur einmal vorkommen.

Lösung anzeigen

Sinnvoll ist ein Set.

E-Mail-Adressen eindeutig speichern
1Set<String> emails = new HashSet<>();

Begründung:

  • Duplikate sollen verhindert werden.

  • Die Reihenfolge ist nicht wichtig.

  • HashSet eignet sich gut für schnelle Existenzprüfungen.

Szenario C

Studierende sollen über ihre Matrikelnummer gefunden werden können.

Lösung anzeigen

Sinnvoll ist eine Map.

Studierende über eine ID speichern
1Map<Integer, Student> studentsById = new HashMap<>();

Begründung:

  • Die Matrikelnummer ist der Schlüssel.

  • Das Student-Objekt ist der Wert.

  • Das Problem ist ein klassisches Schlüssel-Wert-Problem.

Szenario D

Druckaufträge sollen in der Reihenfolge verarbeitet werden, in der sie angekommen sind.

Lösung anzeigen

Sinnvoll ist eine Queue.

Druckaufträge als Warteschlange speichern
1Queue<PrintJob> jobs = new ArrayDeque<>();

Begründung:

  • Der erste Auftrag soll zuerst verarbeitet werden.

  • Das entspricht dem FIFO-Prinzip: first in, first out.

Szenario E

In einem Text soll gezählt werden, wie oft jedes Wort vorkommt.

Lösung anzeigen

Sinnvoll ist eine Map.

Wörter zählen
1Map<String, Integer> wordCounts = new HashMap<>();

Begründung:

  • Das Wort ist der Schlüssel.

  • Die Anzahl ist der Wert.

  • Map ist sehr nützlich zum Zählen und Gruppieren.

Aufgabe 2

Im folgenden Programm werden Benutzernamen gespeichert. Ein Benutzername darf aber nur einmal vorkommen.

Fehlerhafte Modellierung mit einer List
 1import java.util.*;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        List<String> usernames = new ArrayList<>();
 6
 7        usernames.add("anna");
 8        usernames.add("tom");
 9        usernames.add("anna");
10
11        System.out.println(usernames);
12    }
13}

Aufgabe:

  1. Erkläre, warum List hier nicht ideal ist.

  2. Korrigiere den Code.

  3. Erkläre, was mit dem zweiten "anna" passiert.

Lösung anzeigen

List ist hier nicht ideal, weil eine List Duplikate erlaubt. Für eindeutige Benutzernamen ist ein Set passender.

Korrektur mit einem Set
 1import java.util.*;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        Set<String> usernames = new HashSet<>();
 6
 7        usernames.add("anna");
 8        usernames.add("tom");
 9        usernames.add("anna");
10
11        System.out.println(usernames);
12    }
13}

Der zweite Eintrag "anna" wird nicht erneut gespeichert. Ein Set enthält jeden Wert nur einmal.

Wichtig ist aber: Die Ausgabe eines HashSet ist nicht garantiert sortiert und auch nicht zwingend in Einfügereihenfolge.

Aufgabe 3

Im folgenden Programm werden besuchte Webseiten gespeichert.

Besuchte Seiten mit Duplikaten
 1import java.util.*;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        List<String> visitedPages = new ArrayList<>();
 6
 7        visitedPages.add("/home");
 8        visitedPages.add("/login");
 9        visitedPages.add("/home");
10        visitedPages.add("/products");
11        visitedPages.add("/login");
12
13        System.out.println(visitedPages);
14    }
15}

Aufgabe:

  1. Was gibt das Programm aus?

  2. Welche Collection wäre sinnvoll, wenn jede Seite nur einmal erscheinen soll?

  3. Welche Information geht verloren, wenn man ein Set verwendet?

Lösung anzeigen

Die List enthält alle Einträge inklusive Duplikate.

Mögliche Ausgabe der List
[/home, /login, /home, /products, /login]

Wenn man nur wissen will, welche Seiten mindestens einmal besucht wurden, ist ein Set sinnvoll.

Besuchte Seiten eindeutig speichern
 1import java.util.*;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        Set<String> visitedPages = new HashSet<>();
 6
 7        visitedPages.add("/home");
 8        visitedPages.add("/login");
 9        visitedPages.add("/home");
10        visitedPages.add("/products");
11        visitedPages.add("/login");
12
13        System.out.println(visitedPages);
14    }
15}

Ein Set beantwortet die Frage:

Welche unterschiedlichen Seiten wurden besucht?

Eine List beantwortet eher die Frage:

Was ist in welcher Reihenfolge passiert?

Beim Wechsel zu Set verliert man also die Information über mehrfaches Vorkommen und bei HashSet zusätzlich die Einfügereihenfolge.

Aufgabe 4

Zähle, wie oft jeder Name in der Liste vorkommt.

Startercode für einen Häufigkeitszähler
 1import java.util.*;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        List<String> names = List.of(
 6            "Anna", "Tom", "Anna", "Lisa", "Tom", "Anna"
 7        );
 8
 9        Map<String, Integer> counts = new HashMap<>();
10
11        // TODO: Namen zählen
12
13        System.out.println(counts);
14    }
15}

Aufgabe:

  1. Ergänze den Code.

  2. Erkläre, warum eine Map<String, Integer> sinnvoll ist.

  3. Was ist der Schlüssel, was ist der Wert?

Lösung anzeigen

Eine einfache Lösung verwendet containsKey().

Häufigkeiten mit containsKey zählen
1for (String name : names) {
2    if (counts.containsKey(name)) {
3        counts.put(name, counts.get(name) + 1);
4    } else {
5        counts.put(name, 1);
6    }
7}

Kompakter geht es mit getOrDefault().

Häufigkeiten mit getOrDefault zählen
1for (String name : names) {
2    counts.put(name, counts.getOrDefault(name, 0) + 1);
3}

Erwartete Ausgabe ungefähr:

Mögliche Ausgabe
{Tom=2, Lisa=1, Anna=3}

Die Reihenfolge der Ausgabe kann bei HashMap variieren.

Erklärung:

  • Der Schlüssel ist der Name.

  • Der Wert ist die Anzahl.

  • Eine Map eignet sich gut, wenn zu jedem Schlüssel ein Zustand gespeichert werden soll.

Aufgabe 5

In dieser Aufgabe sollen mehrere Studierende einem Kurs zugeordnet werden.

Startercode zur Gruppierung nach Kurs
 1import java.util.*;
 2
 3class Student {
 4    String name;
 5    String course;
 6
 7    Student(String name, String course) {
 8        this.name = name;
 9        this.course = course;
10    }
11
12    @Override
13    public String toString() {
14        return name;
15    }
16}
17
18public class Main {
19    public static void main(String[] args) {
20        List<Student> students = List.of(
21            new Student("Anna", "Java"),
22            new Student("Tom", "Python"),
23            new Student("Lisa", "Java"),
24            new Student("Ben", "Databases")
25        );
26
27        Map<String, List<Student>> studentsByCourse = new HashMap<>();
28
29        // TODO: Studierende nach Kurs gruppieren
30
31        System.out.println(studentsByCourse);
32    }
33}

Aufgabe:

  1. Ergänze den Code.

  2. Erkläre den Typ Map<String, List<Student>>.

  3. Warum reicht hier keine Map<String, Student>?

Lösung anzeigen

Eine verständliche Lösung prüft zuerst, ob für den Kurs bereits eine Liste existiert.

Gruppierung mit containsKey
1for (Student student : students) {
2    if (!studentsByCourse.containsKey(student.course)) {
3        studentsByCourse.put(student.course, new ArrayList<>());
4    }
5
6    studentsByCourse.get(student.course).add(student);
7}

Eine kompaktere Lösung verwendet computeIfAbsent().

Gruppierung mit computeIfAbsent
1for (Student student : students) {
2    studentsByCourse
3        .computeIfAbsent(student.course, key -> new ArrayList<>())
4        .add(student);
5}

Mögliche Ausgabe:

Mögliche Ausgabe der Gruppierung
{Java=[Anna, Lisa], Databases=[Ben], Python=[Tom]}

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

  • Der Schlüssel ist der Kursname.

  • Der Wert ist eine Liste von Studierenden in diesem Kurs.

Eine Map<String, Student> wäre nicht ausreichend, weil pro Kurs mehrere Studierende gespeichert werden können. Eine einfache Map mit nur einem Student als Wert würde alte Einträge überschreiben.

Aufgabe 6

Ein Telefonbuch ordnet Namen Telefonnummern zu.

Telefonbuch mit einer Map
 1import java.util.*;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        Map<String, String> phoneBook = new HashMap<>();
 6
 7        phoneBook.put("Anna", "12345");
 8        phoneBook.put("Tom", "99999");
 9        phoneBook.put("Lisa", "54321");
10
11        System.out.println(phoneBook.get("Tom"));
12    }
13}

Aufgabe:

  1. Was gibt das Programm aus?

  2. Was passiert, wenn danach phoneBook.put("Tom", "11111") ausgeführt wird?

  3. Dürfen zwei verschiedene Namen dieselbe Telefonnummer haben?

Lösung anzeigen

Das Programm gibt die Telefonnummer von Tom aus.

Ausgabe
99999

Wenn anschließend ein neuer Wert für denselben Schlüssel gespeichert wird, wird der alte Wert ersetzt.

Wert für denselben Schlüssel ersetzen
1phoneBook.put("Tom", "11111");

Danach ist "11111" die gespeicherte Telefonnummer für Tom.

Wichtig:

  • Schlüssel in einer Map sind eindeutig.

  • Werte in einer Map müssen nicht eindeutig sein.

Das ist also erlaubt:

Gleicher Wert bei verschiedenen Schlüsseln
1phoneBook.put("Anna", "12345");
2phoneBook.put("Ben", "12345");

Aufgabe 7

Eine Warteschlange verarbeitet Elemente in der Reihenfolge, in der sie eingefügt wurden.

Warteschlange mit Queue und ArrayDeque
 1import java.util.*;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        Queue<String> waitingLine = new ArrayDeque<>();
 6
 7        waitingLine.add("Anna");
 8        waitingLine.add("Tom");
 9        waitingLine.add("Lisa");
10
11        System.out.println(waitingLine.poll());
12        System.out.println(waitingLine.poll());
13
14        waitingLine.add("Ben");
15
16        System.out.println(waitingLine.poll());
17        System.out.println(waitingLine.poll());
18    }
19}

Aufgabe:

  1. Was gibt das Programm aus?

  2. Warum passt Queue hier besser als List?

  3. Was bedeutet FIFO?

Lösung anzeigen

Ausgabe:

Ausgabe der Warteschlange
1Anna
2Tom
3Lisa
4Ben

Queue passt gut, weil die Collection nicht nur Werte speichert, sondern ein Verhalten modelliert: Das zuerst eingefügte Element wird zuerst verarbeitet.

FIFO bedeutet:

First in, first out.

Typische Beispiele sind Druckaufträge, Warteschlangen oder Aufgaben, die in Eingangsreihenfolge verarbeitet werden sollen.

Aufgabe 8

Eine Undo-Funktion macht meistens die zuletzt ausgeführte Aktion zuerst rückgängig.

Undo-Verlauf mit Deque
 1import java.util.*;
 2
 3public class Main {
 4    public static void main(String[] args) {
 5        Deque<String> history = new ArrayDeque<>();
 6
 7        history.push("Typed A");
 8        history.push("Typed B");
 9        history.push("Deleted B");
10
11        System.out.println("Undo: " + history.pop());
12        System.out.println("Undo: " + history.pop());
13    }
14}

Aufgabe:

  1. Was gibt das Programm aus?

  2. Warum ist Deque hier sinnvoll?

  3. Was bedeutet LIFO?

Lösung anzeigen

Ausgabe:

Ausgabe der Undo-Funktion
1Undo: Deleted B
2Undo: Typed B

Deque ist sinnvoll, weil mit push() vorne eingefügt und mit pop() das zuletzt eingefügte Element wieder entfernt wird.

LIFO bedeutet:

Last in, first out.

In modernem Java verwendet man für Stack-Verhalten häufig Deque statt der alten Klasse Stack.

Aufgabe 9

Dieses Beispiel zeigt, dass HashSet bei eigenen Klassen von equals() und hashCode() abhängt.

Zwei scheinbar gleiche User in einem HashSet
 1import java.util.*;
 2
 3class User {
 4    String name;
 5
 6    User(String name) {
 7        this.name = name;
 8    }
 9}
10
11public class Main {
12    public static void main(String[] args) {
13        Set<User> users = new HashSet<>();
14
15        users.add(new User("Anna"));
16        users.add(new User("Anna"));
17
18        System.out.println(users.size());
19    }
20}

Aufgabe:

  1. Ist die Ausgabe 1 oder 2?

  2. Warum?

  3. Wie kann man das Verhalten korrigieren?

Lösung anzeigen

Die Ausgabe ist:

Ausgabe ohne equals und hashCode
2

Grund:

Java weiß nicht automatisch, dass zwei unterschiedliche User-Objekte mit demselben Namen als gleich gelten sollen. Ohne eigene equals()-Methode werden die Objekte als verschiedene Objekte behandelt.

Korrektur:

User mit equals und hashCode
 1import java.util.*;
 2
 3class User {
 4    String name;
 5
 6    User(String name) {
 7        this.name = name;
 8    }
 9
10    @Override
11    public boolean equals(Object o) {
12        if (!(o instanceof User other)) {
13            return false;
14        }
15
16        return Objects.equals(this.name, other.name);
17    }
18
19    @Override
20    public int hashCode() {
21        return Objects.hash(name);
22    }
23}

Danach wird nur ein User mit dem Namen Anna im HashSet gespeichert.

Wichtiger Merksatz:

HashSet und HashMap verlassen sich bei eigenen Objekten auf equals() und hashCode().

Aufgabe 10

In dieser Aufgabe sollen mehrere Collections kombiniert werden.

Ein Kurs verwaltet Studierende.

Ein Student hat:

  • id

  • name

  • email

  • grade

Das Programm soll:

  1. alle Studierenden in Einfügereihenfolge speichern,

  2. Studierende über ihre ID finden,

  3. doppelte E-Mail-Adressen verhindern,

  4. Studierende nach Note gruppieren.

Entscheide, welche Collections für die einzelnen Anforderungen sinnvoll sind. Implementiere anschließend eine einfache Version.

Lösung anzeigen

Sinnvolle Collections:

Sinnvolle Datenstrukturen für die Studierendenverwaltung
1List<Student> students = new ArrayList<>();
2Map<Integer, Student> studentsById = new HashMap<>();
3Set<String> usedEmails = new HashSet<>();
4Map<Integer, List<Student>> studentsByGrade = new HashMap<>();

Bedeutung:

  • List<Student> speichert alle Studierenden in Reihenfolge.

  • Map<Integer, Student> ermöglicht schnellen Zugriff über die ID.

  • Set<String> verhindert doppelte E-Mail-Adressen.

  • Map<Integer, List<Student>> gruppiert mehrere Studierende nach Note.

Beispielimplementierung:

Mini-Projekt mit mehreren Collections
 1import java.util.*;
 2
 3class Student {
 4    int id;
 5    String name;
 6    String email;
 7    int grade;
 8
 9    Student(int id, String name, String email, int grade) {
10        this.id = id;
11        this.name = name;
12        this.email = email;
13        this.grade = grade;
14    }
15
16    @Override
17    public String toString() {
18        return name + "(" + id + ")";
19    }
20}
21
22public class Main {
23    public static void main(String[] args) {
24        List<Student> students = new ArrayList<>();
25        Map<Integer, Student> studentsById = new HashMap<>();
26        Set<String> usedEmails = new HashSet<>();
27        Map<Integer, List<Student>> studentsByGrade = new HashMap<>();
28
29        addStudent(
30            new Student(1, "Anna", "anna@example.com", 1),
31            students,
32            studentsById,
33            usedEmails,
34            studentsByGrade
35        );
36
37        addStudent(
38            new Student(2, "Tom", "tom@example.com", 2),
39            students,
40            studentsById,
41            usedEmails,
42            studentsByGrade
43        );
44
45        addStudent(
46            new Student(3, "Lisa", "lisa@example.com", 1),
47            students,
48            studentsById,
49            usedEmails,
50            studentsByGrade
51        );
52
53        System.out.println(students);
54        System.out.println(studentsById.get(2));
55        System.out.println(studentsByGrade);
56    }
57
58    static void addStudent(
59        Student student,
60        List<Student> students,
61        Map<Integer, Student> studentsById,
62        Set<String> usedEmails,
63        Map<Integer, List<Student>> studentsByGrade
64    ) {
65        if (usedEmails.contains(student.email)) {
66            System.out.println("E-Mail bereits vergeben: " + student.email);
67            return;
68        }
69
70        students.add(student);
71        studentsById.put(student.id, student);
72        usedEmails.add(student.email);
73
74        studentsByGrade
75            .computeIfAbsent(student.grade, grade -> new ArrayList<>())
76            .add(student);
77    }
78}

Der wichtigste Punkt ist hier nicht die konkrete Syntax, sondern die Erkenntnis:

Dieselben Objekte können in mehreren Collections organisiert werden, weil jede Collection eine andere Zugriffsfrage beantwortet.

Zusatzaufgabe

Ordne jedem Problem eine Collection zu und begründe kurz.

Szenarien

Szenario

Collection

Eine Einkaufsliste, bei der die Reihenfolge erhalten bleiben soll.

?

Eine Menge erlaubter Rollen wie ADMIN, USER und GUEST.

?

Eine Zuordnung von Produktnummer zu Produkt.

?

Eine Warteschlange von Support-Tickets.

?

Eine Undo-Historie in einem Texteditor.

?

Eine alphabetisch sortierte Liste eindeutiger Namen.

?

Lösung anzeigen
Mögliche Lösungen

Szenario

Mögliche Collection

Einkaufsliste mit Reihenfolge

List<String>

Menge erlaubter Rollen

Set<Role> oder bei Enums EnumSet<Role>

Produktnummer zu Produkt

Map<Integer, Product>

Warteschlange von Support-Tickets

Queue<Ticket>

Undo-Historie

Deque<Action>

Alphabetisch sortierte eindeutige Namen

TreeSet<String>