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.
Top comments (0)