Monday, 25 September 2017

Convenience Factory Methods for Collections in Java 9

Java 9 doesn't bring as dramatic changes to the way of coding as its predecessor did but surely we will have some fancy features, one of which I would like to present to you in this article. When I started coding in Groovy about 5 years ago, I was amazed by the collections support there. Especially when it came to initialize an immutable list, I could do it in one simple line:

def band = ["Bruce", "Steve", "Adrian", "Dave", "Janic", "Nicko"].asImmutable()

This year I also started coding in scala, and here we have some fancy tricks like:
val band = "Bruce" :: "Steve" :: "Adrian" :: "Dave" :: "Janick" :: "Nicko" :: Nil

or simply

val band = List("Bruce", "Steve", "Adrian", "Dave", "Janick", "Nicko")

At the same time Java seemed to be torturing me with an add statement list:

List<String> band = new ArrayList<>();
band.add("Bruce");
band.add("Steve");
band.add("Adrian");
band.add("Janick");
band.add("Nicko");
band = Collections.unmodifiableList(band);

or so called "double-brace" initialization (which in fact isn't any special Java feature, but rather a workaround benefiting from anonymous classes and initialization blocks):

List<String> band = Collections.unmodifiableList(new ArrayList<>() {{
  add("Bruce");
  add("Steve");
  add("Adrian");
  add("Janick");
  add("Nicko");
}});

or using arrays API to convert array to an ArrayList

List<String> band = Collections
  .unmodifiableList(Arrays.asList("Bruce","Steve","Adrian", 
                                  "Dave", "Janick","Nicko"));

Lately I could also benefit from Stream API:

List<String> band = Collections
  .unmodifiableList(Stream.of("Bruce","Steve","Adrian", "Dave", "Janick","Nicko")
    .collect(toList()));


Two last options are the cutest ones, but why do I need to first create an array or a stream in order to create a list, and why can't I just use Collections API instead of Arrays API or Stream API? Well, I don't even want to recall the way we can create Sets or Maps in Java - thinking of it makes me wake up in sweat in the middle of the night.

So far, the most concise and elegant way of creating immutable collections was provided by Guava library and classes like ImmutableList, ImmutableMap, ImmutableSet, etc. Below is an example of what was desired to be available in Java out of the box:

List<String> band = ImmutableList.of("Bruce", "Steve", "Dave",
                                     "Adrian", "Janick", "Nicko");

Fortunately, authors of Java 9 implemented JEP 269: Convenience Factory Methods for Collections, which simply provides a set of static factory methods supporting creation of immutable collection instances. Saviours! Read on to get the overview of this feature.

Immutable collections


Before we dig into the topic you should know what immutable collections really are. So, they are collections that cannot be modified once they are created. What I mean by modified is that their state (references they hold, order in which those references are being kept, and the number of elements) will stay untouched. Please note that if an immutable collection holds mutable objects, their state won't be protected anyhow. Although immutable collections still implement List, Set or Map interfaces, methods modifying their contents throw UnsupportedOperationException. You will find sets of such methods in subsequent sections.

Implementation


Implementation can be devided into two main parts. Firstly, java.util package was enriched with package-private ImmutableCollections class, that contains classes providing the immutability feature. Secondly, instances of those classes are being created with the help of static factory methods in already existing interfaces, i.e. List, Set, and Map. In following sections you will find description of both sets of functionalities per each collection interface.

List


An immutable list has an abstract base class AbstractImmutableList<E> and four implementations:

  • List0<E>
  • List1<E>
  • List2<E>
  • ListN<E>

Each of these types correspond to the number of elements that is used to their creation. In java.util.List interface we have 12 static factory methods that use the above implementations to create immutable objects:

// creates empty immutable list
static <E> List<E> of()

// creates one-element immutable list
static <E> List<E> of(E e1)

// creates two-element immutable list
static <E> List<E> of(E e1, E e2)

...

// creates ten-element immutable list
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)

// creates N-element immutable list
static <E> List<E> of(E... elements)

Methods that throw UnsupportedOperationException:

boolean add(E e);
boolean addAll(Collection<? extends E> c);
boolean addAll(int index, Collection<? extends E> c);
void    clear();
boolean remove(Object o);
boolean removeAll(Collection<?> c);
boolean removeIf(Predicate<? super E> filter);
void    replaceAll(UnaryOperator<E> operator);
boolean retainAll(Collection<?> c);
void    sort(Comparator<? super E> c);

Apart from protecting the content of our list, we also get a validation, that prevents us from initiating a list with a null value. Trying to run the following piece of code, we will end up with NullPointerException:

// throws NullPointerException
List<String> band = List.of("Bruce","Steve","Adrian", "Dave", "Janick", null);

Now here is an example of how to properly create immutable list:

List<String> band = List.of("Bruce","Steve","Adrian", "Dave", "Janick","Nicko");

Set


An immutable set is implemented similarly to what we have seen with the List interface. It has an abstract base class AbstractImmutableSet<E> and four implementations:

  • Set0<E>
  • Set1<E>
  • Set2<E>
  • SetN<E>

that again correspond to the number of elements that is used to their creation. In java.util.Set interface we have 12 static factory methods:

// creates empty immutable set
static <E> Set<E> of()

// creates one-element immutable set
static <E> Set<E> of(E e1)

// creates two-element immutable set
static <E> Set<E> of(E e1, E e2)

...

// creates ten-element immutable set
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)

// creates N-element immutable set
static <E> Set<E> of(E... elements)


Methods that throw UnsupportedOperationException:

boolean add(E e)
boolean addAll(Collection<? extends E> c)
void    clear()
boolean remove(Object o)
boolean removeAll(Collection<?> c)
boolean removeIf(Predicate<? super E> filter)
boolean retainAll(Collection<?> c)


Like with immutable lists, we cannot instantiate a Set with a null value:

// throws NullPointerException
Set<String> band = Set.of("Bruce","Steve","Adrian", "Dave", "Janick", null);


You should also know, that sets differ from lists in a way, that they cannot have duplicate values. With newly provided factory methods, we won't be able to initialize an immutable Set passing more than one object of the same value - we will get IllegalArgumentException:

// throws IllegalArgumentException
Set<String> guitarists = Set.of("Adrian", "Dave", "Janick", "Janick");

Now here is an example of how to properly create immutable set:

Set<String> band = Set.of("Bruce","Steve","Adrian", "Dave", "Janick","Nicko");

Map


Before we describe immutable map technical details we should start with the concept of an entry (java.util.Map.Entry interface) - an aggregate of key - value pair. From Java 9 we have yet another Entry's package private implementation - java.util.KeyValueHolder -  an immutable container, that prevents from instantiating an entry with key or value equal to null (throwing NullPointerException if done so).

In order to create an immutable entry, we can use following static factory method from java.util.Map interface:

static <K, V> Entry<K, V> entry(K k, V v)

Immutable map has an abstract base class AbstractImmutableMap<K, V> with three implementations:

  • Map0<K, V>
  • Map1<K, V>
  • MapN<K, V>

Again we have the following set of factory methods inside java.util.Map interface:

// creates an empty map
static <K, V> Map<K, V> of()

// creates one-element map
static <K, V> Map<K, V> of(K k1, V v1)

// creates two-element map
static <K, V> Map<K, V> of(K k1, V v1, K k2, V v2)

...

// creates ten-element map
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)

// creates N-element map
static <K, V> Map<K, V> ofEntries(Entry<? extends K, ? extends V>... entries)

You can see that it differs from List or Set factory methods from previous chapters. Using one of the of methods we can create immutable maps that contain up to 10 elements. If we want to have a bigger one, we need to use ofEntries method, accepting varargs of Entry. It shouldn't be any surprise, as we can use varargs for only one argument in a method, so we have no way of passing keys and values of different types this way.

Like with lists and sets, we have some methods that throw UnsupportedOperationException:

void clear()
V compute(K key, BiFunction<? super K,? super V,? extends V> rf)
V computeIfAbsent(K key, Function<? super K,? extends V> mf)
V computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> rf)
V merge(K key, V value, BiFunction<? super V,? super V,? extends V> rf)
V put(K key, V value)
void putAll(Map<? extends K,? extends V> m)
V putIfAbsent(K key, V value)
V remove(Object key)
boolean remove(Object key, Object value)
V replace(K key, V value)
boolean replace(K key, V oldValue, V newValue)
void replaceAll(BiFunction<? super K,? super V,? extends V> f)

Regardless the way of creating immutable map (of or ofEntries method) we won't be able to instantiate it with key, value or whole entry equal to null. Below are the examples of code that will throw NullPointerException:

// throws NullPointerExcepton because of null key
Map<String, Long> age = Map.of(null, 59L, "Steve", 61L);

// throws NullPointerExcepton because of null value
Map<String, Long> age = Map.of("Bruce", null, "Steve", 61L);

// throws NullPointerExcepton because of null entry
Map<String, Long> age = Map.ofEntries(Map.entry("Bruce", 59L), null);

Similarily to immutable set, we cannot create a map with duplicate values. Trying to do so will end up with throwing IllegalArgumentException:

Map<String, Long> age = Map.of("Bruce", 59L, "Bruce", 59L);

Map<String, Long> age = Map.ofEntries(Map.entry("Bruce", 59L),
                                      Map.entry("Bruce", 59L));

And here are some examples of how can we properly create immutable maps in Java 9:

Map<String, Long> age = Map.of("Bruce", 59L, "Steve", 61L, "Dave", 60L,
                               "Adrian", 60L, "Janick", 60L, "Nicko", 65L);

Map<String, Long> age = Map.ofEntries(Map.entry("Bruce", 59L),
                                      Map.entry("Steve", 61L),
                                      Map.entry("Dave", 60L),
                                      Map.entry("Adrian", 60L),
                                      Map.entry("Janick", 60L),
                                      Map.entry("Nicko", 65L));


Conclusions


Since Java 9, creating immutable collections is very convenient. We have a set of static factory methods for each interface, that, apart from creating immutable objects, prevent from inserting nulls or duplicates (in Set and Map). We are warned about any problems at creation time, and we are secured from getting NullPointerExceptions while traversing collection elements. It is a tiny feature, but surely a useful one!


3 comments:

  1. Hi! It would be nice to mention a little bit about guava's Immutable Collections :)

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete