Top 15 Java Collections and Generics Best Practices

Java Collections Framework and Generics are fundamental to writing efficient, scalable, and type-safe applications. However, improper use can lead to performance issues, memory leaks, and runtime errors.

In this article, we will explore the top 15 best practices for Java Collections and Generics to help you write better code. Let’s dive in! ⚡

1️⃣ Choose the Right Collection Type 📌

Not all collections are created equal! Choosing the wrong collection can lead to poor performance and memory wastage.

Best Practices:

✔️ Use ArrayList when frequent get/set operations are needed.
✔️ Use LinkedList for frequent add/remove operations.
✔️ Use HashSet when uniqueness is required but ordering doesn’t matter.
✔️ Use TreeSet for sorted unique elements.
✔️ Use HashMap for key-value pairs with fast lookups.
✔️ Use ConcurrentHashMap for multi-threaded environments.

🔹 Example: Choosing the Right List Implementation

List<String> arrayList = new ArrayList<>(); // Fast random access, slow inserts/deletes
List<String> linkedList = new LinkedList<>(); // Fast insertions/deletions, slow random access

2️⃣ Use Generics to Ensure Type Safety ✅

Generics eliminate ClassCastException and ensure compile-time safety.

Best Practices:

✔️ Use parameterized types instead of raw types.
✔️ Avoid using Object type to store elements.
✔️ Use wildcards (? extends T, ? super T) when flexibility is needed.

🔹 Example: Using Generics Correctly

List<String> names = new ArrayList<>(); // ✅ Type-safe list
names.add("John");
// names.add(123); ❌ Compilation error

3️⃣ Prefer isEmpty() Over size() == 0 for Checking Emptiness 🔍

Calling size() might iterate over elements, making it inefficient for certain collections.

Best Practices:

✔️ Use isEmpty() for better readability & efficiency.
✔️ Avoid size() == 0 unless specifically required.

🔹 Example: Efficient Empty Check

if (names.isEmpty()) { // ✅ Preferred
    System.out.println("List is empty");
}

if (names.size() == 0) { // ❌ Avoid
    System.out.println("List is empty");
}

4️⃣ Return Empty Collections Instead of Null 🚫

Returning null leads to NullPointerExceptions (NPEs) and unnecessary null checks.

Best Practices:

✔️ Return Collections.emptyList(), Collections.emptySet(), or Collections.emptyMap().
✔️ Avoid returning null.

🔹 Example: Returning an Empty List

public List<String> getNames() {
    return Collections.emptyList(); // ✅ No NPE risk
}

5️⃣ Use Immutable Collections Where Possible 🔒

Immutable collections prevent accidental modifications and improve thread safety.

Best Practices:

✔️ Use List.of(), Set.of(), Map.of() (Java 9+).
✔️ Use Collections.unmodifiableList(), unmodifiableSet(), unmodifiableMap() (Java 8-).

🔹 Example: Creating Immutable Lists

List<String> immutableList = List.of("Apple", "Banana", "Mango");
List<String> modifiableList = new ArrayList<>();
modifiableList.add("Item1");
List<String> unmodifiableList = Collections.unmodifiableList(modifiableList);

6️⃣ Use entrySet() Instead of keySet() for Iterating Maps 🔥

Using keySet() and then calling get() on each key is inefficient.

Best Practices:

✔️ Use entrySet() to iterate over Map efficiently.
✔️ Avoid keySet() iteration if values are needed.

🔹 Example: Efficient Map Iteration

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 85);

// ✅ Efficient
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

// ❌ Inefficient
for (String key : scores.keySet()) {
    System.out.println(key + ": " + scores.get(key));
}

7️⃣ Use Concurrent Collections for Multi-Threading ⚡

For multi-threaded applications, traditional collections like ArrayList and HashMap are not safe.

Best Practices:

✔️ Use CopyOnWriteArrayList instead of ArrayList in multi-threaded environments.
✔️ Use ConcurrentHashMap instead of HashMap.
✔️ Use BlockingQueue for producer-consumer scenarios.

🔹 Example: Using ConcurrentHashMap

Map<String, String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("User1", "John");
concurrentMap.put("User2", "Doe");

8️⃣ Use computeIfAbsent() Instead of Checking Key Existence 🔄

Instead of checking if a key exists and then inserting, use computeIfAbsent().

Best Practices:

✔️ Avoid redundant containsKey() calls.
✔️ Use computeIfAbsent() for efficiency.

🔹 Example: Simplified Key Existence Check

Map<String, List<String>> map = new HashMap<>();
map.computeIfAbsent("fruits", k -> new ArrayList<>()).add("Apple");

9️⃣ Avoid Memory Leaks with WeakHashMap for Caching 🔄

HashMap holds strong references, causing memory leaks. Use WeakHashMap when garbage collection should remove entries automatically.

Best Practices:

✔️ Use WeakHashMap for caching.
✔️ Avoid memory leaks with strong references.

🔹 Example: Using WeakHashMap

Map<Object, String> cache = new WeakHashMap<>();
Object key = new Object();
cache.put(key, "Cached Value");

// If key is garbage collected, it will be removed from cache
key = null;
System.gc(); 

🔟 Use removeIf() Instead of Manual Iteration for Filtering 🗑️

Instead of iterating and removing elements manually, use removeIf().

Best Practices:

✔️ Use removeIf() for efficient filtering.
✔️ Avoid concurrent modification issues with manual iteration.

🔹 Example: Removing Elements from a List

List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
numbers.removeIf(n -> n % 2 == 0); // ✅ Removes even numbers

1️⃣1️⃣ Use Bounded Type Parameters to Improve Type Safety 🎯

Bounded type parameters allow us to restrict generic types to a specific hierarchy using extends or super.

Best Practices:

✔️ Use <T extends ClassName> to restrict generic type to a specific superclass.
✔️ Use <T super ClassName> for contravariance (useful in some cases).
✔️ Avoid raw types—always specify the type parameter.

🔹 Example: Using extends for Upper Bounds

class Box<T extends Number> { // ✅ Only allows Number and its subclasses
    private T value;
    
    public Box(T value) {
        this.value = value;
    }
    
    public double getDoubleValue() {
        return value.doubleValue(); // ✅ Safe, as T is guaranteed to be a Number
    }
}

Box<Integer> intBox = new Box<>(10);
System.out.println(intBox.getDoubleValue()); // Output: 10.0

📌 Why?

  • Prevents unintended types (e.g., String or Boolean in a numeric container).
  • Guarantees type safety at compile-time.

1️⃣2️⃣ Avoid Using Raw Types to Prevent Runtime Errors 🚫

Raw types are unsafe because they disable compile-time type checking and allow inserting elements of the wrong type.

Bad Practice (Using Raw Types)

List list = new ArrayList(); // ❌ No type safety
list.add("Java");
list.add(10); // ❌ Runtime error (ClassCastException)

Best Practice (Using Generics)

List<String> list = new ArrayList<>(); // ✅ Type-safe
list.add("Java"); // ✅ Allowed
// list.add(10); // ❌ Compile-time error, preventing bugs

📌 Why?

  • Raw types bypass generics, leading to ClassCastException at runtime.
  • Using generics ensures type safety and allows the compiler to catch mistakes early.

1️⃣3️⃣ Use Generic Methods for Reusability 🔄

Instead of using a generic class, you can create generic methods for more flexibility.

🔹 Example: A Generic Method to Find Maximum

public static <T extends Comparable<T>> T findMax(T a, T b) { // ✅ Generic method
    return a.compareTo(b) > 0 ? a : b;
}

System.out.println(findMax(10, 20)); // Output: 20
System.out.println(findMax("Apple", "Banana")); // Output: Banana

📌 Why?

  • Generic methods allow code reuse across different types.
  • Using <T extends Comparable<T>> ensures that only comparable types can be used.

1️⃣4️⃣ Use Wildcards (? extends and ? super) for Flexibility 🔄

Wildcards (?) help in writing flexible APIs that work with a range of generic types.

Best Practices:

✔️ Use ? extends T when only reading data (covariant).
✔️ Use ? super T when modifying data (contravariant).

🔹 Example: Using ? extends (Reading)

public static void printList(List<? extends Number> list) { // ✅ Accepts any Number subclass
    for (Number num : list) {
        System.out.println(num);
    }
}

List<Integer> numbers = List.of(1, 2, 3);
printList(numbers); // ✅ Works with Integer, Double, etc.

🔹 Example: Using ? super (Writing)

public static void addNumbers(List<? super Integer> list) { // ✅ Allows Integer and its superclasses
    list.add(10); 
    list.add(20);
}

📌 Why?

  • ? extends is useful when the method only needs to read from the collection.
  • ? super allows modifying collections while ensuring type safety.

1️⃣5️⃣ Prefer Class<T> for Type Tokens to Preserve Generic Types 🏷️

Due to type erasure, generic types are not retained at runtime. Using Class<T> can help retrieve type information.

🔹 Example: Creating a Type Token

public class GenericFactory<T> {
    private Class<T> type;

    public GenericFactory(Class<T> type) { // ✅ Captures type at runtime
        this.type = type;
    }

    public T createInstance() throws InstantiationException, IllegalAccessException {
        return type.newInstance(); // ✅ Creates object of type T
    }
}

GenericFactory<String> factory = new GenericFactory<>(String.class);
String instance = factory.createInstance(); // ✅ Creates a String instance

📌 Why?

  • Java erases generic types at runtime, so Class<T> allows capturing type information.
  • Useful for frameworks, serialization, and reflection-based operations.

🚀 Summary: 15 Best Practices for Java Collections & Generics

1️⃣ Choose the right collection type
2️⃣ Use Generics for type safety
3️⃣ Prefer isEmpty() over size() == 0
4️⃣ Return empty collections instead of null
5️⃣ Use Immutable Collections (List.of())
6️⃣ Prefer entrySet() over keySet() for Map iteration
7️⃣ Use Concurrent Collections for multithreading
8️⃣ Use computeIfAbsent() for efficiency
9️⃣ Use WeakHashMap for caching
🔟 Use removeIf() instead of iteration

1️⃣1️⃣ Use bounded type parameters (<T extends Class>)
1️⃣2️⃣ Avoid raw types to prevent runtime errors
1️⃣3️⃣ Use generic methods for better reusability
1️⃣4️⃣ Use wildcards (? extends & ? super) for flexible APIs
✅ 1️⃣5️⃣ Prefer Class<T> for preserving generic type information

Did you find these best practices helpful? 🤔 Let me know in the comments! 🚀🔥

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare