Java Q&A

In C and C++, enums let you define a constrained set of options for an API parameter. But Java doesn't directly support this concept. Evan presents a workaround for this deficiency using simple generic types.


October 01, 1999
URL:http://www.drdobbs.com/windows/java-qa/184411078

Oct99: Java Q&A

Evan is a software architect in the Twin Cities working on better ways to build software. He can be contacted at [email protected].


If you are familiar with C or C++, you know that enums let you define a constrained set of options for an API parameter. Java, however, does not directly support this concept. You can work around this deficiency using simple generic types (such as integers or strings) to pass enum-style information in a program. While common, this approach demonstrates disregard for well-designed, safe interfaces, in preference of quick-and-dirty solutions. This approach is dangerous because generic types are inappropriate for the job. They can represent a set of values that is much larger than the set which is valid for the API call. As a result, the method accepting the parameter must face the possibility that a caller will try to pass it an invalid value. The compiler is not equipped to help avoid the problem. So you must wait until run time to witness the mistake.

There are plenty of examples where this shortcoming may be demonstrated and where a real enum capability would be beneficial. Let's look at a few that can be found in the JDK itself. For instance, the class java.awt.Label's constructor Label (String text, int alignment) takes an integer as the alignment parameter. This integer is supposed to be from the set {Label .LEFT=0, Label.CENTER=1, Label.RIGHT=2}. There is no compile-time mechanism for preventing a call to the Label constructor with an integer that is not in this set. The behavior of Label, if such an integer is used, is to throw an IllegalArgumentException. Listing One throws an exception with the stack trace shown in Listing Two.

Another case involves the class java.awt.BorderLayout, which uses a String as its layout constraint. That string should be in the set {BorderLayout.NORTH, BorderLayout.SOUTH, BorderLayout.EAST, BorderLayout.WEST, BorderLayout.CENTER} (these are the Western values). If a string from outside this set is used (see Listing Three), you get an IllegalArgumentException (Listing Four shows the stack trace).

Yet another example involves the class java.util.Calendar, which uses a number of integers defined to distinguish the various components of a timestamp -- Calendar.YEAR, Calendar.DAY_OF_MONTH, Calendar.MINUTE, and so on. These can be passed into the methods get(int field) and set(int field, int value) to read and change the value of a particular field of a timestamp. Examining the source of the Calendar class reveals that these integers are indices into an internal array and they are unchecked, so using an invalid field (see Listing Five) will result in an ArrayIndexOutOfBoundsException (see Listing Six). This demonstrates a particularly nasty scenario where the exception thrown does not clearly indicate the true problem. In addition to failing at run time, the set method does not indicate that the fault is the result of an invalid input parameter.

Implementing enum Classes

All of these examples have a similar negative consequence -- you receive an exception at run time. Of course, there is nothing wrong with checking an input at run time and throwing an exception if it is invalid. But these are examples where the burden of a possible run-time failure, even if the code is under development, is just plain unnecessary.

The alternative is to use a specialized enum-like class to represent the parameters instead of general types like integers or strings. The most important property that this class must exhibit is that it should only be possible to create valid instances. This is accomplished by taking the following steps:

1. Make the constructor private. You will only be able to create instances from within the class.

2. For each valid enum value, declare a public static final instance of the class.

3. For each declared enum value, initialize a new instance of the class, either at the point of declaration or in the class's static initializer.

For example, consider how Label alignment might have been better done (Listing Seven). Because Label is the only class that can instantiate HorizontalAlignment, there is no need to check the value of the alignment parameter in the constructor or the setAlignment method.

Using the new HorizontalAlignment enum (Listing Eight) is identical to the way that you are supposed to do it with the current JDK version of java.awt.Label. The only difference is that you cannot pass in a raw, invalid integer value.

Serialization Support

The solution is not complete. The class Label, by virtue of being a java.awt.Component, must support the java.io.Serializable interface. This means that the Label's state must be written to the output stream when the Label is serialized. As the previous example is written, however, the HorizontalAlignment class is not serializable. So, if you attempt to serialize a Label, you will receive a java.io.NotSerializableException.

You can't fix this problem by just declaring the HorizontalAlignment inner class as Serializable, however. Serializable objects must have an identity so that they retain their original meaning when they are deserialized. Listing Seven relies upon placement in memory to distinguish the three HorizontalAlignment instances. This assumes that only three instances are ever created in a JVM. If you simply declare HorizontalAlignment as serializable, then deserializing Labels will create HorizontalAlignment instances in addition to the three that are declared in the Label class.

Because there is no good way to circumvent this problem, I'll work with it by giving HorizontalAlignments an identity so that there can be multiple instances of a particular HorizontalAlignment (Label.LEFT, for instance) that will all be considered equal.

Just as with C enums, the identity can be as simple as an integer, as in Listing Nine. Each distinct alignment is distinguished by a number, for example {LEFT=0, CENTER=1, RIGHT=2}. To utilize the identity in comparisons,the methods hashCode and equals are defined so that value equality may be employed to compare instances. Of course, reference equality can no longer be used, and the equals method will have to be used exclusively for all comparisons. Applying these changes results in Listing Nine for the Label class.

What About Java 1.0?

The HorizontalAlignment example was written via an inner class. This is a feature introduced in Java 1.1. If you want, or more likely need, to use the described Java enum technique in Java 1.0, then you'll have to make some changes that are actually quite simple.

First, the HorizontalAlignment class must be declared as a separate public class. Second, to avoid having to make the constructor package visible, declare the three alignment instances in the standalone HorizontalAlignment class (Listing Ten), rather than in the Label class. Last, because serialization didn't exist in Java 1.0, you'll have to remove the customizations made in the previous section to support this feature. The only big consequence is that you will have to replace Label.LEFT with HorizontalAlignment.LEFT because the LEFT, CENTER, and RIGHT are no longer members of the Label class.

// Valid at compile- and run-time

Label label = new Label("Read Me!", Hori- zontalAlignment.RIGHT);

This turns out to be a great side effect. Now the HorizontalAlignment class is a general-purpose class that can be used wherever alignment is needed. The Swing libraries took a small step in this direction by putting a wide variety of constants into a single class javax.swing.SwingConstants.

Supporting the == Operator

Now, wouldn't it be nice if you didn't have to call the equals method to compare serializable instances of HorizontalAlignment? The problem stems from the fact that you can end up with redundant instances of the same logical enum value when you deserialize them. In JDK 1.1, there is no way to resolve identical instances of deserialized objects to the same object without subclassing java.io.ObjectInputStream. However, JDK 1.2 introduced a serialization feature that lets you do this sort of resolution in the serializable class itself.

This feature is the readResolve method on java.io.Serializable. After deserializing an object, Java invokes the readResolve method on that object, if it is defined. You can define this method to use the enum's identity to roll all value-wise identical instances into the same instance.

First, add an instance dictionary to the class. This dictionary will hold onto all distinct instances of HorizontalAlignment. Second, in the constructor, put this into the instance dictionary with the integer representation as the key. The three declared static instances will be added to the dictionary via their constructors when the class is first loaded into memory.

Finally, implement the readResolve method to return the instance of HorizontalAlignment in the instance dictionary that has the same integer value as this. Listing Eleven presents the new methods. I've left out error-handling code to keep things simple. Now you can count on there only being three accessible instances of HorizontalAlignment. This leaves you free to use == when comparing instances to each other.

Admittedly, I've put a lot of work into getting this class right, a process that's surely making you think twice about using these techniques for your enums. However, if you are using JDK 1.2, then all the functionality shown thus far can be rolled into a common base class for all your enum needs. The classes Enum.java, DuplicateEnumIdException.java, and NonExistentEnumException.java (all available electronically; see "Resource Center," page 5) let you define HorizontalAlignment with the simplicity of Listing Twelve.

Readability and Usability Benefits

The benefits up to now have been to improve validation of input parameters. There is another benefit to using enum classes, and that is readability.

Without the enum class, you would see a method declaration like this (from the JDK):

// from class java.util.Calendar

public final void set(int field, int value);

The documentation for that method does not clearly indicate the domain of the field parameter. If instead the field was reimplemented as an enum class named CalendarField, then the method declaration would look like this:

public final void set(CalendarField field, int value);

This minor difference in declaration arguably makes the method more developer friendly. This is especially true because the javadoc documentation tool would create a link to the CalendarField class that you could follow to learn more about the domain it represents.

An IDE that Helps?

In the spirit of usability, you may have noticed that many of the newer Java IDEs offer a feature often referred to as code completion. This feature tries to help you complete code statements as you type them by providing code clauses that are valid in the current editing scope. This feature makes enum class types even easier to use because it can make the valid (declared) values available to developers without having to even look them up. For example, you could type,

new Label("Read Me!", HorizontalAlignment

and the IDE will give you the options of LEFT, CENTER, and RIGHT to complete the parameter. While this works for the integer versions of Label alignment, using an enum class eliminates the clutter of the extra constants declared on the superclass Component. This feature makes the usability of enum classes very attractive.

Performance Concerns

After all the positive arguments for enum classes, you may be clamoring about the negative performance implications. Well, yes, it's true that this technique has some downsides. Now you have to instantiate extra objects and invoke methods on them. However, in a majority of cases, the impact is negligible.

Consider the case of Label. The only place that the alignments really matter is when setting the alignment of the peer Label object. In Listing Thirteen (from the JDK sources) the method could be rewritten with the new HorizontalAlignment class to look like Listing Fourteen. The rewritten code is not much more costly. You've traded a check for a valid integer alignment with a final method call to get the numeric value of the alignment. This is slower, but consider that the setAlignment method gets called rather infrequently, and you'd have to conclude that performance concerns aren't warranted in this case.

Conclusion

While Java doesn't incorporate the notion of enums, it is fairly easy to implement them using classes. The benefits include: enum classes offer compile-time rather than run-time validation of parameter values; and code using the enum classes is very suggestive.

On the flip side, the detriments include: enum classes require that you write and deploy a little extra code, particularly when serialization support is needed; and enum classes may introduce slight performance hits to your software.

In general, however, the benefits will outweigh the detriments. Just remember to weigh them against each other as you would for any design decision. Lastly, make these decisions with your ultimate goals in mind. Hopefully, these goals include producing high-quality, safe, and flexible software.

DDJ

Listing One

// Pass in an invalid alignment
new Label("Read me!", 4);

Back to Article

Listing Two

java.lang.IllegalArgumentException: improper alignment: 4
    at java.awt.Label.setAlignment(Label.java:184)
    at java.awt.Label.<init>(Label.java:127)
    at ...

Back to Article

Listing Three

// Pass in an invalid layout constraint
JPanel p = new JPanel();
p.setLayout(new BorderLayout());
p.add(new JButton("Push Me!"), "TOP");

Back to Article

Listing Four

java.lang.IllegalArgumentException: illegal component position
         at java.awt.Container.addImpl(Compiled Code)
         at java.awt.Container.add(Container.java:230)
         at ...

Back to Article

Listing Five

// Pass in an invalid Calendar field
Calendar calendar = Calendar.getInstance();
calendar.set(123456, 2);

Back to Article

Listing Six

java.lang.ArrayIndexOutOfBoundsException: 123456
         at java.util.Calendar.set(Calendar.java:767)
         at ...

Back to Article

Listing Seven

public class Label
  extends Component
{
  public static final HorizontalAlignment LEFT = new HorizontalAlignment();
  public static final HorizontalAlignment CENTER = new HorizontalAlignment();
  public static final HorizontalAlignment RIGHT = new HorizontalAlignment();

  private HorizontalAlignment alignment_;
  public static class HorizontalAlignment
        {
                // can only be invoke inside Label class scope
                private HorizontalAlignment()
                {
                }
        }
        public Label(String text, HorizontalAlignment alignment)
        {
                ...
                setAlignment(alignment);
        }
        public void setAlignment(HorizontalAlignment alignment)
        {
                // just make the assignment, no check needed
                alignment_ = alignment;
                ...
        }
        ...
}

Back to Article

Listing Eight

// Valid at compile- and run-time
Label label = new Label("Read Me!", Label.RIGHT);

// Invalid at compile-time, which is good!
label = new Label("Read Me!", 4);

Back to Article

Listing Nine

public class Label
  extends Component
  implements java.io.Serializable
{
  public static final HorizontalAlignment LEFT = new HorizontalAlignment(0);
  public static final HorizontalAlignment CENTER = new HorizontalAlignment(1);
  public static final HorizontalAlignment RIGHT = new HorizontalAlignment(2);
  private HorizontalAlignment alignment_ = LEFT;
  /** Enum class for horizontal alignment of Labels. */
         public static class HorizontalAlignment
                 implements java.io.Serializable
         {
                 private int alignValue_;
                 // can only be invoke inside Label class scope
                 private HorizontalAlignment(int align_value)
                 {
                         alignValue_ = align_value;
                 }
                 private int asInt()
                 {
                         return alignValue_;
                 }
                 /** Required for equality comparisons. */
                 public int hashCode()
                 {
                         return asInt();
                 }
                 /** Required for equality comparisons. */
                 public boolean equals(Object obj)
                 {
                   boolean ret = (obj == this);
                   if(!ret && obj != null && obj instanceof HorizontalAlignment)
                         ret = ((HorizontalAlignment)rhs).asInt() == asInt();
                   return ret;
                 }
         }
         public Label(String text, HorizontalAlignment alignment)
         {
                 ...
                 setAlignment(alignment);
         }
         public void setAlignment(HorizontalAlignment alignment)
         {
                 alignment_ = alignment;
                 ...
         }
         ...
}

Back to Article

Listing Ten

public final class HorizontalAlignment
  implements java.io.Serializable
{
  public static final HorizontalAlignment LEFT = new HorizontalAlignment(0);
  public static final HorizontalAlignment CENTER = new HorizontalAlignment(1);
  public static final HorizontalAlignment RIGHT = new HorizontalAlignment(2);

  private int alignValue_;

  // can only be invoke inside HorizontalAlignment class scope
  private HorizontalAlignment(int align_value)
  {
                 alignValue_ = align_value;
  }
         /** Allow others to get the int value. Use carefully. */
         public int asInt()
         {
                 return alignValue_;
         }
         /** Required for equality comparisons. */
         public int hashCode()
         {
                 return asInt();
         }
         /** Required for equality comparisons. */
         public boolean equals(Object rhs)
         {
                 boolean ret = (obj == this);
                 if(!ret && obj != null && obj instanceof HorizontalAlignment)
                         ret = ((HorizontalAlignment)rhs).asInt() == asInt();
                 return ret;
         }
}

Back to Article

Listing Eleven

public final class HorizontalAlignment
   implements java.io.Serializable
 {
         // new field
   private static Hashtable instanceDictionary__ = new Hashtable(3, 0.75f);

         // changed constructor
         private HorizontalAlignment(int align_value)
         {
                 alignValue_ = align_value;
                 // Store this in the instance dictionary using the
                 // int value as the key.  Needs to check for duplicate
                 instanceDictionary__.put(new Integer(align_value), this);
         }
         // new method
         protected Object readResolve()
                 throws ObjectStreamException
         {
            // Simple look up of the instance with the same integer
            // value as mine.  Needs to check for null entry.
            return (HorizontalAlignment)instanceDictionary__.get(new 
                                                          Integer(asInt()));
         }
         // all other fields and methods unchanged
         ...
}

Back to Article

Listing Twelve

public class HorizontalAlignment
  extends Enum
{
  public static final HorizontalAlignment LEFT = new HorizontalAlignment(0);
  public static final HorizontalAlignment CENTER = new HorizontalAlignment(1);
  public static final HorizontalAlignment RIGHT = new HorizontalAlignment(2);

  private HorizontalAlignment(int id)
  {
                 super(id);
  }
}

Back to Article

Listing Thirteen

public synchronized void setAlignment(int alignment)
{
         switch (alignment) {
                 case LEFT:
                 case CENTER:
                 case RIGHT:
                         this.alignment = alignment;
                         LabelPeer peer = (LabelPeer)this.peer;
                         if (peer != null) {
                                 peer.setAlignment(alignment);
                         }
                         return;
      }
      throw new IllegalArgumentException("improper alignment: " + alignment);
}

Back to Article

Listing Fourteen

public synchronized void setAlignment(HorizontalAlignment alignment)
{
         alignment_ = alignment;
         LabelPeer peer = (LabelPeer)this.peer;
         if(peer != null)
         {
peer.setAlignment(alignment.asInt());
         }
}

Back to Article


Copyright © 1999, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.