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.