Singleton List Showdown: Collections::singletonList Vs. List::of
Learn more about Singleton and using Collections::singletonList versus List::of.
Join the DZone community and get the full member experience.
Join For FreeHow do you take a single Java object of type T
and turn it into a single-element List<T>
?
One way, of course, is to instantiate some List implementation like ArrayList
or LinkedList
and add the item, but where's the fun in that? Savvy developers like us want to do such banal things in a single line of code. The good news is that JavaSE provides multiple single-line-of-code approaches to address this problem.
You may also like: All About the Singleton
(I'm going to ignore the so-called "double brace" instantiation approach because even though you can create the single-item list and assign a reference in one statement, it uses two lines of code: one line to instantiate the anonymous List subtype and one line inside the initializer block to add the item.)
Java 8 and Earlier Approaches
Since Java 1.3, we have had the static factory method with the name that says it all: Collections::singletonList
.
List<Object> list = Collections.singletonList(item);
Developers wishing to save a few keystrokes may be tempted to use the Arrays::asList
factory method that has been around since Java 1.2...
List<Object> list = Arrays.asList(item);
...but this is not preferable. The asList
method accepts a single varargs argument, meaning the item parameter gets wrapped in an array before being used to create a list. The type of list created by this method is, not surprisingly, ArrayList
but, perhaps surprisingly, not java.util.ArrayList
; rather, it's the nested private class, java.util.Arrays$ArrayList
, that differs from its bigger brother in some notable ways:
- It does not implement
Cloneable
(OK, that's not really notable). - The list is backed by the array passed into the
asList
method. Thejava.util.ArrayList
class creates and manages its own internal array. - It does not support many operations of the
java.util.List
interface, particularly the mutation methods. (See the table below for details.)
Java 8's Stream API provides even more ways to create a single-item list, albiet in a more roundabout manner:
List<Object> list = Stream.of(item).collect(Collectors.toList());
List<Object> immutableList = Stream.of(item).collect(Collectors.toUnmodifiableList());
Regardless of the type of collector you use to generate the single-item list, this approach is not preferable either as it creates a Stream
and a Collector
in addition to the List
itself, which is the only thing we really care about.
Ultimately, for Java 8 and earlier, Collections::singletonList
is the best approach for creating a single-element list in a single line of code.
But is it still the best approach for versions of Java after 8?
Java 9 and Later Approaches
A wonderful new API addition was included in Java 9 called List::of
that accepts one or more arguments and returns a List
of those arguments. At first blush, this seems no different than Arrays::asList
, but upon closer inspection, you'll notice multiple List::of
methods accepting varying numbers of arguments, including one that is relevant to this particular discussion:
/**
* Returns an unmodifiable list containing one element.
*
* See <a href="#unmodifiable">Unmodifiable Lists</a> for details.
*
* @param <E> the {@code List}'s element type
* @param e1 the single element
* @return a {@code List} containing the specified element
* @throws NullPointerException if the element is {@code null}
*
* @since 9
*/
static <E> List<E> of(E e1) {
return new ImmutableCollections.List12<>(e1);
}
All but one of the List::of
methods accept a fixed number of named arguments, from one argument (as shown above) all the way up to 10 arguments–all for the sake of avoiding the varargs array penalty. As you might expect, the remaining List:of
method features the varargs argument for the rare occasions you need a list of 11 items or more.
How does List::of
compare to our long-standing single-item list champion, Collections::singletonList
?
Collections::singletonList Vs. List::of
We can compare these two factory methods across a number of dimensions.
- Coding Productivity — what is the ease-of-use for each method?
- Readability — how does use of each method affect the readabiltiy of code?
- List API Support — what operations are supported by the List instances returned by each method?
- Null Support — which methods permit
null
items? - Memory Usage — how much memory is consumed by an instance of a List returned by each method?
- Performance — how efficiently can you create Lists with each method?
Coding Productivity
List.of
takes fewer keystrokes to type than Collections.singletonList
. You also save keystrokes by not needing to type (or execute an IDE shortcut) to import java.util.Collections
. More often than not, you already have java.util.List
imported because you need to do something with the returned list.
Furthermore, in the event you later want to pass in more than one item, you don't need to switch factory methods if you're using List::of
; you can simply add more method arguments.
Readability
Although Collections::singletonList
makes it explicitly clear that the returned list contains only one item, List.of(item)
is also clear: "Return a list of this item." It reads quite naturally, in my opinion.
Realistically, the fact that the list has one item is less important than the fact that you're in need of a list, and List::of
puts that fact upfront, whereas Collections::singletonList
keeps us in suspense about what collection type it will return until the final four letters.
List API Support
Each factory method returns a different java.util.List
implementation, and there are subtle differences in the API methods supported. java.util.Collections$SingletonList
— the List implementation used by Collections::singletonList
—supports more operations than List::of
's implementation, java.util.ImmutableCollections$List12
.
We will also include the aforementioned java.util.Arrays$ArrayList
and the venerable java.util.ArrayList
for comparison's sake, the latter of which is the type of list returned by Collectors.toList()
. The type of list returned by Collectors.toUnmodifiableList()
is the same type that is returned by List::of
SingletonList | List::of | Arrays::asList | java.util.ArrayList | |
---|---|---|---|---|
add |
❌ | ❌ | ❌ | ✔️ |
addAll |
❌ | ❌ | ❌ | ✔️ |
clear |
❌ | ❌ | ❌ | ✔️ |
remove |
❌ | ❌ | ❌ | ✔️ |
removeAll |
❗️ | ❌ | ❗️ | ✔️ |
retainAll |
❗️ | ❌ | ❗️ | ✔️ |
replaceAll |
❌ | ❌ | ✔️ | ✔️ |
set |
❌ | ❌ | ✔️ | ✔️ |
sort |
✔️ | ❌ | ✔️ | ✔️ |
remove on iterator |
❌ | ❌ | ❌ | ✔️ |
set on list-iterator |
❌ | ❌ | ✔️ | ✔️ |
Legend:
- ✔️ means the method is supported
- ❌ means that calling this method will throw an
UnsupportedOperationException
- ❗️ means the method is supported only if the method's arguments do not cause a mutation, e.g.
Collections.singletonList("foo").retainAll("foo")
is OK butCollections.singletonList("foo").retainAll("bar")
throws anUnsupportedOperationException
The List::of
method's ImmutableCollections.List12
type is the strongest in terms of immutability; every method will throw an UnsupportedOperationException
regardless of the arguments passed in.
Collections::singletonList
allows some "mutator" methods to be called with certain arguments, but it is still immutable, ultimately.
The Arrays::asList
return type is mutable; its values can be changed (as well as the values of the array which was passed into the factory method) but it cannot be resized via additions or removals.
Interestingly, java.util.Collections$SingletonList
, which does not support the set
method on its list-iterator, does support the sort
method, whose JavaDocs explictily indicate that an UnsupportedOperationException
will be thrown "if the list's list-iterator does not support the set operation." So, it appears that java.util.Collections$SingletonList
is not fully complying with the java.util.List specification.
We could levy a similar charge against ArrayList
and LinkedList
. The JavaDocs for List::sort
also state that "If the specified comparator argument is null then all elements in this list must implement the Comparable interface." Oh, is that so? Then why does this code work just fine?
List<Object> list = new ArrayList<>();
list.add(new Object()); // java.lang.Object does not implement Comparable
list.sort(null); // does not throw a java.lang.ClassCastException
Null Support
If you're planning (for some strange reason) to intentionally create a single-element list with a null
element, you cannot use List:of
. It will NullPointerException
your face (yes, friends, "NullPointerException
" can be used as verb). The same is true for Array::asList
and the Stream-based approaches.
Collections::singletonList
will happily create a List of null
.
Memory Usage
I used the handy jcmd
tool to generate a 'GC.class_histogram' of a simple program that created 100,000 lists using Collections::singletonList
and another 100,000 using List::of
.
#instances #bytes class name (module)
--------------------------------------------------
100077 2401848 java.util.ImmutableCollections$List12 (java.base@12.0.2)
100000 2400000 java.util.Collections$SingletonList (java.base@12.0.2)
I'm not exactly sure where the 77 additional instances of java.util.ImmutableCollections$List12
originated, but when you divide the number of instances by the number of bytes, you'll see that each class instance takes exactly 24 bytes. This makes sense given that each list contains a reference to exactly one item. Every class on a 64-bit JVM consumes 12 bytes (barring Compressed OOPs) and each reference consumes 8 bytes for a total of 20 bytes. When we pad to the nearest multiple of 8, we arrive at 24 bytes.
Performance
Using JMH, I created a benchmark that examined the average time and throughput for creating lists using all of the aforementioned approaches so far:
Benchmark Mode Cnt Score Error Units
Approach.collectionsSingletonList thrpt 5 154.848 ± 16.030 ops/us
Approach.listOf thrpt 5 147.524 ± 10.477 ops/us
Approach.arraysAsList thrpt 5 90.731 ± 2.655 ops/us
Approach.streamAndCollectToList thrpt 5 4.481 ± 0.459 ops/us
Approach.streamAndCollectToUnmodifiableList thrpt 5 4.235 ± 0.081 ops/us
Approach.collectionsSingletonList avgt 5 0.006 ± 0.001 us/op
Approach.listOf avgt 5 0.007 ± 0.001 us/op
Approach.arraysAsList avgt 5 0.011 ± 0.001 us/op
Approach.streamAndCollectToList avgt 5 0.217 ± 0.004 us/op
Approach.streamAndCollectToUnmodifiableList avgt 5 0.241 ± 0.036 us/op
Based on these numbers, throughput is slightly higher and average execution time is trivially faster for Collections::singletonList
than List::of
, but they offer basically identical performance.
The next performant approach is Arrays::asList
, which is roughly twice as slow and has 60 percent the throughput. The two approaches using the Stream API are terrible, comparatively.
Why is Collections::singletonList
ever so slightly more performant than List::of
? My only guess is that the java.util.ImmutableCollections.List12
constructor calls Objects::requireNonNull
to enforce its "null not allowed" policy. As mentioned earlier, java.util.Collections$SingletonList
permits null
for better or worse, so it doesn't do any checks on the constructor's argument.
Conclusion
Both Collections::singletonList
and List:of
are great choices for creating single-element lists. If you're fortunate enough to be using a version of Java that supports both methods (9 and above), then I recommend going with List:of
for its ease of use, readability, and better-documented immutability.
Further Reading
Opinions expressed by DZone contributors are their own.
Comments