Final Collections

Periodically, while programming, you may want to make constant sets and store them in final variables for public use. This desire can lead to all sorts of problems. Consider the code in Example 2-11.

Example 2-11. A collection in a final static member
package oracle.hcj.finalstory;
public class FinalCollections {
 public static class Rainbow {
 public final static Set VALID_COLORS; static {
 VALID_COLORS = new HashSet( );
 VALID_COLORS.add(Color.red);
 VALID_COLORS.add(Color.orange);
 VALID_COLORS.add(Color.yellow);
 VALID_COLORS.add(Color.green);
 VALID_COLORS.add(Color.blue);
 VALID_COLORS.add(Color.decode("#4B0082")); // indigo
 VALID_COLORS.add(Color.decode("#8A2BE2")); // violet
 }
 }
}


The goal of this code is to declare a class with a Set of final and static Colors representing the colors of the rainbow. You want to be able to use this Set without concerning yourself with the possibility of accidentally changing it. The problem is that the Set isn't final at all! Break it with Example 2-12.

Example 2-12. A defect caused by a nonimmutable set
package oracle.hcj.finalstory;
public final static void someMethod( ) {
 Set colors = Rainbow.VALID_COLORS;
 colors.add(Color.black); // <= logic error but allowed by compiler
 System.out.println(colors);
 }


The reference to the Set is final, but the Set itself is mutable. In short, your constant variable isn't very constant. The point is that final is not the same as immutable. You can firm up this code in the same way you locked down returned collections from a bean in :

package oracle.hcj.finalstory;
public static class RainbowBetter {
 public final static Set VALID_COLORS; static {
 Set temp = new HashSet( );
 temp.add(Color.red);
 temp.add(Color.orange);
 temp.add(Color.yellow);
 temp.add(Color.green);
 temp.add(Color.blue);
 temp.add(Color.decode("#4B0082")); // indigo
 temp.add(Color.decode("#8A2BE2")); // violet
 VALID_COLORS = Collections.unmodifiableSet(temp);
 }
 }
}


This version of the class is much better. Your Set of Colors cannot be modified because you have turned it into an immutable object. The reference to the Set is final, and the contents of the collection are locked down. In the static{} initializer, note how you have to use a temporary set to store the colors. This is because you can set a final variable only once, even in the initializer. If you try to set it more than once or change the variable in the initializer, your compiler will give an error message stating that you cannot change the final variable. Remember that deferred finals are a one-shot deal. Once set (no pun intended), they can't be changed. Now that you have a strategy to lock down your Set, let's revisit the old logic bug that we discussed in Example 2-12:

package oracle.hcj.finalstory;
public final static void someMethod( ) {
 Set colors = RainbowBetter.VALID_COLORS;
 colors.add(Color.black); // <= exception here
 System.out.println(colors); }


Now that you have the Set locked down, this code results in an exception. Specifically, the method will throw an UnsupportedOperationException whenever the user tries to use any write methods on VALID_COLORS, as it is now immutable. In this case, you haven't been able to trade a logic bug for a compiler bug, but you have been able to trade a logic bug for an exception. Although this trade isn't as good, it's still definitely worthwhile. Always use the java.util.Collections class to get unmodifiable collections and maps when creating final collections and maps. As far as unmodifiable sets go, the performance hit is negligible. As it turns out, the JDK implements unmodifiable collections in a performance-conscious way. If you look into the JDK source, you will see the static nested classes UnmodifiableSet and UnmodifableCollection. The code in Example 2-13[1] is pasted directly from the JDK source. All I did was change the spacing to conform to Oracle standards and remove the Javadoc for brevity's sake.

[1] From JDK source Java.util.Collections.

Example 2-13. Implementation of unmodifiable collections
package oracle.hcj.finalstory;
public static Collection unmodifiableCollection(Collection c) {
 return new UnmodifiableCollection(c);
}
static class UnmodifiableCollection implements Collection, Serializable {
 // use serialVersionUID from JDK 1.2.2 for interoperability
 private static final long serialVersionUID = 1820017752578914078L;
 Collection c;
 UnmodifiableCollection(Collection c) {
 if (c==null)
 throw new NullPointerException( );
 this.c = c;
 }
 public int size( ) {return c.size( );}
 public boolean isEmpty( ) {return c.isEmpty( );}
 public boolean contains(Object o) {return c.contains(o);}
 public Object[] toArray( ) {return c.toArray( );}
 public Object[] toArray(Object[] a) {return c.toArray(a);}
 public String toString( ) {return c.toString( );}
 public Iterator iterator( ) {
 return new Iterator( ) {
 Iterator i = c.iterator( );
 public boolean hasNext( ) {return i.hasNext( );}
 public Object next( ) {return i.next( );}
 public void remove( ) {
 throw new UnsupportedOperationException( );
 }
 };
 }
 public boolean add(Object o){
 throw new UnsupportedOperationException( );
 }
 public boolean remove(Object o) {
 throw new UnsupportedOperationException( );
 }
 public boolean containsAll(Collection coll) {
 return c.containsAll(coll);
 }
 public boolean addAll(Collection coll) {
 throw new UnsupportedOperationException( );
 }
 public boolean removeAll(Collection coll) {
 throw new UnsupportedOperationException( );
 }
 public boolean retainAll(Collection coll) {
 throw new UnsupportedOperationException( );
 }
 public void clear( ) {
 throw new UnsupportedOperationException( );
 }
}
public static Set unmodifiableSet(Set s) {
 return new UnmodifiableSet(s);
}


When you call Collections.unmodifiableSet( ), the class creates a new instance of this static nested class and sets the source collection as the delegate object. As you can see in the example code from the JDK, the class UnmodifiableSet implements java.util.Set and inherits from UnmodifiableCollection, which in turn implements java.util.Collection. Together, they form a delegate structure. Any read call to the UnmodifiableCollection is forwarded to the delegate collection. However, if the user tries to access a write operation, the class throws an instance of UnsupportedOperationException. Therefore, the additional overhead of the UnmodifiableSet is only a single method call. This delegate structure also plugs another big hole: if the UnmodifiableSet class inherited from HashSet, then the user could just cast the instances back to HashSet to gain access to write methods. The delegate structure in the JDK quite elegantly blocks this, ensuring that an UnmodifiableSet truly is unmodifiable, even when placed in the hands of a clever and sneaky programmer. All of the other collection classes work similarly to UnmodifiableSet. You should use these heavily in your code. Regrettably, there is no similar way to lock down final array objects, so be careful when using them.

      
Comments