Tutorium 20.05¶
Wiederholung¶
Jede Collection beantwortet eine andere Art von Frage.
Problem |
Typische Collection |
Gedanke |
|---|---|---|
Werte in einer Reihenfolge speichern |
|
Reihenfolge ist wichtig, Duplikate sind erlaubt. |
Werte nur einmal speichern |
|
Duplikate sollen automatisch vermieden werden. |
Werte über einen Schlüssel finden |
|
Ein Schlüssel verweist auf einen Wert. |
Elemente in Eingangsreihenfolge verarbeiten |
|
First in, first out. |
Letzte Aktion zuerst rückgängig machen |
|
Last in, first out. |
Werte sortiert speichern |
|
Elemente werden automatisch sortiert gehalten. |
Diese Fragen sollten bei jeder Aufgabe gestellt werden:
Sind Duplikate erlaubt?
Muss die Reihenfolge erhalten bleiben?
Muss ich Werte über einen Schlüssel finden?
Muss ich zählen oder gruppieren?
Muss ich schnell prüfen, ob ein Wert existiert?
Muss die Ausgabe sortiert sein?
Eine
Listmerkt sich Reihenfolge. EinSetverhindert Duplikate. EineMapordnet Schlüssel Werten zu. EineQueuemodelliert Warteschlangen. EineDequemodelliert 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.
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.
1Set<String> emails = new HashSet<>();
Begründung:
Duplikate sollen verhindert werden.
Die Reihenfolge ist nicht wichtig.
HashSeteignet 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.
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.
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.
1Map<String, Integer> wordCounts = new HashMap<>();
Begründung:
Das Wort ist der Schlüssel.
Die Anzahl ist der Wert.
Mapist sehr nützlich zum Zählen und Gruppieren.
Aufgabe 2¶
Im folgenden Programm werden Benutzernamen gespeichert. Ein Benutzername darf aber nur einmal vorkommen.
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:
Erkläre, warum
Listhier nicht ideal ist.Korrigiere den Code.
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.
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.
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:
Was gibt das Programm aus?
Welche Collection wäre sinnvoll, wenn jede Seite nur einmal erscheinen soll?
Welche Information geht verloren, wenn man ein
Setverwendet?
Lösung anzeigen
Die List enthält alle Einträge inklusive Duplikate.
[/home, /login, /home, /products, /login]
Wenn man nur wissen will, welche Seiten mindestens einmal besucht wurden, ist
ein Set sinnvoll.
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.
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:
Ergänze den Code.
Erkläre, warum eine
Map<String, Integer>sinnvoll ist.Was ist der Schlüssel, was ist der Wert?
Lösung anzeigen
Eine einfache Lösung verwendet containsKey().
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().
1for (String name : names) {
2 counts.put(name, counts.getOrDefault(name, 0) + 1);
3}
Erwartete Ausgabe ungefähr:
{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
Mapeignet sich gut, wenn zu jedem Schlüssel ein Zustand gespeichert werden soll.
Aufgabe 5¶
In dieser Aufgabe sollen mehrere Studierende einem Kurs zugeordnet werden.
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:
Ergänze den Code.
Erkläre den Typ
Map<String, List<Student>>.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.
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().
1for (Student student : students) {
2 studentsByCourse
3 .computeIfAbsent(student.course, key -> new ArrayList<>())
4 .add(student);
5}
Mögliche Ausgabe:
{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.
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:
Was gibt das Programm aus?
Was passiert, wenn danach
phoneBook.put("Tom", "11111")ausgeführt wird?Dürfen zwei verschiedene Namen dieselbe Telefonnummer haben?
Lösung anzeigen
Das Programm gibt die Telefonnummer von Tom aus.
99999
Wenn anschließend ein neuer Wert für denselben Schlüssel gespeichert wird, wird der alte Wert ersetzt.
1phoneBook.put("Tom", "11111");
Danach ist "11111" die gespeicherte Telefonnummer für Tom.
Wichtig:
Schlüssel in einer
Mapsind eindeutig.Werte in einer
Mapmüssen nicht eindeutig sein.
Das ist also erlaubt:
1phoneBook.put("Anna", "12345");
2phoneBook.put("Ben", "12345");
Aufgabe 7¶
Eine Warteschlange verarbeitet Elemente in der Reihenfolge, in der sie eingefügt wurden.
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:
Was gibt das Programm aus?
Warum passt
Queuehier besser alsList?Was bedeutet FIFO?
Lösung anzeigen
Ausgabe:
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.
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:
Was gibt das Programm aus?
Warum ist
Dequehier sinnvoll?Was bedeutet LIFO?
Lösung anzeigen
Ausgabe:
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.
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:
Ist die Ausgabe
1oder2?Warum?
Wie kann man das Verhalten korrigieren?
Lösung anzeigen
Die Ausgabe ist:
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:
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:
HashSetundHashMapverlassen sich bei eigenen Objekten aufequals()undhashCode().
Aufgabe 10¶
In dieser Aufgabe sollen mehrere Collections kombiniert werden.
Ein Kurs verwaltet Studierende.
Ein Student hat:
idnameemailgrade
Das Programm soll:
alle Studierenden in Einfügereihenfolge speichern,
Studierende über ihre ID finden,
doppelte E-Mail-Adressen verhindern,
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:
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:
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.
Szenario |
Collection |
|---|---|
Eine Einkaufsliste, bei der die Reihenfolge erhalten bleiben soll. |
? |
Eine Menge erlaubter Rollen wie |
? |
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
Szenario |
Mögliche Collection |
|---|---|
Einkaufsliste mit Reihenfolge |
|
Menge erlaubter Rollen |
|
Produktnummer zu Produkt |
|
Warteschlange von Support-Tickets |
|
Undo-Historie |
|
Alphabetisch sortierte eindeutige Namen |
|