Collections-Hacks: 10 Tricks, die dich sofort zum Java-Profi machen

Julian | Oct 24, 2025 min read

Wenn du schon eine Weile Java programmierst, benutzt du List, Set und Map vermutlich täglich. Aber mal ehrlich: Die meisten Entwickler kratzen kaum an der Oberfläche dessen, was die Java Collections API wirklich kann.

Senior-Developer hingegen wissen, wie sie das Maximum herausholen – sie schreiben saubereren, schnelleren und sicheren Code. Es geht nicht darum, neue Frameworks zu lernen, sondern das Werkzeug, das du schon hast, zu meistern.

Hier sind 10 Java Collections Tricks, die dein Skill-Level anheben und dich wie einen Profi aussehen lassen.


1. Leere Defaults: Collections.emptyList() statt new ArrayList<>()

Hör auf, unnötige Objekte im Speicher zu erzeugen, nur um eine leere Liste zurückzugeben. Und niemals null zurückgeben!

Stattdessen nutzt du die unveränderliche, speichereffiziente Konstante:

// Beispiel: Konfigurations-Parameter, die optional sind
public List<String> getFeatureToggles() {
    // Die Logik, die eine Liste zurueckgibt, wenn Parameter vorhanden sind
    if (isConfigured) {
        return loadFeatureList();
    }
    
    // Spart Speicher und vermeidet NullPointerException
    return Collections.emptyList(); 
}

Das ist Intention-Revealing und spart Speicher, weil das Objekt nur einmalig existiert.


2. Einzelgänger: Collections.singletonList() statt manuellem new ArrayList<>()

Du musst ein einzelnes Item in eine Liste packen, um es einer Methode zu übergeben? Senior-Developer initialisieren dafür keine neue, unnötig große Liste.

// Beispiel: Verarbeite einen einzelnen Transaktions-Vorgang
UUID transactionId = UUID.randomUUID();

// Extrem leichtgewichtige, unveränderliche Liste mit nur einem Element
List<UUID> singleTransaction = Collections.singletonList(transactionId); 

// processBatch erwartet eine Liste, bekommt aber die SingletonList
processBatch(singleTransaction);

Die singletonList ist unveränderlich und extrem leichtgewichtig.


3. Enum-Turbo: EnumSet statt HashSet für Aufzählungen

Wenn du mit Enums arbeitest, ist EnumSet die schnellste und speichereffizienteste Implementierung, die Java zu bieten hat. Intern nutzt es Bit-Vektoren, was es um Größenordnungen schneller macht.

// Beispiel: Eine Menge von erlaubten Status fuer eine Bestellung
EnumSet<OrderStatus> finalStates = EnumSet.of(
    OrderStatus.SHIPPED, 
    OrderStatus.DELIVERED, 
    OrderStatus.CANCELLED
);

if (finalStates.contains(order.getStatus())) {
    // ... Logik, die blitzschnell ausgefuehrt wird
}

4. Key-Dance beenden: Map.computeIfAbsent()

Der klassische Tanz aus if (!map.containsKey(key)) { map.put(key, new ArrayList<>()); } ist überflüssig. Map.computeIfAbsent() erledigt das in einem sauberen Schritt.

// Beispiel: Gruppiere Rechnungen nach Kundennummer
Map<Long, List<Invoice>> invoiceMap = new HashMap<>();
Invoice newInvoice = getNextInvoice();
Long customerId = newInvoice.getCustomerId();

// Erstellt die Liste nur, wenn der Key noch nicht existiert.
invoiceMap.computeIfAbsent(customerId, k -> new ArrayList<>()).add(newInvoice);

Das ist kurz, lesbar und thread-safe in Concurrent Maps.


5. Read-Only mit Stil: List.of(), Set.of(), Map.of()

Seit Java 9 gibt es die eleganteste Art, unveränderliche Collections zu erstellen. Kein unnötiges Wrapping mehr, keine manuelle new-Anweisung.

// Beispiel: Erstelle eine konstante Liste von Datenbankspalten
// Das ist viel kuerzer und sicherer als Collections.unmodifiableList(...)
private static final List<String> REQUIRED_COLUMNS = List.of(
    "ID", 
    "NAME", 
    "CREATED_AT"
);

Sicherer und kürzer als die alten Collections.unmodifiableList()-Methoden.


6. Streams auf Ratenzahlung: Iterator.forEachRemaining()

Manchmal arbeitest du mit einem Iterator und möchtest den Rest der Elemente in einem modernen Stil verarbeiten, ohne eine manuelle while (it.hasNext())-Schleife zu schreiben.

// Beispiel: Ein Iterator, der bereits teilweise durchlaufen wurde
Iterator<String> productIterator = products.iterator();

// Produkt 1 verarbeiten (z.B. manuell in einem Prozess)
String firstProduct = productIterator.next(); 
processProduct(firstProduct); 

// Den Rest der Produkte als Stream-Aehnliche Operation verarbeiten
productIterator.forEachRemaining(p -> {
    System.out.println("Verarbeite restliches Produkt: " + p);
    processProduct(p);
});

7. Schnittmengen-Test: Collections.disjoint()

Du musst schnell prüfen, ob zwei Collections kein einziges gemeinsames Element haben? Vergiss manuelle Schleifen oder das Erstellen einer teuren Schnittmenge.

// Beispiel: Pruefe, ob der User in einer der gesperrten Gruppen ist
List<String> userGroups = getUserGroupMemberships();
Set<String> bannedGroups = Set.of("ADMIN", "BETA", "TEST");

// Prueft effizient, ob es Ueberschneidungen gibt
boolean isSafe = Collections.disjoint(userGroups, bannedGroups);

if (!isSafe) {
    throw new SecurityException("User gehoert zu gesperrten Gruppen!");
}

disjoint ist effizient (insbesondere wenn eine Collection ein Set ist) und drückt die Absicht klar aus.


8. LRU-Caching: LinkedHashMap im Access-Order-Modus

Du brauchst ein einfaches LRU-Cache-Verhalten (Least Recently Used), aber willst keine externe Bibliothek wie Guava nutzen? Die LinkedHashMap hat einen eingebauten Trick.

// Initialisiert eine LinkedHashMap, die Elemente nach ZUGRIFFS-Reihenfolge speichert
Map<String, String> simpleCache = new LinkedHashMap<>(16, 0.75f, true) {
    // Ueberschreibe removeEldestEntry fuer die LRU-Logik
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > 100; // Cache-Groesse auf 100 beschraenken
    }
};

// Jeder get-Aufruf verschiebt das Element ans Ende (Most Recently Used)
simpleCache.get("key1");

Dieser Access-Order-Modus (true im Konstruktor) gibt dir das LRU-Verhalten, das du für Basiscaching brauchst.


9. Der moderne Weg zu unveränderlichen Streams: stream.toList() (Java 16+)

Wenn du unveränderliche Ergebnisse direkt aus Streams willst, nutze nicht mehr den langen Collector-Weg.

// Das ist der neue Standard seit Java 16
List<String> names = users.stream()
    .map(User::getName)
    .toList(); // Kurz, sauber und unveränderlich!

// So schreibt man es, wenn man noch Java 10-15 unterstuetzen muss
List<String> oldNames = users.stream()
    .map(User::getName)
    .collect(Collectors.toUnmodifiableList()); 

Dies ist kürzer, lesbarer und eliminiert die Gefahr, dass jemand versehentlich das Ergebnis deines Streams manipuliert. toList() ist der moderne Weg.

Der Unterschied zwischen Stream.toList() und Collectors.toUnmodifiableList()

Merkmalstream.toList() (Seit Java 16)stream.collect(Collectors.toUnmodifiableList())
VerfügbarkeitAb Java 16Ab Java 10
ImplementierungDirekte Methode auf der Stream-Schnittstelle.Spezieller Sammler (Collector).
ImmutabilityJa, erzeugt eine unveränderliche List.Ja, erzeugt eine unveränderliche List.
Kürze/StilKürzer und sauberer (der moderne Standard).Etwas länger, aber flexibler für komplexe collect-Vorgänge.

10. Aggregation ohne Schleifen: Map.merge()

Das manuelle Aggregieren von Werten in einer Map, wie das Hochzählen von Zählern, ist der Klassiker für Map.merge().

// Beispiel: Zaehle die Anzahl der Transaktionen pro Typ
Map<TransactionType, Integer> typeCounts = new HashMap<>();

// Alte Methode: if (map.containsKey(type)) { map.put(type, map.get(type) + 1); } else { map.put(type, 1); }
TransactionType currentType = getCurrentTransactionType();

// Neue Methode: merge mit der Integer::sum Funktion
typeCounts.merge(currentType, 1, Integer::sum);

Map.merge(key, value, BiFunction) ist extrem sauber: Wenn der Schlüssel existiert, wendet es die Funktion an (hier: Addition). Wenn nicht, fügt es den Wert neu hinzu.


Abschließende Gedanken

Die Meisterschaft in Java Collections liegt nicht nur darin, ArrayList von HashSet unterscheiden zu können. Es geht darum, für jede Aufgabe das richtige, spezialisierte Werkzeug zu wählen, um weniger Code zu schreiben und versteckte Fehler zu vermeiden.

Diese 10 Tricks sind genau die Muster, die Senior-Developer täglich anwenden – und jetzt kannst du das auch!