Immutable Collections

Published by Marco on

Updated by Marco on

Java supports immutable collections of all kinds, but not in the way you would expect. A naive implementation would declare the immutable (unmodifiable in Java parlance) interface as follows[1]:

interface UnmodifiableList<T> {
  function T get();
  function int size();
}

There is no way to modify this list—the API is simply not available. That done, we can now create the modifiable version of the list as follows:

interface List<T> extends UnmodifiableList<T> {
  function void add(T);
  function remove(T);
}

A class can now use these interfaces to carefully control access to a list as follows:

class SomeClass {
  private List<SomeOtherClass> list;

  function UnmodifiableList<SomeOtherClass> getList() {
    return list;
  }
}

That would be pretty cool, right? Unfortunately, even if you declared these interfaces yourself, the example above does not work. Java’s generics support amounts to little more than syntactic sugar, so List<SomeOtherClass> does not conform to UnmodifiableList<SomeOtherClass>. There are several solutions to this problem:

  • Create a result list of the correct type and populate it with the elements of the private list
  • Perform the typecast anyway, ignoring or suppressing the error. Since Java employs erasure to transform generics when compiled, both would compile down to Array<Object> anyway.[2]
  • Declare a method in the List interface that returns it as an unmodifiable list. This is probably the best solution, as the code for unmodifiability will be defined in one place, the implementing collection.

So that was fun, but how exactly does it work in Java, then? In addition to the limited generics, Java is further hampered by a legacy of old code. This means that they can’t (read: won’t) change existing interfaces because it might break existing code. Here’s how Java defines the two interfaces:

interface List<T> {
  function T get();
  function int size();
  function void add(T);
  function remove(T);
}

There is no second interface. All lists have methods for adding and removing elements—even immutable ones. Immutability is enforced at run-time, not compile-time. Pretty cool, huh? Not only that, but List itself doesn’t even have a method to return an unmodifiable version of itself because Sun didn’t want to add methods to existing interfaces. Instead, you use a static method on the Collections class to get an immutable version of a list.

class SomeClass {
  private List<SomeOtherClass> list;

  /**
   * Returns an unmodifiable list (treat as read-only).
   */
  function List<SomeOtherClass> getList() {
    return Collections.unmodifiableList(list);
  }
}

The type system itself has nothing to say about modifiability. Any calling client can happily add elements to and remove elements from the result without any inkling that what they are doing is wrong. The compiler certainly won’t tell them; the Javadoc offers the only clue—in effect supplementing the type systems with comments! When that code is called at run-time, Java will happily issue an UnsupportedOperationException and smile smugly to itself for a job well done.

Say it with me: backwards-compatibility is king!


[1] I know this won’t compile; it’s Java-esque pseudo code to illustrate the idea.
[2] I’m not sure exactly which container class Java employs during erasure, so I’m using Array as a placeholder here.

Using Java 1.5 and Tapestry 4.0.2