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 aNullPointerExceptionif 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/addoperations within an enhancedfor-eachloop.
UseIteratorinstead, 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
Setto 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 usesHashMapinternally, 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/clearmethods 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); // UnsupportedOperationExceptionReference
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.