Migrating Legacy Code to Modern Java Collections

Illustration for Migrating Legacy Code to Modern Java Collections
By Last updated:

Many enterprise Java applications were written years ago using now-outdated collection patterns such as Vector, Hashtable, raw List, and custom utility classes for sorting and filtering. With modern Java versions (Java 8 through 21), the Java Collections Framework (JCF) has evolved significantly—offering better performance, safety, and readability.

This tutorial guides you through migrating legacy collection code to modern Java idioms using Streams, Collectors, Optional, List.of(), Map.of(), and immutable collections. Learn how to refactor legacy projects, avoid common pitfalls, and write clean, maintainable, and performant code.


Core Concepts

The Java Collections Framework is the foundation for storing and managing groups of objects in memory. From Java 8 onwards, it supports:

  • Functional transformations using Stream, Predicate, Function
  • Immutable and thread-safe variants via List.of(), Map.of(), Collectors.collectingAndThen()
  • Performance-tuned data structures with improved memory handling

Legacy Collections to Replace

Legacy Class Modern Replacement
Vector ArrayList + external sync or CopyOnWriteArrayList
Hashtable ConcurrentHashMap
Raw types (List list) List<String>, Set<Employee>, etc.
Collections.synchronizedList() CopyOnWriteArrayList or explicit Collections.synchronizedList() with lock
Custom filtering loops Java 8+ Streams and filter()

Modern Alternatives with Examples

From Vector to ArrayList

Vector<String> names = new Vector<>();
names.add("Alice");

➡️ Replace with:

List<String> names = new ArrayList<>();
names.add("Alice");

From Raw Lists to Generics

List list = new ArrayList();
list.add("hello");

➡️ Replace with:

List<String> list = new ArrayList<>();
list.add("hello");

From Manual Iteration to Streams

List<String> filtered = new ArrayList<>();
for (String s : list) {
    if (s.startsWith("A")) {
        filtered.add(s);
    }
}

➡️ Replace with:

List<String> filtered = list.stream()
    .filter(s -> s.startsWith("A"))
    .collect(Collectors.toList());

Java Syntax and Interface Behavior

  • List.of(), Set.of(), and Map.of() create truly immutable collections.
  • Collectors.toUnmodifiableList() gives you immutability from dynamic content.
  • computeIfAbsent() avoids boilerplate map initializations.

Memory and Internal Structure Improvements

  • Java 8 introduced hash bin treeification for HashMap, optimizing worst-case time from O(n) to O(log n).
  • ArrayList now expands 50% faster, minimizing resize overhead.
  • Immutable lists in Java 9+ are smaller and cache-friendly.

Performance Comparisons

Operation Legacy Style Modern Java
Filtering Manual loops stream().filter()
Safe iteration Enumeration Enhanced for-loop / stream()
Thread-safe list Vector CopyOnWriteArrayList
Map defaulting Null checks computeIfAbsent()

Functional Programming Transformations

Example: Count occurrences

Map<String, Long> counts = list.stream()
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

Example: Flat mapping nested collections

List<String> allTags = articles.stream()
    .flatMap(a -> a.getTags().stream())
    .collect(Collectors.toList());

Anti-Patterns and How to Fix Them

  • ❌ Using synchronized collections (Vector, Hashtable) without knowing their limitations

  • ✅ Use concurrent or immutable alternatives

  • ❌ Modifying collections while iterating

  • ✅ Use Iterator.remove() or removeIf()

  • ❌ Null checks everywhere

  • ✅ Embrace Optional, Map.computeIfAbsent(), and immutable patterns


Refactoring Real-World Code

Before (Legacy)

Hashtable<Integer, String> table = new Hashtable<>();
table.put(1, "one");
table.put(2, "two");

After (Modern)

Map<Integer, String> map = Map.of(1, "one", 2, "two");

Best Practices

  • Prefer immutable collections unless mutation is required
  • Use Collectors.toMap() with merge functions
  • Avoid exposing modifiable collections from APIs
  • Replace nested loops with flatMap() when possible
  • Use computeIfAbsent() to simplify initialization

📌 What's New in Java Versions?

  • Java 8
    • Streams API, Collectors, Optional, lambdas
  • Java 9
    • List.of(), Map.of(), immutable collections
  • Java 10
    • var type inference
  • Java 21
    • Collection memory layout tuning, faster HashMap internals

Conclusion and Key Takeaways

Migrating legacy Java collection code doesn’t just modernize syntax—it improves performance, readability, safety, and developer productivity. Adopting Streams, generics, and immutable structures future-proofs your code and aligns it with modern Java standards.


FAQ

1. What’s the difference between List.of() and Collections.unmodifiableList()?

List.of() creates truly immutable lists, whereas unmodifiableList() is a view over a mutable list.

2. Why should I replace Vector?

Vector is outdated and synchronized; prefer ArrayList with external sync or CopyOnWriteArrayList.

3. Is HashMap thread-safe?

No. Use ConcurrentHashMap in multi-threaded environments.

4. Can I modify a list during iteration?

Only safely via Iterator.remove() or removeIf().

5. When to use Optional in Collections?

To handle potential nulls when fetching from a map or repository.

6. How does computeIfAbsent() help?

Avoids manual null checks and default initialization.

7. Are immutable collections faster?

Yes, especially for read-heavy operations and small datasets.

8. What if I need thread safety and immutability?

Use Collections.unmodifiableList(Collections.synchronizedList(list)), or use CopyOnWriteArrayList.

9. Do streams improve performance?

They improve readability and parallelism, not always raw speed.

10. Should I migrate legacy code incrementally?

Yes. Refactor one module at a time with tests to ensure safety.