This content originally appeared on DEV Community and was authored by Thellu
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<Person> 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 <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> 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<Integer> it = list.iterator();
while (it.hasNext()) {
Integer element = it.next();
if (element % 2 == 0) {
it.remove();
}
}
2.Use the Java 8+ removeIf()
:
List<Integer> 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 <T> Set<T> removeDuplicateBySet(List<T> data) {
if (data == null || data.isEmpty()) {
return new HashSet<>();
}
return new HashSet<>(data);
}
// Using List
public static <T> List<T> removeDuplicateByList(List<T> data) {
if (data == null || data.isEmpty()) {
return new ArrayList<>();
}
List<T> 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<String> 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 <T> List<T> arrayToList(final T[] array) {
final List<T> l = new ArrayList<>(array.length);
for (final T s : array) {
l.add(s);
}
return l;
}
2.Simplest Approach
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
3.Java 8 Streams
Integer[] myArray = {1, 2, 3};
List<Integer> myList = Arrays.stream(myArray).collect(Collectors.toList());
int[] myArray2 = {1, 2, 3};
List<Integer> myList2 = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
4.Guava
// Immutable
List<String> il = ImmutableList.of("string", "elements");
List<String> il2 = ImmutableList.copyOf(aStringArray);
// Mutable
List<String> l1 = Lists.newArrayList(anotherListOrCollection);
List<String> l2 = Lists.newArrayList(aStringArray);
5.Apache Commons Collections
List<String> list = new ArrayList<>();
CollectionUtils.addAll(list, strArray);
6.Java 9 List.of()
(returns an immutable list):
Integer[] array = {1, 2, 3};
List<Integer> 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.
This content originally appeared on DEV Community and was authored by Thellu