Prior to Java 1.5/5, Java lacked language support for enumerations. Although JDK 1.5 has introduced enumerations into the language, development will be done with JDK 1.4 for a while. This tutorial will explore three increasingly complex ways to define enumerations. Each increase in complexity will buy us increased type-safety. It is up to the individual developer to decide what level of complexity is warranted in each situation.
A java interface can contain final static fields.
Consider the following interface definition.
public interface EnumVer1 {
public static final int LEFT = 1;
public static final int RIGHT = 2;
public static final int UP = 3;
public static final int DOWN = 4;
}
This enum can be used two ways. First the variables
can be referenced with their fully-qualified names,
such as
if( answer == EnumVer1.LEFT ) // move leftOr any class that wishes to use these variables can implement this interface,
class UseEnumVer1 implements EnumVer1 {
. . .
void someMethod() {
. . .
if( answer == LEFT ) // move left
. . .
}
}
Obviously this pattern is very simple to implement. But there are several problems with it. First it provides very little type safety. If you want to implement a method that takes a direction as an argument, to use EnumVer1 the method implementation must look like this,
void move( int direction ) {
switch direction:
case EnumVer1.LEFT:
// move left
case EnumVer1.RIGHT:
// move right
case EnumVer1.UP:
// move up
case EnumVer1.DOWN:
// move down
}
If you correctly document the move method, stating that the client is expected to use the EnumVer1 fields and the client calling the move method actually reads and understands your documentation; then they can call the move method like this,
move( EnumVer1.LEFT );this code snippet is of course much more readable than
move( 1 );But the less readable code is still legal and the compiler will process it without comment. Even
move( 7 );will compile without warning. To anticipate this problem the move method should have some error detecting code, such as a default case in the switch block. We will see how to solve this problem in the next section by using concrete classes to represent enums.
Another problem with this pattern is that client code has to explicitly mention all the values of the enum e.g. all EnumVer1 values are hard coded in the switch statement above. If EnumVer1 is modified to include new values every piece of code that references EnumVer1 may have to be changed. We will see later how to solve this problem by including some helper methods in our enum classes.
public class EnumVer2 {
public static final EnumVer2 LEFT = new EnumVer2( );
public static final EnumVer2 RIGHT = new EnumVer2( );
public static final EnumVer2 UP = new EnumVer2( );
public static final EnumVer2 DOWN = new EnumVer2( );
}
This only slightly more complicated class
allows the compiler to do more type checking,
hence improving the safety of any client code.
Now the move method is defined like this,
void move( EnumVer2 direction ) {
if( direction == EnumVer2.LEFT ) {
// move left
} else if( direction == EnumVer2.RIGHT ) {
// move right
} else if( direction == EnumVer2.UP ) {
// move up
} else if( direction == EnumVer2.DOWN ) {
// move down
} else {
// report unknown direction
}
}
Now a user of the move method can only call move
with an argument of type EnumVer2, even though
EnumVer2 has no fields.
The identity of the different values of EnumVer2 correspond
only to their memory addresses.
Since they are also final, their addresses won't change
during the lifetime of any one VM.
If a user attempts to call move like this,
move( 1 );the compiler will report a symbol not found error, since the method move( int ) does not exist. The else clause in the move method is now much less likely to be executed because of this type-checking.
There are still a few problems with this pattern. First, the constructor is public, new EnumVer2 objects can be instantiated. That can be easily solved by making the constructor private. Second, any client code that calls the move method will still have to write a lot of code to decide which EnumVer2 object to use. For example imagine a text-base game that asks a player which way they want to move. If the variable answer is a java.lang.String calling move might look something like this,
if( answer.equals("left") ) {
move( EnumVer2.LEFT );
} else if( answer.equals("right") ) {
move( EnumVer2.RIGHT );
} else if( answer.equals("up") ) {
move( EnumVer2.UP );
} else if( answer.equals("down") ) {
move( EnumVer2.DOWN );
} else {
// report unknown direction
}
Any change to EnumVer2 might require
a change to every section in the game code that
uses EnumVer2. For example, if it
is decided that a player can also jump, this
if-then-else block would have to modified
to prevent it from reporting an unknown direction
when the user wants to jump.
This would not be a diffucult change,
but the more places that have to modified, the more likely
a bug will go unnoticed by the developer or the compiler.
This will be solved by adding a few helper methods
to the enum in the next section.
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
public final class EnumVer3 {
// instance fields
private final String moveName;
// static field
private static final List _VALUES = new ArrayList();
/** Unmodifiable view of all move types */
public static final List VALUES = Collections.unmodifiableList( _VALUES );
// the different values of the enum
public static final EnumVer3 LEFT = new EnumVer3( "left" );
public static final EnumVer3 RIGHT = new EnumVer3( "right" );
public static final EnumVer3 UP = new EnumVer3( "up" );
public static final EnumVer3 DOWN = new EnumVer3( "down" );
private EnumVer3( String name ) {
this.moveName = name;
EnumVer3._VALUES.add( this );
}
public String toString() {
return moveName;
}
public static EnumVer3 valueOf( String i ) {
Iterator iter = VALUES.iterator();
while( iter.hasNext() ) {
Object obj = iter.next();
if( i.equalsIgnoreCase( obj.toString() ) ) {
return (EnumVer3)obj;
}
}
throw new IllegalArgumentException( "Unknown move direction" );
}
}
In this class we have made the constructor private.
This ensures that users of this
class can not instantiate new EnumVer3 objects.
Next notice that _VALUES is a private field. This field is used to hold all the values of the enum as they are being constructed. We don't want to make this field public because this would allow a client to add values to it. While they couldn't add new EnumVer3 objects (remember we made the constructor private), they could add additional copies of any of EnumVer3 objects already constructed. They could also add any other object they want since _VALUES is of type List whose add method takes an Object as an argument. We don't want to have to check every object we pull out of _VALUES. In general anything we don't want a user of our enum to do, we should try to make impossible for them to do. But we would still like to have a visible List that holds every value of our enum. So we declare another field, VALUES, to be public and initialize it using the unmodifiableList( List ) factory method in the java.util.Collections class. Now the user can safety inspect all the values of EnumVer3 through the public field VALUES.
It is important that _VALUES be declared and initialized before any EnumVer3 objects are created. This is because the (private) constructor attempts to add objects to _VALUES. If it were not initialized before these objects are instantiated the VM would throw an NullPointerException. Also note that VALUES can be initialized before the enum values are instantiated. Any object added to _VALUES after VALUES is initialized will be viewable through VALUES. This is a standard feature of the anonymous class implemented inside the java.util.Collections class.
Now let's look at the valueOf( String ) method. It allows client code to get one of the enums without having to explicitly loop over all possible values. Recall our text-base game, it can now check the answer like this,
try {
move( EnemVer3.valueOf( answer ) );
} catch( IllegalArgumentException iae ) {
// report unknown direction
}
Notice how this code snippet is more compact than our earlier
switch or if-then-else statements.
Also if new values of EnumVer3 are created the above
code doesn't need any modification (although the move method
still does, but there really isn't a way to get around that).
Keep in mind that
an enum might have a lot more than four values and there could be
dozens of places where code might want to check user
input.
This is why we put all instantiated
EnumVer3 objects in _VALUES. If we wanted to add
a JUMP value to the EnumVer3 class,
we would only have to add two lines to EnumVer3.
public static final EnumVer3 JUMP =
new EnumVer3( "jump" );
The constructor and the valueOf method wouldn't
need any modifications.
This is the same reason we have the public VALUES variable. If we wanted to list out all the possible moves a player could make, we could iterator over VALUES. When we added JUMP to EnumVer3, it would automatically show up in the list, without requiring any changes out of EnumVer3.