Eine Map ist eine Datenstruktur für Zuordnungen. Du speicherst einen Wert unter einem Schlüssel und holst ihn später über genau diesen Schlüssel wieder heraus. Das klingt simpel, ist aber in der Praxis einer der wichtigsten Unterschiede zwischen sauberem Code und umständlichem Code.
Viele greifen zuerst zu einer List, weil sie bekannt und angenehm wirkt. Das ist auch völlig okay, solange du wirklich nur eine Reihe von Elementen brauchst. Sobald du aber ständig nach einem bestimmten Element suchst, Elemente eindeutig über eine ID findest oder Werte gruppieren möchtest, solltest du an eine Map denken.
Eine Map beantwortet nicht die Frage: Welche Elemente habe ich der Reihe nach? Sie beantwortet eher: Welcher Wert gehört zu diesem Schlüssel?
Genau diese Denkweise ist entscheidend.
Wann eine Map die richtige Wahl ist
Eine Map passt immer dann gut, wenn du ein Objekt über einen eindeutigen Schlüssel wiederfinden möchtest. Typische Schlüssel sind IDs, Benutzernamen, Artikelnummern, Codes, Statuswerte oder Dateinamen. Der Wert kann dann fast alles sein: ein Objekt, eine Zahl, ein Text, eine Liste oder sogar wieder eine andere Map.
Nehmen wir ein einfaches Beispiel. Du hast Benutzer und möchtest sie über ihre ID finden. Mit einer List würdest du jedes Mal durch alle Benutzer laufen und prüfen, ob die ID passt.
User findUserById(List<User> users, long id) {
for (User user : users) {
if (user.id() == id) {
return user;
}
}
return null;
}
Das funktioniert, ist aber nicht besonders elegant. Vor allem wird es langsam und unübersichtlich, wenn du diese Suche an mehreren Stellen brauchst. Mit einer Map formulierst du direkt, was du eigentlich willst: Gib mir den Benutzer zu dieser ID.
Map<Long, User> usersById = new HashMap<>();
usersById.put(user.id(), user);
User user = usersById.get(42L);
Der Code wird dadurch nicht nur kürzer. Er zeigt auch klarer deine Absicht. Die ID ist der Schlüssel, der Benutzer ist der Wert. Diese Lesbarkeit ist im Alltag sehr viel wert.
Eine gute Faustregel ist: Wenn du oft über ein Feld suchst, das eindeutig sein soll, ist eine Map meistens die bessere Wahl als eine List mit Schleife.
Das gilt auch für einfache Zähler. Du möchtest wissen, wie oft ein bestimmter Status vorkommt. Ohne Map landest du schnell bei mehreren Variablen oder verschachtelten Bedingungen. Mit einer Map bleibt es flexibel.
Map<String, Integer> countByStatus = new HashMap<>();
for (Order order : orders) {
String status = order.status();
int currentCount = countByStatus.getOrDefault(status, 0);
countByStatus.put(status, currentCount + 1);
}
Noch etwas kompakter geht es mit merge.
Map<String, Integer> countByStatus = new HashMap<>();
for (Order order : orders) {
countByStatus.merge(order.status(), 1, Integer::sum);
}
Das ist ein gutes Beispiel für eine Map, die nicht nur Daten speichert, sondern Code vereinfacht.
Welche Map du wann verwendest
In Java arbeitest du meistens gegen das Interface Map. Die konkrete Implementierung wählst du danach aus, welches Verhalten du brauchst. Für den normalen Alltag ist HashMap die Standardwahl.
Map<Long, User> usersById = new HashMap<>();
Eine HashMap ist schnell und macht keine Zusage zur Reihenfolge. Genau das willst du oft: Du brauchst schnelle Zugriffe über einen Schlüssel und die Reihenfolge ist egal. Wichtig ist nur, dass deine Schlüssel sauber funktionieren. Bei eigenen Klassen als Schlüssel müssen equals und hashCode korrekt implementiert sein. Bei String, Long, Integer oder Enum musst du dir darum normalerweise keine Gedanken machen.
Wenn die Einfügereihenfolge erhalten bleiben soll, nimmst du eine LinkedHashMap.
Map<String, String> labels = new LinkedHashMap<>();
labels.put("firstName", "Vorname");
labels.put("lastName", "Nachname");
labels.put("email", "E-Mail");
Das ist nützlich, wenn du Werte später in derselben Reihenfolge anzeigen möchtest, in der du sie eingetragen hast. Zum Beispiel bei Spalten in einer Tabelle, Exporten oder einfachen Konfigurationen.
Wenn die Schlüssel sortiert sein sollen, nimmst du eine TreeMap.
Map<LocalDate, List<Booking>> bookingsByDate = new TreeMap<>();
Eine TreeMap sortiert nach dem natürlichen Vergleich der Schlüssel oder nach einem Comparator. Das ist praktisch für Datumswerte, Namen oder Nummern, wenn du die Ausgabe direkt sortiert brauchst. Dafür ist sie im Zugriff meist nicht so schnell wie eine HashMap. Du kaufst dir also Sortierung mit etwas mehr Aufwand ein.
Wenn dein Schlüssel ein Enum ist, ist EnumMap oft die sauberste Wahl.
Map<OrderStatus, Integer> countByStatus = new EnumMap<>(OrderStatus.class);
Das ist lesbar, effizient und drückt sehr klar aus, dass nur Werte dieses Enums als Schlüssel erlaubt sind. Gerade bei Statuswerten ist das meistens besser als String, weil Tippfehler dann gar nicht erst kompilieren.
Bei parallelem Zugriff aus mehreren Threads solltest du nicht einfach eine normale HashMap verwenden. Dafür gibt es ConcurrentHashMap.
Map<String, SessionData> sessions = new ConcurrentHashMap<>();
Das heißt nicht, dass du für jede Anwendung sofort nebenläufig denken musst. Aber in Serveranwendungen, Hintergrundjobs oder gemeinsam genutzten Caches ist es wichtig, diese Unterscheidung zu kennen.
Typische Vorteile und typische Fallen
Der größte Vorteil einer Map ist der direkte Zugriff über einen Schlüssel. Du musst nicht jedes Mal durch eine komplette Liste laufen. Dadurch wird der Code oft klarer und bei größeren Datenmengen auch deutlich effizienter.
Ein zweiter Vorteil ist die natürliche Eindeutigkeit der Schlüssel. In einer Map kann ein Schlüssel nur einmal vorkommen. Wenn du unter demselben Schlüssel erneut etwas speicherst, wird der alte Wert ersetzt.
Map<String, String> settings = new HashMap<>();
settings.put("language", "de");
settings.put("language", "en");
System.out.println(settings.get("language"));
Die Ausgabe ist en, weil der zweite Eintrag den ersten überschreibt. Das kann gewollt sein, zum Beispiel bei Einstellungen. Es kann aber auch ein Fehler sein, wenn du eigentlich doppelte Werte erkennen möchtest.
Dann solltest du bewusst prüfen, ob schon ein Wert vorhanden ist.
Map<String, User> usersByEmail = new HashMap<>();
for (User user : users) {
User previous = usersByEmail.putIfAbsent(user.email(), user);
if (previous != null) {
throw new IllegalStateException("E-Mail doppelt: " + user.email());
}
}
Hier zeigt die Map eine ihrer Stärken: Du kannst sehr einfach prüfen, ob ein Schlüssel bereits existiert. Das ist deutlich sauberer als eine verschachtelte Suche in einer Liste.
Eine weitere Stärke ist Gruppierung. Stell dir vor, du hast mehrere Bestellungen und möchtest sie nach Kundennummer sammeln. Dann ist der Schlüssel die Kundennummer und der Wert eine Liste von Bestellungen.
Map<Long, List<Order>> ordersByCustomerId = new HashMap<>();
for (Order order : orders) {
ordersByCustomerId
.computeIfAbsent(order.customerId(), id -> new ArrayList<>())
.add(order);
}
Das sieht beim ersten Lesen etwas dichter aus, ist aber sehr praxisnah. computeIfAbsent bedeutet: Wenn es für diesen Schlüssel noch keinen Wert gibt, lege einen neuen Wert an. Danach wird die Bestellung in die passende Liste gelegt.
Genau hier merkt man, warum Maps so nützlich sind. Du modellierst die Daten so, wie du sie später brauchst. Wenn du danach alle Bestellungen eines Kunden suchst, musst du nicht mehr die komplette Liste filtern.
List<Order> customerOrders = ordersByCustomerId.getOrDefault(customerId, List.of());
Natürlich haben Maps auch Nachteile. Sie sind nicht automatisch leichter verständlich, nur weil sie kürzer sind. Eine schlecht benannte Map wie dataMap hilft niemandem. Gute Namen sind wichtig. usersById, ordersByCustomerId oder countByStatus erklären sofort, was Schlüssel und Wert bedeuten.
Außerdem solltest du eine Map nicht verwenden, wenn die Reihenfolge der eigentliche Kern deiner Daten ist. Eine Einkaufsliste, ein Ablaufplan oder eine sortierte Ergebnisliste ist oft besser als List modelliert. Eine Map ist auch keine gute Wahl, wenn du überwiegend alle Elemente der Reihe nach verarbeitest und kaum über Schlüssel zugreifst.
Ein weiterer Punkt ist null. get liefert null, wenn kein Wert vorhanden ist. Je nach Map kann aber auch ein gespeicherter Wert selbst null sein. Das kann zu unklarem Code führen. In neuem Code ist es oft besser, null als gespeicherten Wert zu vermeiden und stattdessen mit containsKey, getOrDefault oder klaren Rückgabewerten zu arbeiten.
String language = settings.getOrDefault("language", "de");
Das ist meist besser lesbar als eine extra Prüfung auf null.
Iteration und Best Practice
Über eine Map kannst du auf verschiedene Arten iterieren. Welche Variante sinnvoll ist, hängt davon ab, was du brauchst.
Wenn du Schlüssel und Wert brauchst, iterierst du über entrySet. Das ist in den meisten Fällen die beste und klarste Variante.
for (Map.Entry<Long, User> entry : usersById.entrySet()) {
Long id = entry.getKey();
User user = entry.getValue();
System.out.println(id + ": " + user.name());
}
Das ist besser als erst über alle Schlüssel zu laufen und dann für jeden Schlüssel wieder get aufzurufen.
for (Long id : usersById.keySet()) {
User user = usersById.get(id);
System.out.println(id + ": " + user.name());
}
Dieser Code funktioniert, ist aber unnötig umständlich, wenn du den Wert sowieso brauchst. entrySet sagt klar: Ich arbeite mit dem kompletten Eintrag.
Wenn du nur die Schlüssel brauchst, ist keySet richtig.
for (Long id : usersById.keySet()) {
System.out.println(id);
}
Wenn du nur die Werte brauchst, ist values richtig.
for (User user : usersById.values()) {
System.out.println(user.name());
}
Für kurze Aktionen kann auch forEach angenehm sein.
usersById.forEach((id, user) ->
System.out.println(id + ": " + user.name())
);
Ich verwende forEach gerne bei sehr kleinen, klaren Aktionen. Sobald mehrere Bedingungen, Rückgaben oder Fehlerbehandlungen dazukommen, ist eine normale for Schleife oft besser lesbar. Lesbarkeit schlägt Kürze.
Ein etwas größeres Beispiel zeigt den praktischen Vorteil einer Map gut. Angenommen, du bekommst Buchungen und möchtest am Ende pro Projekt die Summe der Minuten berechnen.
Map<String, Integer> minutesByProject = new HashMap<>();
for (Booking booking : bookings) {
minutesByProject.merge(
booking.projectKey(),
booking.minutes(),
Integer::sum
);
}
for (Map.Entry<String, Integer> entry : minutesByProject.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue() + " Minuten");
}
Ohne Map müsstest du für jedes Projekt immer wieder suchen, ob es schon eine Summe gibt. Mit Map formulierst du direkt: Pro Projekt gibt es eine Summe.
Noch ein typischer Anwendungsfall ist eine Zuordnung von Befehlen zu Aktionen. Das solltest du nicht übertreiben, aber für kleine, klar abgegrenzte Fälle kann es lesbarer sein als eine lange Kette aus if oder switch.
Map<String, Runnable> actions = new HashMap<>();
actions.put("start", service::start);
actions.put("stop", service::stop);
actions.put("reload", service::reload);
Runnable action = actions.get(command);
if (action == null) {
throw new IllegalArgumentException("Unbekannter Befehl: " + command);
}
action.run();
Der Vorteil ist hier nicht nur weniger Code. Die Zuordnung steht zentral an einer Stelle. Neue Befehle lassen sich ergänzen, ohne dass die eigentliche Ausführung unübersichtlich wird.
Fazit
Eine Map ist dann richtig, wenn du Werte über Schlüssel findest, zählst, gruppierst oder eindeutig zuordnest. Sie ist keine bessere Liste, sondern ein anderes Werkzeug. Eine List beschreibt Reihenfolge. Eine Map beschreibt Beziehung.
Für die meisten Fälle startest du mit HashMap. Wenn du Einfügereihenfolge brauchst, nimmst du LinkedHashMap. Wenn die Schlüssel sortiert sein sollen, nimmst du TreeMap. Bei Enum Schlüsseln ist EnumMap eine sehr gute Wahl. Bei gemeinsamem Zugriff aus mehreren Threads kommt ConcurrentHashMap ins Spiel.
Achte besonders auf gute Namen. Eine Map sollte möglichst schon im Namen erklären, welcher Wert über welchen Schlüssel gefunden wird. usersById ist fast immer besser als userMap. Gute Namen machen Maps verständlich.
Beim Iterieren gilt: Wenn du Schlüssel und Wert brauchst, nimm entrySet. Wenn du nur Schlüssel brauchst, nimm keySet. Wenn du nur Werte brauchst, nimm values. Das ist einfach, sauber und in Java der übliche Weg.
Richtig eingesetzt machen Maps deinen Code nicht magisch, sondern klarer. Und genau darum geht es: Die Datenstruktur soll zu deiner Frage passen. Wenn deine Frage lautet: Was gehört zu diesem Schlüssel?, dann ist eine Map meistens die passende Antwort.
