Skip to content

Instantly share code, notes, and snippets.

@bdkosher
Last active September 24, 2019 14:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bdkosher/68ded61abf17332e2aa0f2174078f8ce to your computer and use it in GitHub Desktop.
Save bdkosher/68ded61abf17332e2aa0f2174078f8ce to your computer and use it in GitHub Desktop.
Single Item List Showdown

Single Item List Showdown: Collections::singletonList vs. List::of

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.)

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

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

Java 9+ 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 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?

Comparing List::of with Collections::singletonList

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.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.

Readability

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.

List API Support

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

Null Support

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.

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 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 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.

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 exclusively using List:of for its ease of use, readability, and better-documented immutability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment