Java Collections: Usage Precautions, Best Practices, and Pitfalls

In this post, we'll explore several best practices and common pitfalls when working with Java collections. Topics covered include: Collection Empty Check Collection to Map Conversion Collection Traversal Collection Deduplication Collection to Array Conversion Array to Collection Conversion By the end, you should have a clearer picture of how to use Java collections more safely and effectively in your day-to-day coding. 1. Collection Empty Check To check whether all elements inside a collection are empty, use the isEmpty() method instead of size() == 0. isEmpty() provides better readability and typically has a time complexity of O(1). While size() is also O(1) for most collections, many concurrent collections (e.g., in java.util.concurrent) do not guarantee O(1) for size(). Therefore, isEmpty() is generally safer and more readable. Below is the source code for the size() and isEmpty() methods in ConcurrentHashMap. Notice how they both call sumCount(), but isEmpty() just checks if the count is (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); } final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; } public boolean isEmpty() { return sumCount()

Apr 6, 2025 - 16:59
 0
Java Collections: Usage Precautions, Best Practices, and Pitfalls

In this post, we'll explore several best practices and common pitfalls when working with Java collections. Topics covered include:

  1. Collection Empty Check
  2. Collection to Map Conversion
  3. Collection Traversal
  4. Collection Deduplication
  5. Collection to Array Conversion
  6. Array to Collection Conversion

By the end, you should have a clearer picture of how to use Java collections more safely and effectively in your day-to-day coding.

1. Collection Empty Check

To check whether all elements inside a collection are empty, use the isEmpty() method instead of size() == 0.

  • isEmpty() provides better readability and typically has a time complexity of O(1).
  • While size() is also O(1) for most collections, many concurrent collections (e.g., in java.util.concurrent) do not guarantee O(1) for size(). Therefore, isEmpty() is generally safer and more readable.

Below is the source code for the size() and isEmpty() methods in ConcurrentHashMap. Notice how they both call sumCount(), but isEmpty() just checks if the count is <= 0, whereas size() must compute the full count.

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

public boolean isEmpty() {
    return sumCount() <= 0L; // ignore transient negative values
}

2. Collection to Map Conversion

When using java.util.stream.Collectors.toMap() to convert a collection to a Map, beware of a NullPointerException if the *value* is null.

Consider this example:

class Person {
    private String name;
    private String phoneNumber;
    // getters and setters
}

List bookList = new ArrayList<>();
bookList.add(new Person("jack", "18163138123"));
bookList.add(new Person("martin", null));

// NPE occurs here!
bookList.stream()
        .collect(Collectors.toMap(Person::getName, Person::getPhoneNumber));

Why does this cause an NPE?

Inside Collectors.toMap(), the map.merge(...) method is used, which calls Objects.requireNonNull(value). If the value (in this case, the phone number) is null, it triggers a NullPointerException.

public static >
Collector toMap(Function keyMapper,
                         Function valueMapper,
                         BinaryOperator mergeFunction,
                         Supplier mapSupplier) {
    BiConsumer accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

And the merge() implementation:

default V merge(K key, V value,
    BiFunction remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value); // <-- NPE if value is null
    ...
}

Hence, if a key or value might be null, handle it before using toMap() (e.g., filter out nulls or provide a default).

3. Collection Traversal

Avoid performing element remove/add operations within an enhanced for-each loop.
Use Iterator instead, or methods designed for removal (like removeIf() in Java 8).

Under the hood, a for-each loop depends on the Iterator. However, calling remove/add directly on the collection (rather than the iterator) leads to a fail-fast ConcurrentModificationException.

Fail-fast mechanism: When multiple threads modify a fail-fast collection, a ConcurrentModificationException may be thrown to indicate concurrent modification.

Alternatives

1.Iterator approach (using iterator.remove()):

   Iterator it = list.iterator();
   while (it.hasNext()) {
       Integer element = it.next();
       if (element % 2 == 0) {
           it.remove();
       }
   }

2.Use the Java 8+ removeIf():

   List list = new ArrayList<>();
   for (int i = 1; i <= 10; ++i) {
       list.add(i);
   }
   list.removeIf(num -> num % 2 == 0);
   // result -> [1, 3, 5, 7, 9]

3.Fail-safe collections from java.util.concurrent, which typically avoid ConcurrentModificationException by working on a separate copy or with internal concurrency control.

4. Collection Deduplication

Use a Set to leverage its uniqueness property for quick deduplication.
This avoids using List.contains() repeatedly, which can be O(n) for each containment check.

Example

// Using Set
public static  Set removeDuplicateBySet(List data) {
    if (data == null || data.isEmpty()) {
        return new HashSet<>();
    }
    return new HashSet<>(data);
}

// Using List
public static  List removeDuplicateByList(List data) {
    if (data == null || data.isEmpty()) {
        return new ArrayList<>();
    }
    List result = new ArrayList<>(data.size());
    for (T current : data) {
        if (!result.contains(current)) {
            result.add(current);
        }
    }
    return result;
}
  • The HashSet-based approach uses HashMap internally, giving near O(1) time complexity for contains() when there are few collisions.
  • The ArrayList-based approach has O(n) complexity for each contains() check, resulting in O(n^2) in the worst case for deduplication.

5. Collection to Array Conversion

Use collection.toArray(new String[0]) (or the type you need) to get a correctly typed array.

String[] s = new String[]{
    "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A"
};
List list = Arrays.asList(s);
Collections.reverse(list);

// Convert back to array
s = list.toArray(new String[0]);

Why new String[0]?

  • It serves as a type template for the returned array.
  • The JVM optimizes this approach, so the actual performance cost of creating a “zero-length” array is negligible.

If you use toArray() without parameters, it returns an Object[]. Always pass in a typed array if you want a String[], Integer[], etc.

6. Array to Collection Conversion

When using Arrays.asList() to convert an array to a collection, be aware that its add/remove/clear methods will throw UnsupportedOperationException.

Why?

Arrays.asList() returns a fixed-size list backed by the original array. It’s an inner class of java.util.Arrays that inherits from AbstractList, which does not override the add/remove/clear methods—thus they throw exceptions.

javaCopyEditList myList = Arrays.asList(1, 2, 3);
myList.add(4); // UnsupportedOperationException
myList.remove(1); // UnsupportedOperationException
myList.clear(); // UnsupportedOperationException

How to properly convert arrays to ArrayList?

1.Manual Utility

   static  List arrayToList(final T[] array) {
     final List l = new ArrayList<>(array.length);
     for (final T s : array) {
       l.add(s);
     }
     return l;
   }

2.Simplest Approach

   List list = new ArrayList<>(Arrays.asList("a", "b", "c"));

3.Java 8 Streams

   Integer[] myArray = {1, 2, 3};
   List myList = Arrays.stream(myArray).collect(Collectors.toList());

   int[] myArray2 = {1, 2, 3};
   List myList2 = Arrays.stream(myArray2).boxed().collect(Collectors.toList());

4.Guava

   // Immutable
   List il = ImmutableList.of("string", "elements");
   List il2 = ImmutableList.copyOf(aStringArray);

   // Mutable
   List l1 = Lists.newArrayList(anotherListOrCollection);
   List l2 = Lists.newArrayList(aStringArray);

5.Apache Commons Collections

   List list = new ArrayList<>();
   CollectionUtils.addAll(list, strArray);

6.Java 9 List.of() (returns an immutable list):

   Integer[] array = {1, 2, 3};
   List list = List.of(array);
   // list.add(4); // UnsupportedOperationException

Reference

  1. JavaGuide: Java Collections Best Practices

Wrapping Up

Working with collections effectively is crucial for building robust, efficient Java applications. Whether you're checking if a collection is empty, converting a collection to a map, removing duplicates, or converting arrays, keep these best practices and potential pitfalls in mind.

Thanks for reading! If you found this helpful, feel free to leave a comment or share your own Java collections tips in the discussion below.