Java 9's other new enhancements, Part 1: Factory methods in Java Collections

Discover the new convenience factory methods that have been added to Java 9's Collections Framework

factory methods
Credit: Alden Jewell/Flickr

Barring further delays, Java 9 will reach general availability status on July 27. Its module system and Java Shell Read-Eval-Print Loop (REPL) tool are receiving considerable attention, but Java 9 also offers additional enhancements that will make this release memorable.

I've created a series of posts that explores some of these other new enhancements. This series attempts to answer at least some of your questions about these offerings. We'll begin by focusing on the new convenience factory methods that have been added to various interfaces in the Java Collections Framework.

Convenience factory methods for collections

Java Enhancement Proposal (JEP) 269: Convenience Factory Methods for Collections defines several factory methods for conveniently creating instances of unmodifiable collections and maps with small numbers of elements. This section presents these methods after explaining why they're necessary.

The need for convenience factory methods

Java is often criticized for its verbosity. For example, creating a small, unmodifiable collection (e.g., a list) involves constructing it, storing its reference in a local variable, invoking add() via the reference several times, and finally wrapping the collection to obtain an unmodifiable view. Consider the following example:

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

This verbose example cannot be reduced to a single expression, which implies that static (unchangeable) collections must be populated in static initializer blocks rather than via more convenient field expressions. However, there are alternatives that allow a single expression to be specified:

List<String> list1 = 
   Collections.unmodifiableList(new ArrayList<>(Arrays.asList("a", "b", "c")));
List<String> list2 = 
   Collections.unmodifiableList(new ArrayList<String>() {{ add("a"); add("b"); add("c"); }});
List<String> list3 = 
   Collections.unmodifiableList(Stream.of("a", "b", "c").collect(toList()));

The first line populates a java.util.List from another List, which is returned from java.util.Arrays.asList(). The example is still somewhat verbose, requires a second List object to be created (and ultimately garbage collected), and the creation of this second List might not be obvious to some.

The second line employs the instance-initializer construct in an anonymous inner class to achieve reduced verbosity. However, this technique is quite obscure and costs an extra class at each usage. It also holds hidden references to the enclosing instance and to any captured objects. Ultimately, memory leaks and/or serialization problems may occur.

The third line uses Java 8's Streams API to achieve the desired result. Although less verbose, it involves a certain amount of unnecessary object creation and computation. Also, Streams cannot be used in this way to construct a java.util.Map, unless the value can be computed from the key or the stream elements contain the key and the value.

The problems with these potential solutions led to the introduction of JEP 186: Collection Literals, which advocates adding collection literals to the Java language. A collection literal is a syntactic expression that evaluates to an array, List, Map, or other aggregate type. Consider the following concise representation of the original example:

List<String> list = #[ "a", "b", "c" ];

No new language feature is as simple or as clean as one might first imagine, which is why collection literals weren't added to Java 9. Instead, Java 9 provides factory methods that offer much of the benefit for creating small unmodifiable collection/map instances, but at a significantly reduced cost and risk when compared to changing the language.

Exploring the factory methods

JEP 269's factory methods were inspired by similar factory methods in the java.util.Collections and java.util.EnumSet classes. Collections provides factory methods for creating empty Lists, java.util.Sets, and Maps, and for creating singleton Lists, Sets, and Maps that have exactly one element or key-value pair. EnumSet provides several overloaded of(...) factory methods, which take fixed or variable numbers of arguments, for conveniently creating an EnumSet with the specified elements. Java 9 models EnumSet's of() methods to provide a consistent and general-purpose way to create Lists, Sets, and Maps that contain objects of arbitrary types.

The following factory methods have been added to the List interface:

static <E> List<E> of()
static <E> List<E> of(E e1)
static <E> List<E> of(E e1, E e2)
static <E> List<E> of(E e1, E e2, E e3)
static <E> List<E> of(E e1, E e2, E e3, E e4)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9)	
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10)
static <E> List<E> of(E... elements)

The following factory methods have been added to the Set interface:

static <E> Set<E> of()
static <E> Set<E> of(E e1)
static <E> Set<E> of(E e1, E e2)
static <E> Set<E> of(E e1, E e2, E e3)
static <E> Set<E> of(E e1, E e2, E e3, E e4)
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5)
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6)
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7)
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9)
static <E> Set<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10)
static <E> Set<E> of(E... elements)

In each method list, the first method creates an empty unmodifiable collection. The next 10 methods create unmodifiable collections with up to 10 elements. Despite their API clutter, these methods avoid the array allocation, initialization, and garbage collection overhead incurred by the final varargs method, which supports arbitrary-sized collections.

Listing 1 demonstrates List and Set factory methods.

Listing 1. Demonstrating collection factory methods

import java.util.List;
import java.util.Set;

public class ColDemo
{
   public static void main(String[] args)
   {
      List<String> fruits = List.of("apple", "orange", "banana");
      for (String fruit: fruits)
         System.out.println(fruit);
      try
      {
         fruits.add("pear");
      }
      catch (UnsupportedOperationException uoe)
      {
         System.err.println("unable to modify fruits list");
      }

      Set<String> marbles = Set.of("aggie", "alley", "steely");
      for (String marble: marbles)
         System.out.println(marble);
      try
      {
         marbles.add("swirly");
      }
      catch (UnsupportedOperationException uoe)
      {
         System.err.println("unable to modify marbles set");
      }
   }
}

Compile Listing 1 as follows:

javac ColDemo.java

Run the resulting application as follows:

java ColDemo

I observed the following output in one run:

apple
orange
banana
unable to modify fruits list
steely
alley
aggie
unable to modify marbles set

The following factory methods have been added to the Map interface:

static <K,V> Map<K,V> 
   of()
static <K,V> Map<K,V> 
   of(K k1, V v1)
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2)
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2, K k3, V v3)
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4)
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5	
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6)
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7	
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, 
      K k8, V v8)
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, 
      K k8, V v8, K k9, V v9)
static <K,V> Map<K,V> 
   of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, 
      K k8, V v8, K k9, V v9, K k10, V v10)
static <K,V> Map<K,V> 
   ofEntries(Map.Entry<? extends K,? extends V>... entries)

The first method creates an empty unmodifiable map. The next 10 methods create unmodifiable maps with up to 10 key/value entries. These methods add some API clutter, but avoid the array allocation, initialization, and garbage collection overhead that is incurred by the final varargs method, which supports arbitrary-sized maps.

While the varargs approach is analogous to the equivalent varargs methods for List and Set, it requires that each key-value pair be boxed. The following new Map method, which can be statically imported, makes it convenient to box keys and values into map entries:

Map.Entry<K,V> entry(K k, V v)

Listing 2 demonstrates Map's ofEntries() and entry() methods.

Listing 2. Demonstrating a map factory method

import java.util.Map;

import static java.util.Map.entry;

public class MapDemo
{
   public static void main(String[] args)
   {
      Map<String, String> capCities = 
         Map.ofEntries(entry("Manitoba", "Winnipeg"), 
                       entry("Alberta", "Edmonton"));
      capCities.forEach((k, v) -> 
                        System.out.printf("Key = %s, Value = %s%n", k, v));
      try
      {
         capCities.put("British Columbia", "Victoria");
      }
      catch (UnsupportedOperationException uoe)
      {
         System.err.println("unable to modify capCities map");
      }
   }
}

Compile Listing 2 as follows:

javac MapDemo.java

Run the resulting application as follows:

java MapDemo

I observed the following output in one run:

Key = Alberta, Value = Edmonton
Key = Manitoba, Value = Winnipeg
unable to modify capCities map

Note that a future version of the JDK might mitigate the expense of boxing by using value types. The entry() convenience method returns a newly-introduced concrete type that implements Map.Entry in order to facilitate potential future migration to a value type.

Architectural details

The Collections class offers wrapper methods for creating unmodifiable Lists, Sets, and Maps. These methods don't create inherently unmodifiable collections/maps. Instead, they take another collection/map and wrap it in a class that rejects modification requests, creating an unmodifiable view of the original collection/map. Possession of a reference to the underlying collection/map still allows modification. Each wrapper is an additional object, requiring another level of indirection and consuming more memory than the original collection/map. Finally, the wrapped collection/map still bears the expense of supporting mutation even when it's never intended to be modified. This is not the case for the new factory methods.

Providing factory methods for creating small, unmodifiable collections/maps satisfies a large set of use cases, and it helps keep the specification and implementations simple. Unmodifiable collections/maps avoid the need to make defensive copies, and they are more amenable to parallel processing. Also, the runtime space occupied by small collections/maps is important. A straightforward creation of an unmodifiable java.util.HashSet with two elements, using the Collections wrapper methods, would consist of six objects: the wrapper, the HashSet, which contains a java.util.HashMap, its table of buckets (an array), and one node instance per element. This is a lot of overhead compared to the amount of data stored, and access to the data unavoidably requires multiple method calls and pointer dereferences. The factory methods for small, fixed-sized collections avoid most of this overhead, using a compact field-based or array-based layout. Not needing to support mutation (and knowing the collection/map size at creation time) also contributes to space savings.

Here are a few more details:

1 2 Page 1