Convenience Factory Methods for Collections

Introduction

In Java, creating a collection (a list or a set) or a map with a few values requires a large amount of code. The Collections library does not offer any method that will help to create an instance of a collection or a map with a small number of elements. Java 9 introduced the Convenience Factory Methods for Collections. These are APIs that make it convenient to create instances of collections or maps with few elements. The collections and the maps we create using these new convenience factory methods will be unmodifiable collection instances.
Refer to this post to understand the advantages when working with immutable classes.

Creating unmodifiable collection before Java 9

Let us look at the ways to create an unmodifiable collection before the Convenience Factory Methods for Collections were introduced in Java 9.

Option #1 – Passing a mutable collection to unmodifiableXXX method

To create an immutable List having a few elements requires to do the following

  • Create a concrete List instance (an ArrayList or a LinkedList)
  • Add elements to them
  • Call Collections.unmodifiableList passing the above list to get a new list that is unmodifiable.
An example is shown below
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list = Collections.unmodifiableList(list);

This is verbose and we cannot express this in one line. If the list variable is an instance variable, we have to have this population logic in the constructor. Had it been a static variable, this would have been in a static initialiser block.

To create a Set or a map, we could do a similar thing, and call the unmodifiableSet or the unmodifiableMap method on the Collection interface to get an unmodifiable set or a map.

Option #2 – Using Arrays.asList

We can reduce the verbosity in the above example, by creating the list using Arrays.asList. To create a Set, we would do like,

Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));

We could argue we were able to do this in one line, but still it is verbose. Also, we had to create a list before creating a set.

Option #3 – The double brace initialization idiom

Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{
    add("a"); add("b"); add("c");
}});

By using the double brace initialization, we are creating an anonymous class of type HashSet and calling the add() method from an instance initializer block. This is highly discouraged to do as it can affect performance (the anonymous class has reference to the enclosing instance) and causes problems with serialization.

Option 4 – Leveraging Java 8 streams

Set<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c")
    .collect(Collectors.toSet()));
List<String> list = Collections.unmodifiableList(Stream.of("a", "b", "c")
    .collect(Collectors.toList()));

We create a Stream of elements and collect it as a Set or a List before calling the unmodifiableSet or unmodifiableList. This seems to be the best of all the options considered, but creating a map in this way may not be easy or possible.

Problems with the above Options

The biggest problem with Option 1 is that the created collection is not immutable. It is only unmodifiable. Remember that Unmodifiable != Immutable.

Calling the Collections.unmodifiableXXX method creates and returns a new collection instance (list/map/set) that uses the underlying instance we pass for reads. It throws an UnsupportedOperationException for the write operations.

If the source of the collection (the one we pass to Collections.unmodifiableXXX) can be modified in any way, then we would not have an immutable collection.

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
List<String> unmodifiableList = Collections.unmodifiableList(list);

System.out.println(unmodifiableList);//[a, b, c]

//Source collection modified
list.add("d");

System.out.println(unmodifiableList);//[a, b, c, d]

This problem is not there in Options 2 and we have to be careful to not let out (leak) the reference of the source collection (created by Arrays.asList(“a”, “b”, “c”)).

I already told why Option 3 is very bad. The common problem in Options 1, 3 and 4 is in the unnecessary steps involved. They also create unnecessary objects – first a mutable collection and then wrapping it within an unmodifiable collection. This increases the memory footprint and affects performance (when measuring at a micro or a nano second level).

 
Surely, we should be able to do better.

The Convenience factory methods for Collections

As part of JDK 9, the JDK developers have added convenience static factory methods on the Collection interfaces. These will help create a compact, highly performant small collection interfaces. The API is kept minimal to create collections with few instances.

General structure of the APIs

We have static factory methods on the Collection interfaces (List, Set, and Map) to create small immutable collections and maps. These methods are named as of(). This method is overloaded.

  1. There are eleven methods (of()) each taking zero to ten elements. 
  2. One method that takes an var-args to build an immutable collection from an arbitrary number of elements.
In Java, a var-args is used to pass an arbitrary number of elements (arguments) of a certain type to a method. Internally, it will wrap the elements in an array of that type and pass it. 
 
Since the whole point of the new convenience methods is to provide highly performant APIs to build small immutable collections, copying the elements into an array will have an impact on the performance. Thus, to support building a collection or a map with up to 10 elements, they have provided eleven overloaded of() methods (taking 0 to 10 elements). This will avoid the array allocation, initialization, and garbage collection overhead. If we need to build a collection with more than 10 elements, the var-args overload method would be used.

Let us now look into the static factory methods added in Java 9 to the Collection interfaces.

List.of()

The of() method was added to the List Interface.

List<String> emptyList = List.of();
List<String> singleElementList = List.of("a");
List<String> list = List.of("a", "b", "c");

It is now very simple and elegant to create a list using the of() method. 

  • The first creates an empty list. We would have been using `Collections.emptyList()` before this. It returns an immutable empty list.
  • The second call creates a single element list. We have been using `Collections.singletonList(“a”)` to build a single element list.
  • But there wasn’t a way to create a list (before Java 9) with more than one element in one line. The List.of() has overloads to create an efficient immutable list with up to 10 elements. 
If we pass more than 10 elements, the var-args overload of the of() method will be called (like shown below).
List<String> bigList = List.of("a", "b", "c", "d", "e", "f", "g", "h", "i", "j" ,"k");

Set.of()

This is similar to the List.of() except that it returns a Set.

Set<String> emptySet = Set.of();
Set<String> singleElementSet = Set.of("a");
Set<String> set = Set.of("a", "b", "c");
  • The first call creates an empty set (equivalent to `Collections.emptySet()`)
  • The second call to of() passes a single element to create a single element Set. We can do this before Java 9 as `Collections.singleton(“a”)`
  • The last call creates a Set with three elements
If we pass more than 10 elements, the var-args overload of the of() method will be called to create a Set.
Set<String> bigSet = Set.of("a", "b", "c", "d", "e", "f", "g", "h", "i", "j" ,"k");

Map.of()

Creating a Map is a little different. A Map has a set of entries where each entry is a key-value pair. The API must allow us to create both.

The Map.of() method has eleven overloaded versions starting with the one to create an empty map, next the one to create a map with one key-value pair in it and so on till the method that creates a Map with five key-value pairs in it. The of() method accepts the entries’ key and value one after the other (the key and value of the first entry followed by the key and the value of the second entry and so on).

Map<String, Integer> emptyMap = Map.of();
Map<String, Integer> singleEntryMap = Map.of("a", 1);
Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3); //{c=3, b=2, a=1}
  • The first method call to of() would create an empty immutable map.
  • The second call creates an immutable map with one entry in it {a=1}
  • Finally, the third call creates an immutable map with three entries. The first entry’s key is a and value is 1. The second entry’s key is b and value is 2 and so on.
map = Map.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 6, "g", 7, "h", 8, "i", 9, "j", 10);
System.out.println(map);//{c=3, d=4, e=5, f=6, g=7, h=8, i=9, j=10, a=1, b=2}

The above calls the last of the overloaded of() methods passing 10 key-value pairs. We cannot add another entry (say “k”, 11) to this as there are only 11 overloaded of() methods.

 
You might be thinking what about the var-args version of the of() method (like in Set and List). A var-args would not be possible to add as the key and value types are alternating in the of() method. The type of the arbitrary elements passed as var-args must be of the same type. To overcome the problem to create a map with more than 5 entries, we can use the ofEntries() method.

Map.ofEntries() and Map.entry()

The Map.ofEntries() accepts an var-args argument of type Map.Entry. The Map.Entry is a nested (inner) interface of the Map interface. Thus, we can create multiple entries by creating multiple instances of the Map.Entry and pass them to the ofEntries() method.

The Map interface has added a method, entry(), to create an instance of type Map.Entry. It returns an unmodifiable entry using the passed key and value. Since the entry is unmodifiable, calling setValue() on the returned Entry will throw an UnsupportedOperationException

Map<String, Integer> map = Map.ofEntries(Map.entry("a", 1),
        Map.entry("b", 2),
        Map.entry("c", 3));
System.out.println(map);//{a=1, b=2, c=3}

Beware of nulls

None of the methods we saw would permit a null. This is good as it avoids runtime exceptions and catches bugs early. Avoiding nulls also improves the runtime performance of the returned collection.You cannot pass a null to the List.of(), Set.of(), or Map.of(). The same is true for Map.entry() and Map.ofEntries().

The concrete classes of the factory methods

You might be wondering what is the concrete type of the collections and Map returned by the new convenience factory methods? Is it an ArrayList/LinkedList for List? Is it a HashSet for a Set? A HashMap/TreeMap for a Map?

No..

These new convenience methods return an object that is internal to the JDK and it is not any of the public collection implementations. No guarantees were made about the type that is returned and it can change in the future. You have to program to an interface and consider the returned object as a List/Set or Map.

Having a peek into the implementation, we can see a special class used for list, set with up to two elements and map with up to one entry. For collections with more than two elements (a map with more than one entry), it returns a different instance.

Implementation used for List.of()

Implementation used for Set.of()
Implementation used for Set.of()

This is the current implementation and can change in the future. This is just to tell that the implementations are highly optimized for creating instances of collections and maps with small numbers of elements.

Iteration order of the Set and Map

Since the returned collection instance types are not public, you should not rely on the iteration order of the Set or the Map returned by these methods. The iteration order is unspecified for sets and maps returned by the convenience factory methods.

You can see that a map with same values for the three entries we created using the of() method and using the ofEntries()method were printed in different order.

A word of caution: Be careful to replace parts of the code using the old style of creating collections with the new methods. If you had any tests or any business logic that relied on the iteration order of the elements in a collection (which you should not), it can now break.

Conclusion

We saw how difficult and verbose was it to create a collection with a small number of elements in it. We learnt that in Java 9, convenience factory methods for collections have been added (the new methods added to the ListSet and the Map interface in Java 9). Using it, we can easily create instances of collections and map. We learnt that the new methods are highly optimized to create collections and map with up to a few elements (ten for the collections and five entries for the map). Last, we saw that the implementation used for this is not any of the existing public Collection APIs. Instead, it is a private implementation that is tailored for small, fixed-size collections.

References

Some good stack overflow on the new collection factory methods in Java 9

Leave a Reply