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

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 ofsize() == 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., injava.util.concurrent
) do not guarantee O(1) forsize()
. 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 aMap
, beware of aNullPointerException
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 super T, ? extends K> keyMapper,
Function super T, ? extends U> 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 super V, ? super V, ? extends V> 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 enhancedfor-each
loop.
UseIterator
instead, or methods designed for removal (likeremoveIf()
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 usingList.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 usesHashMap
internally, giving near O(1) time complexity forcontains()
when there are few collisions. - The
ArrayList
-based approach has O(n) complexity for eachcontains()
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 itsadd/remove/clear
methods will throwUnsupportedOperationException
.
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
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.