Monday, October 31, 2011

Immutable Collections, Guava-Style

My general preference is to use immutable classes and collections as often as possible. I have often used the Collections methods for returning unmodifiable collections. One drawback of the "unmodifiable" methods is that they only create "views" of the data structures passed to them and any changes to those underlying structures do change the contents of those "views." In other words, they are only unmodifiable when used directly, but they are still modifiable when their underlying collection is accessed. Guava provides a nice set of "immutable" collections that are more truly immutable or unmodifiable. In Guava's "immutable" collections, changes to the data structure used to populate the "immutable" collection are NOT reflected in the contents of the immutable collection because it is a separate and distinct copy of the data rather than a mere "view." In this post, I look at this difference a little more closely.

I show a series of code snippets and corresponding output from running these snippets in this post to compare the standard JDK "unmodifiable" collections to Guava's "immutable" collections. Later in the post, I include the entire class's source code that features all these methods, but I show them a couple of methods at a time first so that I can focus on each.

Unmodifiable and Immutable Sets

The next code listing shows two methods, one that uses JDK's Collections.unmodifiableSet(Set) and one that uses Guava's ImmutableSet.copyOf(Collection). I show those two methods and the portion of the main function that calls those two methods. I don't show the methods that build up the sets of data provided to these instantiation methods, but those will be available later in this post in the code listing that contains the entire test class.

Demonstrating ImmutableSet.copyOf(Collection) and Collections.unmodifiableSet(Set)
   /**
    * Demonstrate Guava's ImmutableSet.
    */
   public void demoGuavaImmutableSet()
   {
      printHeader("Guava's ImmutableSet");
      final Set<String> originalStrings = buildUnderlyingSampleSet();
      final ImmutableSet<String> strings = ImmutableSet.copyOf(originalStrings);
      out.println("Immutable Set of Strings: " + strings);
      originalStrings.remove("Java");
      out.println("Original Set of Strings: " + originalStrings);
      out.println("Immutable Set of Strings: " + strings);
   }

   /**
    * Demonstrate JDK's UnmodifiableSet.
    */
   public void demoJdkUnmodifiableSet()
   {
      printHeader("JDK unmodifiableSet");
      final Set<String> originalStrings = buildUnderlyingSampleSet();
      final Set<String> strings = Collections.unmodifiableSet(originalStrings);
      out.println("Unmodifiable Set of Strings: " + strings);
      originalStrings.remove("Java");
      out.println("Original Set of Strings: " + originalStrings);
      out.println("Unmodifiable Set of Strings: " + strings);
   }

   /**
    * Main function to demonstrate Guava's immutable collections support.
    * 
    * @param arguments Command-line arguments; none expected.
    */
   public static void main(final String[] arguments)
   {
      final GuavaImmutableCollections me = new GuavaImmutableCollections();

      // Compare JDK UnmodifiableSet to Guava's ImmutableSet
      me.demoJdkUnmodifiableSet();
      me.demoGuavaImmutableSet();
   }

When the above code is executed, the output (seen in following screen snapshot) indicates that removal of the String "Java" from the original Set removes it also from the JDK "unmodifiable" Set that was created based on that original Set. The Guava "immutable Set, however, retains the "Java" String even when the same underlying Set upon which it was created has the "Java" String removed. We'll see that this is the case of the other Immutable collections as well.

Unmodifiable and Immutable Lists

As was the case for the Sets, I focus in the next code listing on the difference between using JDK's Collections.unmodifiableList(List) and Guava's ImmutableList.copyOf(Collection).

Demonstrating ImmutableList.copyOf(Collection) and Collections.unmodifiableList(List)
   /**
    * Demonstrate Guava's ImmutableList.
    */
   public void demoGuavaImmutableList()
   {
      printHeader("Guava's ImmutableList");
      final List<String> originalStrings = buildUnderlyingSampleList();
      final ImmutableList<String> strings = ImmutableList.copyOf(originalStrings);
      out.println("Immutable List of Strings: " + strings);
      originalStrings.remove("Groovy");
      out.println("Original List of Strings: " + originalStrings);
      out.println("Immutable List of Strings: " + strings);
   }

   /**
    * Demonstrate JDK's UnmodifiableList.
    */
   public void demoJdkUnmodifiableList()
   {
      printHeader("JDK unmodifiableList");
      final List<String> originalStrings = buildUnderlyingSampleList();
      final List<String> strings = Collections.unmodifiableList(originalStrings);
      out.println("Unmodifiable List of Strings: " + strings);
      originalStrings.remove("Groovy");
      out.println("Original List of Strings: " + originalStrings);
      out.println("Unmodifiable List of Strings: " + strings);
   }

   /**
    * Main function to demonstrate Guava's immutable collections support.
    * 
    * @param arguments Command-line arguments; none expected.
    */
   public static void main(final String[] arguments)
   {
      final GuavaImmutableCollections me = new GuavaImmutableCollections();

      // Compare JDK UnmodifiableList to Guava's ImmutableList
      me.demoJdkUnmodifiableList();
      me.demoGuavaImmutableList();

The output that is shown in the next screen snapshot indicates that when the code above is executed, Guava's "immutable" List does not have its element removed even when the original collection does.

Unmodifiable and Immutable Maps

Although Guava's ImmutableMap features a copyOf(Map) method similar to those previously shown for Guava's ImmutableSet and ImmutableList, I choose to use a different approach to instantiating ImmutableMap when comparing it to that returned from the JDK's Collections.unmodifiableMap(Map) method. In this case, I use a "Builder" to build the Guava ImmutableMap. The result is the same: Guava's "immutable" Map does not have its values changed even when the underlying data structure from which the Guava immutable Map was populated is changed.

Demonstrating ImmutableMap.builder().putAll(Map) and Collections.unmodifiableMap(Map)
   /**
    * Demonstrate Guava's ImmutableMap. Uses ImmutableMap.builder().
    */
   public void demoGuavaImmutableMap()
   {
      printHeader("Guava's ImmutableMap");
      final Map<String, String> originalStringsMapping = new HashMap();
      originalStringsMapping.put("D", "Dustin");
      originalStringsMapping.put("G", "Guava");
      originalStringsMapping.put("J", "Java");
      final ImmutableMap<String, String> strings =
         ImmutableMap.<String, String>builder().putAll(originalStringsMapping).build();
      out.println("Immutable Map of Strings: " + strings);
      originalStringsMapping.remove("D");
      out.println("Original Map of Strings: " + originalStringsMapping);
      out.println("Immutable Map of Strings: " + strings);
   }

   /**
    * Demonstrate JDK's UnmodifiableMap.
    */
   public void demoJdkUnmodifiableMap()
   {
      printHeader("JDK unmodifiableMap");
      final Map<String, String> originalStringsMapping = new HashMap();
      originalStringsMapping.put("D", "Dustin");
      originalStringsMapping.put("G", "Guava");
      originalStringsMapping.put("J", "Java");
      final Map<String, String> strings = Collections.unmodifiableMap(originalStringsMapping);
      out.println("Unmodifiable Map of Strings: " + strings);
      originalStringsMapping.remove("D");
      out.println("Original Map of Strings: " + originalStringsMapping);
      out.println("Unmodifiable Map of Strings: " + strings);
   }

   /**
    * Main function to demonstrate Guava's immutable collections support.
    * 
    * @param arguments Command-line arguments; none expected.
    */
   public static void main(final String[] arguments)
   {
      final GuavaImmutableCollections me = new GuavaImmutableCollections();

      // Compare JDK unmodifiableMap to Guava's ImmutableMap
      me.demoJdkUnmodifiableMap();
      me.demoGuavaImmutableMap();
   }

As was the case for Sets and Lists, the code above, when executed, leads to output (screen snapshot below) that shows that while the JDK's "unmodifiable" map is indeed modified if its underlying Map is modified (element removed), Guava's "immutable" Map does not lose an entry even when its original Map does.

Guava's Collections Builders

Although I only demonstrated use of a builder in creating my Guava immutable map example, Guava's immutable collections all support builders. This is shown for some of these collections in the next code listing.

Guava's Collections Builders
   /**
    * Demonstrate using Builders to build up Guava immutable collections.
    */
   public void demoGuavaBuilders()
   {
      printHeader("Guava's Builders");

      final ImmutableMap<String, String> languageStrings =
         ImmutableMap.<String, String>builder().put("C", "C++")
                                               .put("F", "Fortran")
                                               .put("G", "Groovy")
                                               .put("J", "Java")
                                               .put("P", "Pascal")
                                               .put("R", "Ruby")
                                               .put("S", "Scala").build();
      out.println("Languages Map: " + languageStrings);

      final ImmutableSet<String> states =
         ImmutableSet.<String>builder().add("Arizona")
                                       .add("Colorado")
                                       .add("Wyoming").build();
      out.println("States: " + states);

      final ImmutableList<String> cities =
         ImmutableList.<String>builder().add("Boston")
                                        .add("Colorado Springs")
                                        .add("Denver")
                                        .add("Fort Collins")
                                        .add("Salt Lake City")
                                        .add("San Francisco")
                                        .add("Toledo").build();
      out.println("Cities: " + cities);

      final ImmutableMultimap<String, String> multimapLanguages =
              ImmutableMultimap.<String, String>builder().put("C", "C")
                                                         .put("C", "C++")
                                                         .put("C", "C#")
                                                         .put("F", "Fortran")
                                                         .put("G", "Groovy")
                                                         .put("J", "Java")
                                                         .put("P", "Pascal")
                                                         .put("P", "Perl")
                                                         .put("P", "PHP")
                                                         .put("P", "Python")
                                                         .put("R", "Ruby")
                                                         .put("S", "Scala").build();
      out.println("Languages: " + multimapLanguages);
   }

   /**
    * Main function to demonstrate Guava's immutable collections support.
    * 
    * @param arguments Command-line arguments; none expected.
    */
   public static void main(final String[] arguments)
   {
      final GuavaImmutableCollections me = new GuavaImmutableCollections();

      // Demonstrate using builders to build up Guava Immutable Collections
      me.demoGuavaBuilders();
   }
Other Guava Immutable Collections

Guava features immutable collections beyond those shown in this post (ImmutableMap, ImmutableSet, ImmutableList, and ImmutableMultimap). These others include ImmutableListMultimap, ImmutableMultiset, ImmutableBiMap, ImmutableSetMultimap, ImmutableSortedMap, and ImmutableSortedSet.

Elements Immutability or Mutability is Not Determined by the Collection Type

Guava's immutable collections are often preferable to the JDK's "unmodifiable" collections because the Guava immutable collections cannot be changed even when the data structure upon which they were first created changes. However, neither the JDK unmodifiable collections nor the Guava immutable collections can do anything about elements of the respective collections that are themselves mutable. In other words, a person may not be able to add an element or remove an element from an immutable or unmodifiable collection, but that same person can change any given element's contents if that element is mutable. In my examples above, I used String elements. Because Strings are, by their very nature, immutable, my collection elements cannot be changed. However, had I used classes that provide set methods or other ways to modify the given object, then these objects could have their value changed regardless of whether they are stored in immutable or unmodifiable collections.

The Entire Code Listing

The snippets of code shown above, along with some helper methods that they called but were not shown above, are available in the next code listing.

GuavaImmutableCollections.java
package dustin.examples;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import static java.lang.System.out;

import com.google.common.collect.ImmutableSet;
import java.util.*;

/**
 * Class that demonstrates Guava's support of immutable collections.
 * 
 * @author Dustin
 */
public class GuavaImmutableCollections
{
   /**
    * Build a sample set to be used in demonstrations.
    * 
    * @return Sample set of Strings.
    */
   public Set<String> buildUnderlyingSampleSet()
   {
      final Set<String> strings = new HashSet<String>();
      strings.add("Dustin");
      strings.add("Java");
      strings.add("College Football");
      return strings;
   }

   /**
    * Build a sample list to be used in demonstrations.
    * 
    * @return Sample list of Strings.
    */
   public List<String> buildUnderlyingSampleList()
   {
      final List<String> gStrings = new ArrayList<String>();
      gStrings.add("Guava");
      gStrings.add("Groovy");
      gStrings.add("Grails");
      gStrings.add("Gradle");
      gStrings.add("Grape");
      return gStrings;
   }

   /**
    * Demonstrate Guava's ImmutableSet.
    */
   public void demoGuavaImmutableSet()
   {
      printHeader("Guava's ImmutableSet");
      final Set<String> originalStrings = buildUnderlyingSampleSet();
      final ImmutableSet<String> strings = ImmutableSet.copyOf(originalStrings);
      out.println("Immutable Set of Strings: " + strings);
      originalStrings.remove("Java");
      out.println("Original Set of Strings: " + originalStrings);
      out.println("Immutable Set of Strings: " + strings);
   }

   /**
    * Demonstrate JDK's UnmodifiableSet.
    */
   public void demoJdkUnmodifiableSet()
   {
      printHeader("JDK unmodifiableSet");
      final Set<String> originalStrings = buildUnderlyingSampleSet();
      final Set<String> strings = Collections.unmodifiableSet(originalStrings);
      out.println("Unmodifiable Set of Strings: " + strings);
      originalStrings.remove("Java");
      out.println("Original Set of Strings: " + originalStrings);
      out.println("Unmodifiable Set of Strings: " + strings);
   }

   /**
    * Demonstrate Guava's ImmutableList.
    */
   public void demoGuavaImmutableList()
   {
      printHeader("Guava's ImmutableList");
      final List<String> originalStrings = buildUnderlyingSampleList();
      final ImmutableList<String> strings = ImmutableList.copyOf(originalStrings);
      out.println("Immutable List of Strings: " + strings);
      originalStrings.remove("Groovy");
      out.println("Original List of Strings: " + originalStrings);
      out.println("Immutable List of Strings: " + strings);
   }

   /**
    * Demonstrate JDK's UnmodifiableList.
    */
   public void demoJdkUnmodifiableList()
   {
      printHeader("JDK unmodifiableList");
      final List<String> originalStrings = buildUnderlyingSampleList();
      final List<String> strings = Collections.unmodifiableList(originalStrings);
      out.println("Unmodifiable List of Strings: " + strings);
      originalStrings.remove("Groovy");
      out.println("Original List of Strings: " + originalStrings);
      out.println("Unmodifiable List of Strings: " + strings);
   }

   /**
    * Demonstrate Guava's ImmutableMap. Uses ImmutableMap.builder().
    */
   public void demoGuavaImmutableMap()
   {
      printHeader("Guava's ImmutableMap");
      final Map<String, String> originalStringsMapping = new HashMap<String, String>();
      originalStringsMapping.put("D", "Dustin");
      originalStringsMapping.put("G", "Guava");
      originalStringsMapping.put("J", "Java");
      final ImmutableMap<String, String> strings =
         ImmutableMap.<String, String>builder().putAll(originalStringsMapping).build();
      out.println("Immutable Map of Strings: " + strings);
      originalStringsMapping.remove("D");
      out.println("Original Map of Strings: " + originalStringsMapping);
      out.println("Immutable Map of Strings: " + strings);
   }

   /**
    * Demonstrate JDK's UnmodifiableMap.
    */
   public void demoJdkUnmodifiableMap()
   {
      printHeader("JDK unmodifiableMap");
      final Map<String, String> originalStringsMapping = new HashMap<String, String>();
      originalStringsMapping.put("D", "Dustin");
      originalStringsMapping.put("G", "Guava");
      originalStringsMapping.put("J", "Java");
      final Map<String, String> strings = Collections.unmodifiableMap(originalStringsMapping);
      out.println("Unmodifiable Map of Strings: " + strings);
      originalStringsMapping.remove("D");
      out.println("Original Map of Strings: " + originalStringsMapping);
      out.println("Unmodifiable Map of Strings: " + strings);
   }

   /**
    * Demonstrate using Builders to build up Guava immutable collections.
    */
   public void demoGuavaBuilders()
   {
      printHeader("Guava's Builders");

      final ImmutableMap<String, String> languageStrings =
         ImmutableMap.<String, String>builder().put("C", "C++")
                                               .put("F", "Fortran")
                                               .put("G", "Groovy")
                                               .put("J", "Java")
                                               .put("P", "Pascal")
                                               .put("R", "Ruby")
                                               .put("S", "Scala").build();
      out.println("Languages Map: " + languageStrings);

      final ImmutableSet<String> states =
         ImmutableSet.<String>builder().add("Arizona")
                                       .add("Colorado")
                                       .add("Wyoming").build();
      out.println("States: " + states);

      final ImmutableList<String> cities =
         ImmutableList.<String>builder().add("Boston")
                                        .add("Colorado Springs")
                                        .add("Denver")
                                        .add("Fort Collins")
                                        .add("Salt Lake City")
                                        .add("San Francisco")
                                        .add("Toledo").build();
      out.println("Cities: " + cities);

      final ImmutableMultimap<String, String> multimapLanguages =
              ImmutableMultimap.<String, String>builder().put("C", "C")
                                                         .put("C", "C++")
                                                         .put("C", "C#")
                                                         .put("F", "Fortran")
                                                         .put("G", "Groovy")
                                                         .put("J", "Java")
                                                         .put("P", "Pascal")
                                                         .put("P", "Perl")
                                                         .put("P", "PHP")
                                                         .put("P", "Python")
                                                         .put("R", "Ruby")
                                                         .put("S", "Scala").build();
      out.println("Languages: " + multimapLanguages);
   }

   /**
    * Write a separation header to standard output that includes provided header
    * text.
    * 
    * @param headerText Text to be used in separation header.
    */
   public static void printHeader(final String headerText)
   {
      out.println("\n========================================================");
      out.println("== " + headerText);
      out.println("========================================================");
   }

   /**
    * Main function to demonstrate Guava's immutable collections support.
    * 
    * @param arguments Command-line arguments; none expected.
    */
   public static void main(final String[] arguments)
   {
      final GuavaImmutableCollections me = new GuavaImmutableCollections();

      // Compare JDK UnmodifiableSet to Guava's ImmutableSet
      me.demoJdkUnmodifiableSet();
      me.demoGuavaImmutableSet();

      // Compare JDK UnmodifiableList to Guava's ImmutableList
      me.demoJdkUnmodifiableList();
      me.demoGuavaImmutableList();

      // Compare JDK unmodifiableMap to Guava's ImmutableMap
      me.demoJdkUnmodifiableMap();
      me.demoGuavaImmutableMap();

      // Demonstrate using builders to build up Guava Immutable Collections
      me.demoGuavaBuilders();
   }
}
Conclusion

The Guava "immutable" collections are often preferable to the similar JDK "unmodifiable" collections provided by the Collections class because Guava's immutable collections cannot be changed even when the original data structure upon which they were created is changed. The reason for the difference is that the JDK's "unmodifiable" collections are "views" of underlying collections and these views are changed if the thing they are "viewing" is changed. The Guava immutable collections, on the other hand, are not mere views of the source data structure, but are copies of it such that changes to the original structure have no impact on the copied immutable collection.

No comments: