How 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? Saavy 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.
(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.)
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. This 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 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 of the 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 single item list, albiet in a more roundabout manner:
List<Object> singleItemList = Stream.of(item).collect(Collectors.toList());
List<Object> singleItemUnmodifiableList = Stream.of(item).collect(Collectors.toUnmodifiableList());
Regardless of the type of collector you use to generate your 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 all we really care about.
Ultimately, for Java 8 and earlier, Collections::singletonList
is the 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?
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 of the documentation 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 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 instantiation 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
?
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?
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 resulting list.
Furthermore, in the event you 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 arguments.
Although Collections::singletonList
makes it explicitly clear that the returned list has only one item, the method 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 which collection type it will return until the final four letters.
Each factory method returns a different List
implementation and there are subtle differences in the API methods they support.
The List implementation used by Collections::singletonList
, java.util.Collections$SingletonList
, supports more capabilities 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, which is the type of list returned by Collectors.toList()
; Collectors.toUnmodifiableList()
returns the same type as the List::of
method.
Collections::singletonList | List::of | Arrays::asList | java.util.ArrayList | |
---|---|---|---|---|
add |
❌ | ❌ | ❌ | ✔️ |
addAll |
❌ | ❌ | ❌ | ✔️ |
clear |
❌ | ❌ | ❌ | ✔️ |
remove |
❌ | ❌ | ❌ | ✔️ |
removeAll |
❗ | ❌ | ❗ | ✔️ |
retainAll |
❗ | ❌ | ❗ | ✔️ |
replaceAll |
❌ | ❌ | ✔️ | ✔️ |
set |
❌ | ❌ | ✔️ | ✔️ |
sort |
✔️ | ❌ | ✔️ | ✔️ |
remove on iterator() |
❌ | ❌ | ❌ | ✔️ |
set on listIterator() |
❌ | ❌ | ✔️ | ✔️ |
Legend:
- ✔️ means the method is supported
- ❌ means that calling this method throws an
UnsupportedOperationException
- ❗ means the method is supported only if the arguments passed in do not cause a mutation, e.g.
Collections.singletonList("foo").retainAll("foo")
is OK butCollections.singletonList("foo").retainAll("bar")
throws an UnsupportedOperationException
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 methods to be called with certain arguments, but it is still immutable. The Arrays::asList
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.
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 is thrown "if the list's list-iterator does not support the set
operation." So it appears that this class is not fully complying with the specification of java.util.List
here.
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." The term "must" does not seem to include the situation when the element is the sole member of the list, as shown in the code below:
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
If you're planning to intentionally create a single-element list containing a null
element, you cannot use List:of
. It will throw a NullPointerException
, as will Array::asList
and the Stream-based approaches.
Collections::singletonList
will happily create a List of null
, however.
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 ([email protected])
100000 2400000 java.util.Collections$SingletonList ([email protected])
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 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.
Using JMH, I created a benchmark that tested 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
According to the numbers, throughput is slightly higher and average execution time is trivally faster for Collections::singletonList
than List::of
, but they offer basically identical performance. The next best approach is Arrays::asList
, which is roughly twice as slow and has 60% 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 suspicion is that the java.util.ImmutableCollections.List12
constructor calls Objects::requireNonNull
to enforce its "null not allowed" policy. java.util.Collections$SingletonList
does not do this, which is why it permits null
arguments for better or worse.
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 exclusively using List:of
for its ease of use, readability, and better-documented immutability.