As you begin to integrate enums into your own programs, one of the first tasks you'll want to accomplish is using an enum with a switch statement. This is a pretty obvious application; there's little value in using enums if you can't easily react to the set of values available.
Prior to Java 1.4, switch only worked with int, short, char, and byte values. However, since enums have a finite set of values, Tiger adds switch support for them. Here's an example of using an enum in a switch statement:
public void testSwitchStatement(PrintStream out) throws IOException { StringBuffer outputText = new StringBuffer(student1.getFullName( )); switch (student1.getGrade( )) { case A: outputText.append(" excelled with a grade of A"); break; case B: // fall through to C case C: outputText.append(" passed with a grade of ") .append(student1.getGrade( ).toString( )); break; case D: // fall through to F case F: outputText.append(" failed with a grade of ") .append(student1.getGrade( ).toString( )); break; case INCOMPLETE: outputText.append(" did not complete the class."); break; } out.println(outputText.toString( )); }
NOTE
This code assumes that student1 has already been created; this is taken care of in the test class, "GradeTester".
The argument to switch must be an enumerated value; in this case, the return type of getGrade( ) is Grade, which meets these requirements. However, there is another requirement that makes this code a little odd--did you catch it? Note the format of each case clause:
case A: case B: case C: case D: case F: case INCOMPLETE:
See anything missing? How about the enum class identifier:
case Grade.A: case Grade.B: case Grade.C: case Grade.D: case Grade.F: case Grade.INCOMPLETE:
For those of you up on Tiger, this may make you think about the import static feature of the language, which I cover in Chapter 8. However, the two have no relation (except perhaps on an implementation level)Tiger simply requires that you not preface each enumerated type with the enum class name. In fact, it's a compilation error if you do! Sort of a nice convenience function, I think.
There's another issue you should be careful aboutnot handling every enumerated type. In the following version of the switch, I've left out handling of Grade.D:
switch (student1.getGrade( )) { case A: outputText.append(" excelled with a grade of A"); break; case B: // fall through to C case C: outputText.append(" passed with a grade of ") .append(student1.getGrade( ).toString( )); break; case F: outputText.append(" failed with a grade of ") .append(student1.getGrade( ).toString( )); break; case INCOMPLETE: outputText.append(" did not complete the class."); break; }
NOTE
You can compile the book's code with warnings turned on with "ant checkcompile". All "-Xlint" warnings will be displayed.
It's not completely clear as to if the compiler will be required to issue a warning if all types aren't handled; however, it is clear that this is bad coding. You need to be sure that every possible enumerated type is handled, or get ready for some late-night debugging sessions.
|
The handling of an enumerated type by the compiler is a little different than the handling of an integral type. That difference stems from enum values not being compile-time constants; in other words, your code is not turned into the following at compile-time:
switch (student1.getGrade( )) { case 0: outputText.append(" excelled with a grade of A"); break; case 1: // fall through to C case 2: outputText.append(" passed with a grade of ") .append(student1.getGrade( ).toString( )); break; case 3: // fall through to F case 4: outputText.append(" failed with a grade of ") .append(student1.getGrade( ).toString( )); break; case 5: outputText.append(" did not complete the class."); break; }
Instead, assuming that the enum and switch statement exist in the same compilation unit, a jump table is created, relating each enumerated type to the value of ordinal( ), invoked on each type. That results in nearly the same performance as the inlining shown above; the ordinal values aren't inserted into the code, but the compiler can look them up in the jump table extremely quickly. If the enum is changed and recompiled, the jump table is updated, and there's no problem.
More often than not, though, the switch and enum are not in the same compilation unit, and this is not possible. In these cases, most compilers turn the switch statement into a series of if/else statements:
Grade tmp = student1.getGrade( ); if (tmp == Grade.A) outputText.append(" excelled with a grade of A"); else if ((tmp == Grade.B) || (tmp == Grade.C)) outputText.append(" passed with a grade of ") .append(student1.getGrade( ).toString( )); else if ((tmp == Grade.D) || (tmp == Grade.F)) outputText.append(" failed with a grade of ") .append(student1.getGrade( ).toString( )); else if (tmp == Grade.INCOMPLETE) outputText.append(" did not complete the class.");
This isn't efficient as a jump table, but this ensures that if the enum is changed in one compilation unit, the switch statement (in a different unit) continues to function properly. It also removes the need to worry about reordering of an enum, which would affect a jump table.
...using the default keyword? It's perfectly legal, and in fact strongly recommended. Since enum is a new type in Java, it would be easy for someone to come along and add a new type to your enum without you knowing about it:
public enum Grade { A, B, C, D, F, INCOMPLETE, WITHDREW_PASSING, WITHDREW_FAILING };
NOTE
"default" is best used in this way, to catch unexpected valuesit's generally good programming practice to specificallyhandle every known value, though, as that makes your code much clearer. "default" should be for handling unknown conditions, and not used as a catch-all.
Now, your switch code will fail because it doesn't account for all the possible Grade values. It's better to plan for this with a little more robust code:
switch (student1.getGrade( )) { case A: outputText.append(" excelled with a grade of A"); break; case B: // fall through to C case C: outputText.append(" passed with a grade of ") .append(student1.getGrade( ).toString( )); break; case D: // fall through to F case F: outputText.append(" failed with a grade of ") .append(student1.getGrade( ).toString( )); break; case INCOMPLETE: outputText.append(" did not complete the class."); break; default: outputText.append(" has a grade of ") .append(student1.getGrade( ).toString( )); }
An even better idea would be to throw some sort of error on an unexpected type--this will ensure that you and other programmers realize that something is out of sync:
default: throw new AssertionError("Unexpected enumerated value!");