Wednesday, April 11, 2007

Fun with Java Enums

Way back in Java 1.5, enum types were added to the Java spec. The idea is simple enough; create an object with a fixed set of valid values. The values are constant, like coins. Coins come in a fixed number of types (penny, nickel, dime, etc) and each type has a fixed value (1, 5, 10, ... ). An enum type allows you to do just that in your code:
public class Coins {
 enum Coin {
  PENNY(1),NICKEL(5),DIME(10),QUARTER(25);
  int value;
         Coin( int v ) { value = v; }
 }

 public static void main(String[] args) {
  System.out.println("A penny is worth: " + Coin.PENNY.value);
 }
}
A Coin enum type is declared and each possible type of Coin is given it's respective value. And the code prints just as you would expect:
A penny is worth: 1
Part of the assumption is that a penny is always a penny and always worth 1 cent. However, if we add the following lines of code to the main method, let's see what happens.
  Coin.PENNY.value = 1000;
  System.out.println("A penny is now worth: " + Coin.PENNY.value);
When the code is compiled and run again, what is the output? In case you were wondering, it does compile quite nicely.
A penny is worth: 1
A penny is now worth: 1000
Now that's the way all my investments should grow! Seriously, though, what is the problem? The problem is that the value, and any instance variables in an enum, are not magically immutable. The enum type does not do any special checking to ensure that the instances of the type (PENNY, NICKEL, ...) remain constant. Under the covers, the enum type is converted to a static class with static members for each of the declared possbile values like this:
static final class Coins$Coin extends Enum
{
          public static final Coins$Coin[] values()
            {
                return (Coins$Coin[])$VALUES.clone();
            }

            public static Coins$Coin valueOf(String s)
            {
                 return (Coins$Coin)Enum.valueOf(Coins$Coin, s);
            }

            public static final Coins$Coin PENNY;
            public static final Coins$Coin NICKEL;
            public static final Coins$Coin DIME;
            public static final Coins$Coin QUARTER;
            int value;
            private static final Coins$Coin $VALUES[];

            static 
            {
                PENNY = new Coins$Coin("PENNY", 0, 1);
                NICKEL = new Coins$Coin("NICKEL", 1, 5);
                DIME = new Coins$Coin("DIME", 2, 10);
                QUARTER = new Coins$Coin("QUARTER", 3, 25);
                $VALUES = (new Coins$Coin[] {
                    PENNY, NICKEL, DIME, QUARTER
                });
            }

            private Coins$Coin(String s, int i, int j)
            {
                super(s, i);
                value = j;
            }
}
Our value is given the same access modifiers in the generated class as it was given when we defined the enum. Meaning that the value is not able to be modified by any member of the same package.

The fix

The fix is fairly simple. Make the value instance variable final:
  final int value;
Now when the code that tries to modify the value is compiled, this error:
Coins.java:28: cannot assign a value to final variable value
                Coin.PENNY.value = 1000;
                          ^
1 error
Problem solved. Right? Well, not so fast. For primitive types, yes. But for descendants of Object, maybe not. What if instead of an int, value was something like a Map. You could then do Coin.PENNY.value.put("bad key", "bad value") and the compiler would be perfectly happy with it. To fix the problem, ensure that the Map is unmodifiable or immutable
public class Coins
{
 enum Coin {
  PENNY(1),NICKEL(5),DIME(10),QUARTER(25);
  
  final Map value;

  Coin( int v ) { 
   Map newValue = new HashMap();
   newValue.put( "cents", v );
   value = Collections.unmodifiableMap( newValue );
  }
 }

 public static void main(String[] args) {
  System.out.println("A penny is worth: " +    
                       Coin.PENNY.value.get("cents"));
 }
}

Summary

While the compiler will ensure that the enum types are immutable, it is the responsibility of the developer to ensure that any instance variables added to the enum are also immutable. This means the instance variable needs to be final AND the type of the instance variable must itself be immutable.

No comments: