{{announcement.body}}
{{announcement.title}}

Singleton List Showdown: Collections::singletonList Vs. List::of

DZone 's Guide to

Singleton List Showdown: Collections::singletonList Vs. List::of

Learn more about Singleton and using Collections::singletonList versus List::of.

· Java Zone ·
Free Resource

Singleton List showdown

When it comes to Singleton, it's going down!

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? 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:

  1. It does not implement Cloneable (OK, that's not really notable).
  2. The list is backed by the array passed into the asList method. The java.util.ArrayList class creates and manages its own internal array.
  3. 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.

  1. Coding Productivity — what is the ease-of-use for each method?
  2. Readability — how does use of each method affect the readabiltiy of code?
  3. List API Support — what operations are supported by the List instances returned by each method?
  4. Null Support — which methods permit null items?
  5. Memory Usage — how much memory is consumed by an instance of a List returned by each method?
  6. 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.Listimported 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 but Collections.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 "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

All About the Singleton

How to Create an ArrayList in Java

Topics:
java ,collections api ,java jdk ,jmh ,jcmd ,list ,singleton ,collections ,arraylist

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}