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

Julian | Oct 24, 2025 min read

If you’ve been programming Java for a while, you probably use List, Set, and Map every day. But let’s be honest: most developers barely scratch the surface of what the Java Collections API can really do.

Senior developers, on the other hand, know how to get the most out of it - they write cleaner, faster and more secure code. It’s not about learning new frameworks, but rather mastering the tools you already have.

Here are 10 Java Collections Tricks that will raise your skill level and make you look like a pro.


1. Empty defaults: Collections.emptyList() instead of new ArrayList<>()

Stop creating unnecessary objects in memory only to return an empty list. And never return null!

Instead, you use the immutable, memory-efficient constant:

// 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(); 
}

This is intention-revealing and saves memory because the object only exists once.


2. Loner: Collections.singletonList() instead of manual new ArrayList<>()

Do you need to put a single item in a list to pass it to a method? Senior developers do not initialize a new, unnecessarily large list for this purpose.

// 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);

The singletonList is immutable and extremely lightweight.


3. Enum-Turbo: EnumSet instead of HashSet for enums

When working with enums, EnumSet is the fastest and most memory efficient implementation Java has to offer. Internally it uses bit vectors, which makes it orders of magnitude faster.

// 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. End key dance: Map.computeIfAbsent()

The classic dance of if (!map.containsKey(key)) { map.put(key, new ArrayList<>()); } is redundant. Map.computeIfAbsent() does this in one clean step.

// 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);

This is short, readable and thread-safe in Concurrent Maps.


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

Since Java 9 there is the most elegant way to create immutable collections. No more unnecessary wrapping, no manual new statement.

// 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"
);

Safer and shorter than the old Collections.unmodifiableList() methods.


6. Streams on installment plan: Iterator.forEachRemaining()

Sometimes you are working with an iterator and want to process the rest of the elements in a modern style without writing a manual while (it.hasNext()) loop.

// 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. Intersection test: Collections.disjoint()

Need to quickly check if two collections have not a single element in common? Forget about manual loops or creating an expensive intersection.

// 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 is efficient (especially when a Collection is a Set) and clearly expresses the intent.


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

Need a simple LRU cache behavior (Least Recently Used) but don’t want to use an external library like Guava? The LinkedHashMap has a built-in 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");

This access order mode (true in constructor) gives you the LRU behavior you need for basic caching.


9. The modern way to immutable streams: stream.toList() (Java 16+)

If you want immutable results directly from streams, stop using the long Collector route.

// 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()); 

This is shorter, more readable and eliminates the risk of someone accidentally manipulating the result of your stream. toList() is the modern way.

The difference between Stream.toList() and Collectors.toUnmodifiableList()

Featurestream.toList() (Since Java 16)stream.collect(Collectors.toUnmodifiableList())
AvailabilityFrom Java 16From Java 10
ImplementationDirect method on Stream interface.Special collector (Collector).
ImmutabilityYes, creates an immutable List.Yes, creates an immutable List.
Brief/StyleShorter and cleaner (the modern standard).Slightly longer, but more flexible for complex collect operations.

10. Aggregation without loops: Map.merge()

Manually aggregating values ​​in a map, like incrementing counters, is the classic for 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) is extremely clean: if the key exists, it applies the function (here: addition). If not, it re-adds the value.


Final Thoughts

The mastery of Java Collections lies not only in being able to distinguish ArrayList from HashSet. It’s about choosing the right, specialized tool for each task to write less code and avoid hidden errors.

These 10 tricks are exactly the patterns that senior developers use every day - and now you can too!