Tuesday, June 3, 2008

The Power and Flexibility of the Java Enum

As I learned and first worked with Java, I missed not having an enum like I had become used to in C++. However, when Java finally introduced the enum in J2SE 5, the result was worth the wait. In this blog entry, I'm going to cover some of the things that I really like about the enum because I have been surprised at how many experienced Java developers don't fully appreciate their power and flexibility.

For my first example, I'll demonstrate that the Java Enum is really a Java class that extends Object like other Java classes. This is most easily demonstrated by overriding the toString method of one of the enum's values.

PlainOldEnum.java

public enum PlainOldEnum
{
VANILLA,
BORING,
BLAND
{
@Override
public String toString(){ return "Bland"; }
},
ORDINARY
}


For demonstrating some of the primary characteristics of the Java enum , I will be using the following general Enum-handling methods (method names highlighted).


/**
* Print out the provided enum without explicitly calling its toString()
* method (which is implicitly called).
*
* @param theEnum The enum to be printed.
*/
private void outputEnumDirectly(final Enum theEnum)
{
System.out.println("Direct Enum: " + theEnum);
}

/**
* Print out the provided enum by explicitly calling its toString()
* method.
*
* @param theEnum The enum to be printed.
*/
private void outputEnumToString(final Enum theEnum)
{
System.out.println("\ttoString: " + theEnum.toString());
}

/**
* Print out the provided enum by explicitly calling its name() method
* (a final method provided by the parent Enum class).
*
* @param theEnum The enum to be printed.
*/
private void outputEnumName(final Enum theEnum)
{
System.out.println("\tname: " + theEnum.name() );
}

/**
* Print out the provided enum's ordinal value by explicitly calling its
* ordinal() method.
*
* @param theEnum The enum whose ordinal value is to be printed.
*/
private void outputEnumOrdinal(final Enum theEnum)
{
System.out.println("\tordinal: " + theEnum.ordinal());
}


Other commonly used enum-handling methods are shown in the next code listing.


private void processEnumForOutput(final Enum theEnum)
{
if ( theEnum == null )
{
return;
}
outputEnumDirectly(theEnum);
outputEnumToString(theEnum);
outputEnumName(theEnum);
outputEnumOrdinal(theEnum);
}

public static void printHeader(final String headerText)
{
System.out.println(
"------------------------------------------------------------------");
System.out.println("----- " + headerText);
System.out.println(
"------------------------------------------------------------------");
}


All of the methods shown above are generic enum-handling methods. The next code listing demonstrates a method written specifically to handle PlainOldEnum.


private PlainOldEnum getPlainEnumFromValue(final String enumValue)
{
PlainOldEnum plainEnum = null;
try
{
plainEnum = PlainOldEnum.valueOf(enumValue);
}
catch (IllegalArgumentException badValueForEnum)
{
System.err.println(
"ERROR: " + enumValue
+ " is not a valid value for enum PlainOldEnum:\n\t"
+ badValueForEnum.getMessage() );
}
return plainEnum;
}


This last code listing shows code that attempts to take a passed-in String and convert it to the appropriate enum with an underlying representation matching the provided String. It catches the unchecked exception IllegalArgumentException and prints a related warning out to the standard error.

When the PlainOldEnum is run through the general enum-handling methods shown above and through the specific getPlainEnumFromValue method, the results are as shown in the next screen snapshot.



The results depicted in the screen snapshot above tell us some things about the enum handling. First, we see that by overriding the toString method of PlainOldEnum.BLAND, we are able to have it printed implicitly and explicitly as the mixed-cased "Bland."

The second observation from the above snapshot that we see is the Enum.name() method returns "BLAND" despite the overridden toString() method that returns "Bland." This is not surprising considering that the Javadoc comments for this method state: "Returns the name of this enum constant, exactly as declared in its enum declaration." It is also interesting to note that one cannot override this method because it is declared as final in the Enum class.

A third observation, related to the second observation regarding the underlying representation of the enum value, is that the Enum.valueOf(String) method cannot return the appropriate Enum constant for "Bland" even though that is the String returned from the toString() for PlainOldEnum.BLAND. In other words, if one overrides the toString() method, it will change how the enum's value is presented, but it does not allow the valueOf to go the other way. This is the primary reason that I don't care for overriding an enum's String representation with toString(). I prefer to have my enums' toString() and valueOf() methods be symmetric. In other words, I like the String written out by the enum's toString() method to be the same one that can be used with the valueOf to get access to the appropriate enum value.

The issue observed above can also be seen with another example. The enum for this example, VehicleMakeEnum, is shown in the next code listing.

VehicleMakeEnum.java

public enum VehicleMakeEnum
{
CHEVROLET,
FORD,
HONDA,
PONTIAC,
SATURN,
TOYOTA;
}


The next code listing exercises VehicleMakeEnum and intentionally attempts to use its valueOf method incorrectly to demonstrate once again that the String value passed to valueOf must match the enum's underlying representation.


private VehicleMakeEnum getVehicleEnumFromValue(final String enumValue)
{
VehicleMakeEnum vehicleMake = null;
try
{
vehicleMake = VehicleMakeEnum.valueOf(enumValue);
}
catch (IllegalArgumentException badValueForEnum)
{
System.err.println(
"ERROR: " + enumValue
+ " is not a valid value for enum VehicleMakeEnum:\n\t"
+ badValueForEnum.getMessage() );
}
return vehicleMake;
}



printHeader("VehicleMakeEnum");
VehicleMakeEnum vehicleMake = VehicleMakeEnum.HONDA;
me.processEnumForOutput(vehicleMake);

vehicleMake = me.getVehicleEnumFromValue("CHEVROLET");
me.processEnumForOutput(vehicleMake);

vehicleMake = me.getVehicleEnumFromValue("Chevrolet");
me.processEnumForOutput(vehicleMake);

vehicleMake = me.getVehicleEnumFromValue("Saturn");
me.processEnumForOutput(vehicleMake);

vehicleMake = me.getVehicleEnumFromValue("TOYOTA");
me.processEnumForOutput(vehicleMake);


The output from running the above code is shown next.



This example again demonstrates that a mixed case String cannot be used with the enum's valueOf method to identify a specific enum value. This brings me to another useful feature of the Java enum. I don't like to override toString simply for a different representation because then it does not match the String required for valueOf to work properly. I prefer, therefore, to write a separate method for providing the enum value alternate representation. Another advantage of this approach is that I can present the same enum value in multiple ways.

An example of provide alternative representation methods for presenting an enum's value is best started with the StateEnum shown in the following code list.

StateEnum.java

public enum StateEnum
{
ALABAMA
{
@Override public String getAbbreviation() { return "AL"; }
@Override public String getCapital() { return "Montgomery"; }
@Override public int getStatehoodYear() { return 1819; }
@Override public String getSlogan() { return "Unforgettable"; }
@Override public String toDisplayString() { return "Alabama"; }
},
ALASKA
{
@Override public String getAbbreviation() { return "AK"; }
@Override public String getCapital() { return "Juneau"; }
@Override public int getStatehoodYear() { return 1959; }
@Override public String getSlogan() { return "North! To Alaska"; }
@Override public String toDisplayString() { return "Alaska"; }
},
ARIZONA
{
@Override public String getAbbreviation() { return "AZ"; }
@Override public String getCapital() { return "Phoenix"; }
@Override public int getStatehoodYear() { return 1912; }
@Override public String getSlogan() { return "Grand Canyon State"; }
@Override public String toDisplayString() { return "Arizona"; }
},
ARKANSAS
{
@Override public String getAbbreviation() { return "AR"; }
@Override public String getCapital() { return "Little Rock"; }
@Override public int getStatehoodYear() { return 1836; }
@Override public String getSlogan() { return "The Natural State"; }
@Override public String toDisplayString() { return "Arkansas"; }
@Override public String toString() { return "ARKANSAS"; }
},
CALIFORNIA
{
@Override public String getAbbreviation() { return "CA"; }
@Override public String getCapital() { return "Sacramento"; }
@Override public int getStatehoodYear() { return 1850; }
@Override public String getSlogan() { return "Find Yourself Here"; }
@Override public String toDisplayString() { return "California"; }
@Override public String toString() { return toDisplayString(); };
},
COLORADO
{
@Override public String getAbbreviation() { return "CO"; }
@Override public String getCapital() { return "Denver"; }
@Override public int getStatehoodYear() { return 1876; }
@Override public String getSlogan() { return "Colorful Colorado"; }
@Override public String toDisplayString() { return "Colorado"; }
};

abstract String getAbbreviation();
abstract String getCapital();
abstract int getStatehoodYear();
abstract String getSlogan();
abstract String toDisplayString();
}


As the code directly above shows, each value in the enum provides it own representation as a two-letter abbreviation (getAbbreviation()) and as a displayable String (toDisplayString()). For most of the states in the enum example above, I did not override toString, so the String returned by each toString will match the String needed in valueOf to get the appropriate enum value. However, I intentionally overrode CALIFORNIA.toString() for the example.

The next code listing shows enum-handling code specific to StateEnum.


private StateEnum getStateEnumFromValue(final String stateEnumValue)
{
StateEnum state = null;
try
{
state = StateEnum.valueOf(stateEnumValue);
}
catch (IllegalArgumentException badValueForEnum)
{
System.err.println(
"ERROR: " + stateEnumValue
+ " is not a valid value for enum StateEnum:\n\t"
+ badValueForEnum.getMessage() );
}
return state;
}

printHeader("StateEnum");
StateEnum state = StateEnum.ALASKA;
me.processEnumForOutput(state);

state = me.getStateEnumFromValue("Arizona");
me.processEnumForOutput(state);

state = me.getStateEnumFromValue("ARKANSAS");
me.processEnumForOutput(state);

state = me.getStateEnumFromValue("California");
me.processEnumForOutput(state);

state = me.getStateEnumFromValue("CALIFORNIA");
me.processEnumForOutput(state);

state = me.getStateEnumFromValue("COLORADO");
me.processEnumForOutput(state);


When the above StateEnum-handling code is executed, the output is that shown in the next screen snapshot.



We can see in the above output that "Arizona" cannot be used with valueOf even though its toDisplayString() method is overridden because the underlying representations of the states are all completely uppercase. The example with the California enum shows that even the specification of "California" in both toDisplayString() and toString does not allow the mixed-cased representation to be used with valueOf.

While the ability to represent the same enum value in different ways is a useful feature of Java enums, it is not the only useful benefit. The two methods in the following code listing show how much more efficient code that handles an enum can be if the enum in question has various support methods. In this example, StateEnum is used again. The first method "pretends" that useful methods for supplying the displayable name and for supplying the state capital are not present. Without those methods, one would need to switch on the enum value and then hard-code the response to it. The second method in the code listing shows how this same data (displayable string and capital) can be encapsulated within the enum and then easily rendered in the client code.


/**
* Process enums by switching on provided state and outputting hard-coded
* information dependent on the provided enum.
*
* @param state State on which to switch and act.
*/
public void processSimpleStateEnumsWithSwitch(final StateEnum state)
{
final String templateString = "The capital of SSSSS is CCCCC.";
switch ( state )
{
case ALASKA :
System.out.println(templateString.replace("SSSSS", "Alaska").replace("CCCCC", "Juneau"));
break;
case ARIZONA :
System.out.println(templateString.replace("SSSSS", "Arizona").replace("CCCCC", "Phoenix"));
break;
case ARKANSAS :
System.out.println(templateString.replace("SSSSS", "Arkansas").replace("CCCCC", "Little Rock"));
break;
case CALIFORNIA :
System.out.println(templateString.replace("SSSSS", "California").replace("CCCCC", "Sacramento"));
break;
case COLORADO :
System.out.println(templateString.replace("SSSSS", "Colorado").replace("CCCCC", "Denver"));
break;
default:
System.out.println( "WARNING: Apparently the state of " + state.name()
+ " was not considered in the processing code.");
break;
}
}

/**
* Process state enum. This is intended to show how much easier it is to
* process the StateEnum when the data is encapsulated in the StateEnum.
*
* @param state State to be processed with encapsulated data.
*/
public void processStatesWithEnumPower(final StateEnum state)
{
System.out.println( "The capital of " + state.toDisplayString() + " is "
+ state.getCapital() + "." );
}

printHeader("Compare switch to powerful enum");
me.processSimpleStateEnumsWithSwitch(StateEnum.ALABAMA);
me.processStatesWithEnumPower(StateEnum.ALABAMA);
me.processSimpleStateEnumsWithSwitch(StateEnum.ALASKA);
me.processStatesWithEnumPower(StateEnum.ALASKA);
me.processSimpleStateEnumsWithSwitch(StateEnum.ARIZONA);
me.processStatesWithEnumPower(StateEnum.ARIZONA);
me.processSimpleStateEnumsWithSwitch(StateEnum.ARKANSAS);
me.processStatesWithEnumPower(StateEnum.ARKANSAS);
me.processSimpleStateEnumsWithSwitch(StateEnum.CALIFORNIA);
me.processStatesWithEnumPower(StateEnum.CALIFORNIA);
me.processSimpleStateEnumsWithSwitch(StateEnum.COLORADO);
me.processStatesWithEnumPower(StateEnum.COLORADO);


The last code listing shows how much cleaner and simpler it is for the client code when the enum encapsulates its static data. Another advantage, as discussed in A Better Way to Use Java Enum than Switch, is that no concern about missing a case statement in the switch is present with the cleaner, simpler encapsulated enum approach. In the above example, I intentionally neglected to place a case statement for ALABAMA to show how and why the default keyword is necessary.

The output for running this code is shown next.



As this output demonstrates, encapsulating data in the enum allows for the same functionality to be performed by the client, but the client's code is much cleaner and safer. One thing related to this that I did not demonstrate here is that one can even pass the information associated with an enum to it via a constructor. This can be preferable to providing the information in the individual methods on each enum value. For example, I now show a piece of the StateEnum (Alabama's and Arkansas's entries only) demonstrating this approach.

StateWithConstructorEnum.java

public enum StateWithConstructorEnum
{
ALABAMA("Alabama", "AL", "Montgomery", "Unforgettable", 1819),
ALASKA("Alaska", "AK", "Juneau", "North! To Alaska", 1959),
ARIZONA("Arizona", "AZ", "Phoenix", "Grand Canyon State", 1912),
ARKANSAS("Arkansas", "AR", "Little Rock", "The Natural State", 1836),
CALIFORNIA("California", "CA", "Sacramento", "Find Yourself Here", 1850),
COLORADO("Colorado", "CO", "Denver", "Colorful Colorado", 1876);

private String displayName;
private String abbreviation;
private String capital;
private String slogan;
private int statehoodYear;

StateWithConstructorEnum(
final String displayName,
final String abbreviation,
final String capital,
final String slogan,
final int statehoodYear)
{
this.displayName = displayName;
this.abbreviation = abbreviation;
this.capital = capital;
this.slogan = slogan;
this.statehoodYear = statehoodYear;
};

public String getAbbreviation() { return this.abbreviation; }
public String getCapital() { return this.capital; }
public int getStatehoodYear() {return this.statehoodYear; }
public String getSlogan() { return this.slogan; }
public String toDisplayString() { return this.displayName; }
}


The approach shown immediately above can often be preferable because all of the data encapsulated with each enum value is easily identifiable in the constructor of that enum value and then single public methods for the entire enum can be defined rather than overriding these methods for each individual value in the enum. Another advantage of this approach occurs in situations when multiple methods use the same underlying data, which only needs to be expressed once in the constructor rather than each time in every output method it affects.

Java enums are very powerful, but there are obviously times when a regular class is preferable to the enum. Enums are fits for finite sets of data and are best used with static data. The StateEnum was a perfect example of this. All of the data returned by the methods encapsulated in that enum are for static data. Although more dynamic methods can be used with enums, I tend to prefer to place dynamic behavior in a class rather than an enum, even if it is in a class that "wraps" a member enum. The article Java Enums Want to Be Classes - Or Do They? provides good coverage of the issue of using too powerful of enums. The blog entry Java Practice - When Not To Use Enums (and its feedback comments) provides useful information on when not to use enum.

No comments: