The Joys of Guava: Immutable Collections

The Guava library, for me, is hands down the best Java utility library out there. It is a shining example of clean code and API design, both in its implementation and documentation. If you’re not already using it in your Java projects – you absolutely should be! Check out the wiki here to get started.

In this series I want to highlight different pieces of Guava that I have found particularly useful. Often there’s a Guava utility that fits your need perfectly, so becoming familiar with the different features of the library can be extremely helpful!

While I probably end up using Preconditions the most, I really think the immutable collections are the best that Guava has to offer. They have uses in so many places and can give your application a huge boost both in performance and API elegance. The implementations are also incredibly sophisticated if you ever want to take a look under the hood.

Main Idea

An immutable collection is exactly what you think it is – a Collection whose contents will never change. This means that elements can never be added or removed after the collection has been created. Because nothing can change, immutable collections are always thread-safe!

Beyond thread-safety, immutable collections have a few other important properties to note:

  • No null elements allowed (hooray!).
  • Iteration order is always well-defined (aka deterministic), depending on how the collection was created.
  • The types cannot be subclassed outside of the package, guaranteeing the integrity of any implementation your application may come across at runtime.

Sounds pretty nice, huh? The best thing about it is that Guava offers an immutable implementation for each standard collection type of the Java Collections Framework, as well as for Guava’s specialized Collection variations. You can check out the full list here.

From a performance standpoint, not having to support mutation allows the immutable collections to make considerable time and space savings. No need for concurrent modification checks, extra memory allocation in arrays/hash tables – immutable collection implementations can be streamlined to fit the exact size and needs of your data structure.

Creation

Creating and initializing an immutable collection is even easier than with a standard collection. Each immutable type will typically provide four static operations you can use to obtain instances of that type.

of

The first and simplest is a static factory method named of. This method accepts an explicit list of elements or entries with which to initialize the collection.

ImmutableList<Integer> numbers = ImmutableList.of(1, 2, 3);
ImmutableMap<String, Integer> myMap = ImmutableMap.of("a", 1, "b", 2);

Gotta love how clean having creation and initialization on one line is!

This method is the easiest way to create an empty collection with its no-args overload:

ImmutableSet<String> emptySet = ImmutableSet.of();

There’s also a wonderful Spinal Tap Easter egg in the ImmutableList implementation that’s too good not to point out.

copyOf

To transform an existing collection into an immutable type, the copyOf static factory method does the trick.

List<String> strings = new ArrayList<>();
strings.add("hello");
strings.add("world");

ImmutableList<String> immutableStrings = ImmutableList.copyOf(strings);

Builder

For more fine-tuned control of the initialization of the immutable collection, each type has a static nested Builder class.

ImmutableList.Builder<String> stringBuilder = new ImmutableList.Builder<>();
stringBuilder.add("hello");
stringBuilder.add("world");
ImmutableList<String> strings = stringBuilder.build();

As you’d expect with the builder pattern, all immutable collection builders have a fluent API that allows you to chain these calls together. Note that there’s a static factory for the builder in addition to the public constructor.

ImmutableList<String> strings = ImmutableList.<String>builder()
        .add("hello")
        .add("world")
        .build();

It’s good to know that the performance of using the associated Builder class can be assumed to be no worse, and possibly better, than creating a mutable collection and copying it.

Collectors

In more recent releases of the JRE flavor of Guava, immutable collection implementations were outfitted with Collectors that define reduction strategies for your stream pipelines. As you start using immutable collections more in your code, you’ll find this facility extremely handy.

Let’s consider extracting a top-ten list from a frequency table (EJ 46):

Map<String, Long> freq;
...

ImmutableList<String> topTen = freq.keySet().stream()
        .sorted(comparing(freq::get).reversed())
        .limit(10)
        .collect(toImmutableList());

Notice that I’ve static imported the collector method toImmutableList to make the stream pipeline more readable.

Alternatives

There are several more traditional approaches to creating immutable collections in Java, and chances are you may already be using them. Let’s consider each one and compare it to Guava’s offering.

Collections.unmodifiableXXX

The JDK comes loaded with its own methods for generating unmodifiable views of each of the major collection types. Unfortunately, a "view" will only get you so far – the returned collections are only truly immutable if nobody holds a reference to the original collection. You also don’t get the efficiency boost like you do with Guava, as the data structures still have all the overhead of mutable collections. These methods are also a bit verbose, making it unpleasant to use them everywhere you want to make defensive copies.

Convenience Factory Methods for Collections

Java 9 added some much-needed static factory methods for basic collection types to the Java language. These methods return collections that behave very similarly to Guava’s implementations – they are truly immutable, null-hostile, and offer deterministic iteration based on the order of the provided arguments. Stacked up next to Guava, they are even a bit more concise:

Set<Integer> mySet = Set.of(1, 2, 3);
ImmutableSet<Integer> mySet = ImmutableSet.of(1, 2, 3);

So why not just use these instead of Guava’s types?

It may seem subtle, but the difference in return type (Set vs. ImmutableSet) has a dramatic effect on your API. Expressing the immutability guarantee directly in the type that client code references is a powerful advantage on Guava’s side. We’ll dive into this more a bit later.

Uses

Immutable collections have many, many uses. Anywhere in your code that you use a collection whose contents will not change is an easy candidate for migration.

I’ll present a few ideas here for common use cases, but this list is far from exhaustive.

Constant Fields

Unlike your standard collection types, immutable collections can be used as a constant field. Recall that a constant field is a static final field whose value is immutable. Using the static factories makes it easy to define and initialize a constant field:

private static final ImmutableSet<String> RESERVED_CODES = ImmutableSet.of("AZ", "CQ", "ZX");

Instance Fields

Immutable collections also form great building blocks for classes, especially immutable ones.

Imagine you had a user with a name and list of permissions. Assuming you didn’t want anything to change during your user’s lifecycle, writing an immutable value class to represent your user becomes trivial using ImmutableList:

@Immutable
public final class User
{
    private final String name;

    private final ImmutableList<Permission> permissions;

    public User(String name, Iterable<Permission> permissions)
    {
        this.name = checkNotNull(name);
        this.permissions = ImmutableList.copyOf(permissions);
    }

    public String getName()
    {
        return name;
    }

    public ImmutableList<Permission> getPermissions()
    {
        return permissions;
    }

    @Override
    public String toString()
    {
        return name;
    }

    @Override
    public boolean equals(Object o)
    {
        if (o == this)
        {
            return true;
        }
        if (!(o instanceof User))
        {
            return false;
        }
        User other = (User) o;
        return other.name.equals(name)
                && other.permissions.equals(permissions);
    }

    @Override
    public int hashCode()
    {
        int result = name.hashCode();
        result = 31 * result + permissions.hashCode();
        return result;
    }
}

Defensive Copying

As you know from reading Effective Java item 50, making immutable copies of objects is a good defensive programming technique. Any time that you need to do this, the copyOf method has your back – just like in the constructor of the User class above. And before you get too worried about the performance penalty associated with defensive copying, remember that copyOf is smarter than you think.

API Considerations

And finally, the best part for last. You know how great Guava’s immutable collections are and how useful they can be. But let’s talk about them from an API perspective.

Interface vs. Implementation

Traditional API design guidelines would tell you to refer to objects by their interfaces (EJ 64), and their most general interfaces at that. For your typical list, you have several options:

Iterable<String> strings = new ArrayList<>();   // if you just need to iterate
Collection<String> strings = new ArrayList<>(); // if you also need .size() or .stream()
List<String> strings = new ArrayList<>();       // if you also need .get(index)

You would rarely, if ever, refer to the list by its implementation type ArrayList. The List interface covers all the common operations you would ever need out of an ArrayList, except for perhaps ensureCapacity(). But I doubt you’ll ever need that.

Writing your code this way makes it easier to change your implementation without breaking the rest of your application. Need to switch to a thread-safe List implementation? If you’ve referred to your variable as a List throughout your API, all you have to do is substitute the new implementation and you’re good to go! Had you used ArrayList as the type and some client decided to use a method of ArrayList that’s not in List, you’d be stuck.

Clearly, tying yourself to an implementation is a bad idea. It’s the whole reason Java has an interface-based collections framework.

Guava Types

When you see that Guava’s immutable collections like ImmutableList are classes and not interfaces, it’s natural to want to treat them like other implementation types (ArrayList, HashMap, etc.). Part of the reason they are classes has to do with preventing external subtyping, but for API purposes they should be thought of as interfaces in every important sense.

A class like ImmutableList defines a type, complete with meaningful behavioral guarantees just like List or Collection. This is substantially different from the case of (say) ArrayList, which is an implementation, with semantics that were largely defined by its supertype List.

The Guava authors were even clever enough to deprecate some of the methods inhereted from Collection and List to further refine the APIs of the immutable types. If a client tries to call add, remove, or any other method that modifies the contents of the collection, the IDE and compiler will flag it.

So how does all of this play out when designing an API?

Method Return Types

For method return types, you should generally use the immutable type (such as ImmutableList) instead of the general collection interface type (such as List). This communicates to your callers all of the semantic guarantees listed above, which is almost always very useful information.

Clients can still "catch" your return types as any supertype like List or Collection based on their needs, so there’s no reason to worry about somehow burdening the client with an overly specific type.

Field Types

Field types should also use the immutable type for the reasons listed above.

If we had used the type List for the permissions field in the User class example above, the Error Prone compiler would have complained about the class not being immutable even though we annotated it as such.

This is absolutely valid! Even if our implementation type happens to be ImmutableList, the compiler has no way of verifying that. All it sees is a field of type List, and it knows the List type is mutable.

Method Parameter Types

On the other hand, a parameter type of ImmutableList is generally a nuisance to callers. Instead, accept Iterable and have your method or constructor body pass it to the appropriate copyOf method itself, trusting once again that copyOf is smarter than it sounds. The constructor of the User class above demonstrates this approach.

Further Reading and Tips

Hopefully this post has given you enough of an overview to get started using Guava’s immutable collections in your code. If you want to learn even more, be sure to check out the wiki page as well as the Javadoc.

One final consideration is that these implementations offer only shallow immutability. While the contents of the collection cannot change, the implementation cannot do anything to guarantee the immutability of the types stored within the collection itself.

As with any collection, it is almost always a bad idea to modify an element (in a way that affects its Object.equals(java.lang.Object) behavior) while it is contained in a collection. Undefined behavior and bugs will result. It’s generally best to avoid using mutable objects as elements at all, as many users may expect your "immutable" object to be deeply immutable.

3 Comments

  • JR says:

    I really like this overview of the Immutable Guava types. I would like to know your opinion about other libraries for having immutable objects, like Lombok, Immutable or vavr.
    You say “the implementation cannot do anything to guarantee the immutability of the types stored within the collection itself”. Are there no any functional libraries that can guarantee that ? If I understood correctly, only the “errorprone” compiler can guarantee it. Is this correct ?

    • michael.b.fullan says:

      Thanks! I don’t have any experience with those libraries. Guava has been my go-to in the Java world and typically has everything I need, plus it’s nice to only need a single dependency!

      Regarding shallow vs. deep immutability – you are correct! Because my User example is annotated with the errorprone @Immutable annotation, it will check to make sure that Permission is also an immutable type. Super helpful! The javadocs for @Immutable have more details if you’re curious: https://github.com/google/error-prone/blob/master/annotations/src/main/java/com/google/errorprone/annotations/Immutable.java

      • JR says:

        This errorprone tool is fantastic !! It can be used for refactoring too. I have never seen Guava at work, only Lombok and ApacheUtils. It’s a shame that many programmers don’t know about this library and the associated static analyzer. This is as powerful as Kotlin in some cases. Thanks!!

Leave a Reply