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}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 }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 }
Enter fullscreen mode Exit fullscreen mode
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));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));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));
Enter fullscreen mode Exit fullscreen mode
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);}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); }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); }
Enter fullscreen mode Exit fullscreen mode
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...}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 ... }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 ... }
Enter fullscreen mode Exit fullscreen mode
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();}}Iterator<Integer> it = list.iterator(); while (it.hasNext()) { Integer element = it.next(); if (element % 2 == 0) { it.remove(); } }Iterator<Integer> it = list.iterator(); while (it.hasNext()) { Integer element = it.next(); if (element % 2 == 0) { it.remove(); } }
Enter fullscreen mode Exit fullscreen mode
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]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]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]
Enter fullscreen mode Exit fullscreen mode
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 Setpublic static <T> Set<T> removeDuplicateBySet(List<T> data) {if (data == null || data.isEmpty()) {return new HashSet<>();}return new HashSet<>(data);}// Using Listpublic 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;}// 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; }// 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; }
Enter fullscreen mode Exit fullscreen mode
- 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 arrays = list.toArray(new String[0]);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]);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]);
Enter fullscreen mode Exit fullscreen mode
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); // UnsupportedOperationExceptionmyList.remove(1); // UnsupportedOperationExceptionmyList.clear(); // UnsupportedOperationExceptionjavaCopyEditList myList = Arrays.asList(1, 2, 3); myList.add(4); // UnsupportedOperationException myList.remove(1); // UnsupportedOperationException myList.clear(); // UnsupportedOperationExceptionjavaCopyEditList myList = Arrays.asList(1, 2, 3); myList.add(4); // UnsupportedOperationException myList.remove(1); // UnsupportedOperationException myList.clear(); // UnsupportedOperationException
Enter fullscreen mode Exit fullscreen mode
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;}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; }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; }
Enter fullscreen mode Exit fullscreen mode
2.Simplest Approach
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Enter fullscreen mode Exit fullscreen mode
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());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());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());
Enter fullscreen mode Exit fullscreen mode
4.Guava
// ImmutableList<String> il = ImmutableList.of("string", "elements");List<String> il2 = ImmutableList.copyOf(aStringArray);// MutableList<String> l1 = Lists.newArrayList(anotherListOrCollection);List<String> l2 = Lists.newArrayList(aStringArray);// 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);// 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);
Enter fullscreen mode Exit fullscreen mode
5.Apache Commons Collections
List<String> list = new ArrayList<>();CollectionUtils.addAll(list, strArray);List<String> list = new ArrayList<>(); CollectionUtils.addAll(list, strArray);List<String> list = new ArrayList<>(); CollectionUtils.addAll(list, strArray);
Enter fullscreen mode Exit fullscreen mode
6.Java 9 List.of()
(returns an immutable list):
Integer[] array = {1, 2, 3};List<Integer> list = List.of(array);// list.add(4); // UnsupportedOperationExceptionInteger[] array = {1, 2, 3}; List<Integer> list = List.of(array); // list.add(4); // UnsupportedOperationExceptionInteger[] array = {1, 2, 3}; List<Integer> list = List.of(array); // list.add(4); // UnsupportedOperationException
Enter fullscreen mode Exit fullscreen mode
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.
原文链接:Java Collections: Usage Precautions, Best Practices, and Pitfalls
暂无评论内容