1. Einführung
Klassische Collections: Flexibilität und Fallstricke
Wenn Sie mit new ArrayList<>() eine Collection erstellen, erhalten Sie eine Struktur, die sich frei ändern lässt: Elemente hinzufügen, löschen, austauschen. Das ist praktisch, wenn Sie Daten „on the fly“ aufbauen. Aber was, wenn Sie diese Collection an eine andere Klasse oder Methode übergeben, wo sie nicht verändert werden darf? Und wenn Sie die Collection versehentlich nach außen geben und jemand sie ändert? Genau hier beginnen die Probleme.
Beispiel für einen klassischen Fehler
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// Wir geben die Collection "nach außen" weiter
processNames(names);
// Wir erwarten, dass die Liste unverändert ist ...
System.out.println(names);
}
public static void processNames(List<String> list) {
// Und jemand hat einfach ein Element entfernt!
list.remove("Bob");
}
}
Ausgabe:
[Alice, Charlie]
Ihre Collection hat sich verändert, obwohl Sie das gar nicht geplant hatten. In großen Projekten werden solche „Überraschungen“ leicht zu äußerst unangenehmen und schwer auffindbaren Bugs.
Die Gefahr besteht darin, dass Code, der mit der Collection arbeitet, plötzlich auf unvorhersehbare Datenänderungen stößt. Außerdem besteht stets das Risiko von Informationsverlust — jemand hat versehentlich ein Element gelöscht oder überschrieben. Und wenn die Collection gleichzeitig aus mehreren Threads verändert wird, droht nicht nur eine ConcurrentModificationException, sondern auch ein noch tückischeres Problem — inkonsistente Daten.
Absicherung von Collections: alter Ansatz
Bis Java 9 musste man Wrapper-Methoden wie Collections.unmodifiableList(...) verwenden, über die wir im vorherigen Level gesprochen haben. Sie helfen, eine „eingefrorene“ Collection zurückzugeben. Dieser Ansatz ist jedoch nicht immer bequem und löst nicht alle Probleme (mehr dazu — in der nächsten Vorlesung).
2. Moderne Lösung: Factory-Methoden List.of, Set.of, Map.of
In Java 9 sind neue statische Methoden in den Collection-Interfaces erschienen: List.of, Set.of, Map.of. Sie ermöglichen es, schnell und bequem eine Collection zu erstellen, die nicht verändert werden kann. Es ist, als hätten Sie die Collection erstellt und sofort in Beton gegossen — niemand kann Elemente hinzufügen, löschen oder ändern.
Beispiel zum Erstellen unveränderlicher Collections
import java.util.*;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);
System.out.println(names);
System.out.println(numbers);
System.out.println(ages);
}
}
Ausgabe:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
Wie funktioniert das?
- List.of(...) — erstellt eine unveränderliche Liste.
- Set.of(...) — erstellt ein unveränderliches Set.
- Map.of(...) — erstellt eine unveränderliche Map (bis zu 10 Schlüssel-Wert-Paare; für mehr verwenden Sie Map.ofEntries(...)).
Achtung! Die mit diesen Methoden erstellten Collections lassen keine Änderungen zu. Jeder Versuch, ein Element hinzuzufügen, zu löschen oder zu ersetzen, führt zu einer Ausnahme.
3. Anwendungsbeispiele und „Fallstricke“
Beispiel: Versuch, die Collection zu ändern
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // Fehler zur Laufzeit!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("Element kann nicht hinzugefügt werden: " + ex.getClass().getSimpleName());
}
}
}
Ausgabe:
Element kann nicht hinzugefügt werden: UnsupportedOperationException
Beispiel: Versuch, null hinzuzufügen
import java.util.*;
public class NullFail {
public static void main(String[] args) {
try {
List<String> badList = List.of("Alice", null, "Bob");
} catch (NullPointerException ex) {
System.out.println("Null ist verboten: " + ex.getClass().getSimpleName());
}
}
}
Ausgabe:
Null ist verboten: NullPointerException
Beispiel: Duplikate in Set.of
import java.util.*;
public class DuplicatesFail {
public static void main(String[] args) {
try {
Set<String> badSet = Set.of("one", "two", "one");
} catch (IllegalArgumentException ex) {
System.out.println("Duplikate sind verboten: " + ex.getClass().getSimpleName());
}
}
}
Ausgabe:
Duplikate sind verboten: IllegalArgumentException
Beispiel: Map.of mit vielen Paaren
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of unterstützt bis zu 10 Schlüssel-Wert-Paare
Map<String, Integer> map = Map.of(
"one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
"six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
);
System.out.println(map);
// Für eine größere Anzahl verwenden Sie Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ... und so weiter
);
System.out.println(bigMap);
}
}
4. Besonderheiten und Einschränkungen unveränderlicher Collections
Nicht veränderbar.
Jeder Versuch, ein Element hinzuzufügen, zu löschen oder zu ändern, führt zu UnsupportedOperationException. Selbst Methoden, die normalerweise erlaubt sind (add, remove, set), funktionieren nicht.
Kein null erlaubt.
Wenn Sie null als Element einer Liste oder eines Sets oder als Schlüssel/Wert in einer Map hinzufügen, erhalten Sie eine NullPointerException. Das dient der Sicherheit: null-Elemente führen in Collections häufig zu Fehlern.
Konkrete Implementierung ist nicht garantiert.
Sie erfahren nicht, welche konkrete Klasse unter der Haube der über List.of usw. erstellten Collection steckt. Verwenden Sie kein instanceof ArrayList und versuchen Sie nicht, die Collection auf einen bestimmten Typ zu casten.
Elementreihenfolge.
— Bei List.of bleibt die Reihenfolge erhalten (wie bei einer normalen Liste).
— Bei Set.of ist die Reihenfolge nicht garantiert (in der Praxis kann sie mit der Argumentreihenfolge übereinstimmen, aber darauf sollte man sich nicht verlassen).
— Bei Map.of ist die Reihenfolge der Paare nicht garantiert.
Performance.
Über die Factory-Methoden erstellte Collections sind in der Regel schneller als Wrapper über veränderlichen Collections, da sie keinen Speicher für unnötige Möglichkeiten verbrauchen.
5. Wann und warum unveränderliche Collections verwenden
Konstante Datensätze
Wenn Sie eine Liste, ein Set oder eine Map haben, die sich während der Programmausführung nicht ändern soll, verwenden Sie List.of, Set.of, Map.of. Zum Beispiel:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
Jetzt kann niemand dieser Liste eine zusätzliche Rolle hinzufügen.
Collections aus Methoden zurückgeben
Wenn Sie eine Collection aus einer Methode zurückgeben und nicht möchten, dass sie von außen verändert wird:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
Der Empfänger kann Ihre Daten nicht verändern.
Übergabe zwischen Anwendungsschichten
Wenn Sie Collections zwischen verschiedenen Teilen des Programms übergeben (z. B. zwischen den Schichten Controller und Service in einer Webanwendung), verwenden Sie besser unveränderliche Collections, damit sie niemand „heimlich“ ändern kann.
Sicherheit und Threadsicherheit
Unveränderliche Collections sind per Definition beim Lesen threadsicher: Wenn sie niemand ändern kann, können sie gefahrlos aus mehreren Threads ohne Synchronisation verwendet werden.
6. Praktische Beispiele für eine allgemeine Anwendung
Nehmen wir an, unsere Lernanwendung hat eine Liste unterstützter Befehle:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
Wenn Sie Folgendes versuchen:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
Sie erhalten eine Ausnahme und können der Anwendung nicht schaden.
Oder wenn Sie z. B. eine Map mit Fehlercodes haben:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
Jeder Versuch, einen neuen Code hinzuzufügen, löst eine Ausnahme aus.
7. Vergleich der Ansätze zur Erstellung von Collections
| Erstellungsweise | Änderbar? | Null erlaubt? | Duplikate? | Threadsicherheit | Beispiel |
|---|---|---|---|---|---|
|
Ja | Ja | Ja | Nein | |
|
Nein | Nein | Ja | Ja* | |
|
Nein | Nein | Nein | Ja* | |
|
Nein | Nein | Nein | Ja* | |
|
Nein | Hängt von der ursprünglichen Collection ab | Ja | Nein | |
* — Threadsicherheit nur in Bezug auf Unveränderlichkeit: Wenn die Collection niemand verändert, kann sie gefahrlos aus mehreren Threads gelesen werden.
8. Typische Fehler beim Umgang mit List.of, Set.of, Map.of
Fehler Nr. 1: Versuch, die Collection zu ändern.
Sehr häufig ist der Versuch, ein Element zu einer über List.of, Set.of oder Map.of erstellten Collection hinzuzufügen oder daraus zu löschen. Zum Beispiel names.add("Dmitry") oder ages.remove("Bob"). Das führt zur Laufzeit immer zu einer UnsupportedOperationException.
Fehler Nr. 2: Versuch, null hinzuzufügen.
Wenn Sie versehentlich null an eine der Methoden übergeben (z. B. List.of("Alice", null)), erhalten Sie eine NullPointerException. Unveränderliche Collections in Java 9+ mögen null nicht — und das ist eigentlich gut so.
Fehler Nr. 3: Duplikate in Set.of oder Map.of.
Set.of("a", "b", "a") oder Map.of("x", 1, "x", 2) führen zu IllegalArgumentException. Ein Set und eine Map können per Definition keine Duplikate enthalten.
Fehler Nr. 4: Erwartung einer konkreten Implementierung.
Tun Sie Folgendes nicht:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // Das ist immer false!
Die interne Implementierung ist verborgen — verlassen Sie sich nicht auf Implementierungsdetails.
Fehler Nr. 5: Versuch, Änderungsmethoden zu verwenden.
Selbst Methoden wie clear(), set(index, value) (bei Listen) werden Ausnahmen auslösen. Denken Sie daran: Über die Factory-Methoden erstellte Collections sind unveränderlich.
GO TO FULL VERSION