Unlike most modern languages, Java does not support the concept of user-defined enumerated types--enums. This article revisits the topic one more time, briefly defining what it means for a programming language to support enums and reviewing the strengths and weaknesses of two alternative approaches for Java. It then presents a mini-language for defining enums compactly. A small "compiler" that translates this mini-language into Java source code is provided as a resource accompanying this article.
I first encountered user-defined enumerated types in the late 1970s when I switched from FORTRAN to Pascal. Pascal, Ada, and C++, the programming languages that dominated my life before I switched to Java in 1997, have always had this feature. Even though Java is currently my programming language of choice, I've had to adjust to the lack of enums.
What is an enumerated type? Consider, for example, this C++ code fragment:
enum Day {SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY};
This declaration provides a user-defined type named Day. It
enumerates all of the possible values for a variable of this type.
|
Related Reading
Java Data Objects |
Different programming languages take slightly different approaches, but the most desirable characteristics of an enumerated type facility in a programming language are:
Let's consider these characteristics one at a time.
An enum declaration should create a new type that is distinct from all
other types, including primitive types and all other enum types currently in
scope. In particular, when it comes to assignments or function arguments,
variables of type int and variables of an enumerated type cannot
be freely interchanged without explicit casts. The compiler should be able to
enforce this. For example, given the above enum declaration and a method with
the following specification:
public void foo(Day);
the compiler would report an error if we attempt to call this method with
an argument of type int, as in
foo(4); // compilation error
Unlike Pascal, Ada, and C++, C enums are not simply like ints, they
are ints. Thus, C also fails to support enums, according to my
criteria.
The amount of typing effort to create the enum type should be minimal. For example, compare the above enum declaration with the following Java alternative:
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
The enum declaration is compact and efficient, but the Java constant declarations are not. Enums with larger numbers of values provide even better examples. While compact, efficient declaration of enumerated values is important, especially when attempting to get programmers to use the language feature, this criterion is of lesser importance than the other three.
Language operators (such as assignment, equality, and less than) should work for enums. Enums should be usable as array indices and in control flow constructs such as the case alternatives of switch statements. For example, I would like to be able to write a statement similar to the following:
for (Day d = SUNDAY; d <= SATURDAY; ++d) {
switch(d) {
case MONDAY: ...;
break;
case TUESDAY: ...;
break;
case WEDNESDAY: ...;
break;
case THURSDAY: ...;
break;
case FRIDAY: ...;
break;
case SATURDAY:
case SUNDAY: ...;
}
}
I can do this if the enums are defined as integer constants as in characteristic 2, above,
but not if they are objects. For Java, methods such as equals()
and compareTo() provide suitable alternatives to the relational
operators, but there are no good non-int alternatives for array indices and
switch statements.
Enums should have performance characteristics at the level of primitive integer types. There should be no runtime penalty for using enums over ints.
A language fully supports enumerated types if it meets all four of the above criteria. Thus, Pascal, Ada, and C++ all support enumerated types, while Java does not. As indicated in Resources below, enum support is one of the most requested Java language extensions.
James Gosling obviously knew about enums when he designed Java, so their omission must have been intentional. Perhaps he did not fully appreciate their value, or perhaps he made a conscious decision to promote polymorphism by discouraging processing based on multi-way branching, as is often associated with enums. Whatever the reason, we are (still!) forced to live without them in Java.
|
Java does not directly support user-defined enumerated types, but two general approaches have evolved as alternatives.
The first alternative, which I will refer to as the enums-as-integer-constants alternative, uses integer constants. For example, a Java program might contain:
public static final int SUNDAY = 0;
public static final int MONDAY = 1;
public static final int TUESDAY = 2;
public static final int WEDNESDAY = 3;
public static final int THURSDAY = 4;
public static final int FRIDAY = 5;
public static final int SATURDAY = 6;
The integer constants allow us to work in Java using symbolic names instead
of raw constant values, making the source code much more readable and
maintainable. The constants can be declared within a larger class with other
fields and methods, such as a Date class, or they can be collected
in a separate class, such as Day. With the separate class
approach, constants will need to be qualified, as in
"Day.MONDAY." As a variation of the separate class
approach, one could use a Java interface. Then, other classes needing to
access the constants could "implement" the interface and access the
constants by their simple name; i.e., "MONDAY."
Personally, I dislike the use of interfaces for this purpose.
This enums-as-integer-constants alternative clearly satisfies criteria 3 (language integration) and 4 (efficiency) since the enumerated values are implemented as ints, but it fails on the other two. It is too verbose and certainly isn't type-safe, the more serious failure. Although I have used this approach in the past, I do not consider it to be a viable alternative to actually having enums built into the language.
As outlined in publications by Eric Armstrong, Joshua Bloch, Glen McCluskey,
and others (see Resources), the second alternative, which I will refer to as the enums-as-objects alternative, creates a distinct
class for the enumerated type and uses public objects of that class for the
enumerated values. The class definition for Day would look
something like the following:
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.io.Serializable;
import java.io.InvalidObjectException;
public final class Day implements Comparable, Serializable {
private static int size = 0;
private static int nextOrd = 0;
private static Map nameMap = new HashMap(10);
private static Day first = null;
private static Day last = null;
private final int ord;
private final String label;
private Day prev;
private Day next;
public static final Day SUNDAY = new Day("SUNDAY");
public static final Day MONDAY = new Day("MONDAY");
public static final Day TUESDAY = new Day("TUESDAY");
public static final Day WEDNESDAY = new Day("WEDNESDAY");
public static final Day THURSDAY = new Day("THURSDAY");
public static final Day FRIDAY = new Day("FRIDAY");
public static final Day SATURDAY = new Day("SATURDAY");
/**
* Constructs a new Day with its label.
* (Uses default value for ord.)
*/
private Day(String label) {
this(label, nextOrd);
}
/**
* Constructs a new Day with its label and ord value.
*/
private Day(String label, int ord) {
this.label = label;
this.ord = ord;
++size;
nextOrd = ord + 1;
nameMap.put(label, this);
if (first == null)
first = this;
if (last != null) {
this.prev = last;
last.next = this;
}
last = this;
}
/**
* Compares two Day objects based on their ordinal values.
* Satisfies requirements of interface java.lang.Comparable.
*/
public int compareTo(Object obj) {
return ord - ((Day)obj).ord;
}
/**
* Compares two Day objects for equality. Returns true
* only if the specified Day is equal to this one.
*/
public boolean equals(Object obj) {
return super.equals(obj);
}
/**
* Returns a hash code value for this Day.
*/
public int hashCode() {
return super.hashCode();
}
/**
* Resolves deserialized Day objects.
* @throws InvalidObjectException if deserialization fails.
*/
private Object readResolve() throws InvalidObjectException {
Day d = get(label);
if (d != null)
return d;
else {
String msg = "invalid deserialized object: label = ";
throw new InvalidObjectException(msg + label);
}
}
/**
* Returns Day with the specified label.
* Returns null if not found.
*/
public static Day get(String label) {
return (Day) nameMap.get(label);
}
/**
* Returns the label for this Day.
*/
public String toString() {
return label;
}
/**
* Always throws CloneNotSupportedException; guarantees that
* Day objects are never cloned.
*
* @return (never returns)
*/
protected Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
/**
* Returns an iterator over all Day objects in declared order.
*/
public static Iterator iterator() {
// anonymous inner class
return new Iterator()
{
private Day current = first;
public boolean hasNext() {
return current != null;
}
public Object next() {
Day d = current;
current = current.next();
return d;
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns the ordinal value of this Day.
*/
public int ord() {
return this.ord;
}
/**
* Returns the number of declared Day objects.
*/
public static int size() {
return size;
}
/**
* Returns the first declared Day.
*/
public static Day first() {
return first;
}
/**
* Returns the last declared Day.
*/
public static Day last() {
return last;
}
/**
* Returns the previous Day before this one in declared order.
* Returns null for the first declared Day.
*/
public Day prev() {
return this.prev;
}
/**
* Returns the next Day after this one in declared order.
* Returns null for the last declared Day.
*/
public Day next() {
return this.next;
}
}
The enumerated values are declared as public static objects in the class.
Additionally, the class contains private constructors, an Iterator
to step over all of the values, common Java methods such as
toString(), equals(), and compareTo(),
and methods similar to those in other languages that are designed to make the
class more usable, such as ord(), prev(),
next(), first(), and last(). Additional
examples are available in the articles listed in the Resources section below and in the download files for this article.
This enums-as-objects alternative has the very desirable characteristics of type safety and runtime efficiency (criteria 1 and 4 above), but it fails miserably on the other two criteria. In particular, it is much wordier than the enums-as-integer-constants alternative, and therefore less likely to be used by most programmers. Also, it does not permit enums to be used as array indices or as case alternatives for a switch statement, thereby decreasing its utility. In general, I prefer this second (enums-as-objects) alternative, but in practice, I have used it less often.
In summary, neither approach provides a satisfactory alternative to having enums built into the language. Now, I am not in a position to modify the Java language beyond voting for proposed language changes, so I can't do anything about the integration of enums-as-objects with the rest of Java, but I can remove the burden imposed by the effort to create the class and objects necessary for the second alternative.
|
I have designed a mini-language called jEnum for declaring enums, and I
have written a small "compiler-like" translator that translates
jEnum declarations into the equivalent Java class. For example, the
declaration for enum Day given in the beginning of this article is
translated into the equivalent Java class listed above--this is actually
how the class was created. Moreover, jEnum permits an optional package
declaration (simply copied over into the class) and an optional class
comment (also copied), and allows the specification of underlying integer
values and string representations for the objects, as illustrated in the
following example:
package com.softmoore.util;
/**
* Various USA coins
*/
enum Coin { PENNY("penny") = 1, NICKEL("nickel") = 5, DIME("dime") = 10,
QUARTER("quarter") = 25, HALF_DOLLAR("half dollar") = 50 };
Although the enums-as-integer-constants alternative has merit in certain circumstances, the enums-as-objects alternative is generally preferable because of its type safety. Of course the enums-as-objects alternative still has its shortcomings. This section briefly outlines jEnum, which retains the strengths of the enums-as-objects alternative while at the same time providing a compact, efficient declaration of enumerated values. In other words, it also satisfies criteria number 2.
For the compiler-oriented reader, here is the simple grammar for the jEnum mini-language.
compilationUnit = ( packageDecl )? ( docComment )? enumTypeDecl .
packageDecl = "package" packagePath ";" .
packagePath = packageName ( "." packageName )* .
docComment = "/**" commentChars "*/" .
enumTypeDecl = "enum" enumTypeName "{" enumList "}" ";" .
enumList = enumDecl ( "," enumDecl )* .
enumDecl = enumLiteral ( "(" stringLiteral ")" )? ( "=" intLiteral )? .
packageName = identifier .
enumTypeName = identifier .
enumLiteral = identifier .
commentChars = any-char-sequence-except-"*/"
The grammar allows an optional package declaration in the beginning that
looks just like any Java package declaration, optionally followed by a standard
Javadoc comment. The enum type declaration begins with the word
"enum" (itself also a reserved word in the jEnum
language). The enumerated values are enclosed in curly braces and are
separated by commas. Each declaration of an enumerated value consists of a
standard Java identifier, optionally followed by a string literal enclosed in
parentheses, optionally followed by the assignment operator "=" and
an integer literal.
If you omit the string literal, then the name for the enumerated value
will be used. If you omit the assignment of an integer literal, then the
compiler will assign values sequentially after the last assigned value,
starting with zero, if none are assigned. The string values are returned as
part of the toString() method, and the integer values are returned
by the ord() method. For example, given the following enum
declaration:
enum Color { RED("Red") = 2, WHITE("White") = 4, BLUE };
RED has the label "Red" and ord 2WHITE has the label "White" and ord 4BLUE has the label "BLUE" and ord 5 Note that Java reserved words are still reserved in jEnum; you can't
have a package named "this" or an enum type named
"for." Additionally, all string labels (string
literals or enumerated value names) must be distinct, and all integer literals
must be strictly increasing. Thus the following would not compile, as labels
are not distinct:
enum Color { RED("Red"), WHITE("BLUE"), BLUE };
Nor would this declaration compile, because integers are not increasing
(since WHITE is auto-assigned the value 2):
enum Color { RED = 1, WHITE, BLUE = 2 };
As a more complete and realistic example, consider this enum type declaration that, through the magic of bootstrapping, is actually used by the jEnum translator.
package com.softmoore.jEnum;
/**
* This class encapsulates the symbols (a.k.a. token types)
* of a language token.
*/
enum Symbol {
identifier,
enumRW("Reserved Word: enum"),
abstractRW("Reserved Word: abstract"),
assertRW("Reserved Word: assert"),
booleanRW("Reserved Word: boolean"),
breakRW("Reserved Word: break"),
byteRW("Reserved Word: byte"),
caseRW("Reserved Word: case"),
catchRW("Reserved Word: catch"),
charRW("Reserved Word: char"),
classRW("Reserved Word: class"),
constRW("Reserved Word: const"),
continueRW("Reserved Word: continue"),
defaultRW("Reserved Word: default"),
doRW("Reserved Word: do"),
doubleRW("Reserved Word: double"),
elseRW("Reserved Word: else"),
extendsRW("Reserved Word: extends"),
finalRW("Reserved Word: final"),
finallyRW("Reserved Word: finally"),
floatRW("Reserved Word: float"),
forRW("Reserved Word: for"),
gotoRW("Reserved Word: goto"),
ifRW("Reserved Word: if"),
implementsRW("Reserved Word: implements"),
importRW("Reserved Word: import"),
instanceOfRW("Reserved Word: instanceOf"),
intRW("Reserved Word: int"),
interfaceRW("Reserved Word: interface"),
longRW("Reserved Word: long"),
nativeRW("Reserved Word: native"),
newRW("Reserved Word: new"),
nullRW("Reserved Word: null"),
packageRW("Reserved Word: package"),
privateRW("Reserved Word: private"),
protectedRW("Reserved Word: protected"),
publicRW("Reserved Word: public"),
returnRW("Reserved Word: return"),
shortRW("Reserved Word: short"),
staticRW("Reserved Word: static"),
strictfpRW("Reserved Word: strictfp"),
superRW("Reserved Word: super"),
switchRW("Reserved Word: switch"),
synchronizedRW("Reserved Word: synchronized"),
thisRW("Reserved Word: this"),
throwRW("Reserved Word: throw"),
throwsRW("Reserved Word: throws"),
transientRW("Reserved Word: transient"),
tryRW("Reserved Word: try"),
voidRW("Reserved Word: void"),
volatileRW("Reserved Word: volatile"),
whileRW("Reserved Word: while"),
equals("="),
leftParen("("),
rightParen(")"),
leftBrace("{"),
rightBrace("}"),
comma(","),
semicolon(";"),
period("."),
intLiteral,
stringLiteral,
docComment,
EOF,
unknown
};
The mini-language jEnum translator accompanies this article. If the enum
declaration for Day is defined in a file named
"Day.enum," then the command
$ java -jar jEnum.jar Day.enum
will generate the complete class Day.java shown above for the
enums-as-objects alternative, including standard Javadoc comments for the
methods. You can combine the call to java and its command line
arguments into an executable file (a Unix shell script or a Windows batch
file), so that the translator can be invoked as simply as:
$ jec Day.enum
There are four important points that you should know about using the jEnum
translator. First, the source file name does not have to end in
".enum." Any suffix is accepted as long as the full
file name is supplied.
Second, if the file name does end in the suffix
.enum, then supplying the suffix as part of the
command line is optional. The translator will search first for a file with the
exact name specified on the command line, and then for a file ending in
.enum. The following is also acceptable:
$ java -jar jEnum.jar Day
|
Related Reading
Mac OS X for Java Geeks |
Third, the name of the Java file produced by the translator is based on the
name given in the enum definition, not the name of the source file. In other
words, if the file were named simply "d.enum," the
Java source file would still be named "Day.java." Of
course, good naming conventions recommend naming the source file
"Day.enum."
Fourth, the compiler accepts three flags that allow you to generate the different enum alternatives as follows:
-o generates the enums-as-objects alternative.
(This is the default.)-c generates the enums-as-integer-constants
alternative, implemented as a class.-i generates the enums-as-integer-constants
alternative, implemented as an interface. Additionally, the -c flag not only creates the
class containing the integer constants, but also attempts to provide some of the
functionality of the enums-as-objects approach by defining several public
static methods such as first(), last(),
toString(int n), prev(int n), and next(int
n).
Complete code for the jEnum compiler, plus several examples and Javadoc files for this article, are available in jEnum.zip
Adding type-safe enums is one of the most requested enhancements for Java. See developer.java.sun.com/developer/bugParade/bugs/4401321.html.
Eric Armstrong wrote one of the earliest (and still one of the best) articles describing enums in Java in "Create enumerated constants in Java."
For another excellent reference on typesafe enums in Java, see Chapter 5: "Substitutes for Missing C Constructs" of Joshua Bloch's Effective Java Programming Language Guide (Addison-Wesley, 2001; ISBN: 0201310058); specifically, "Item 21: Replace enum constructs with classes."
Glen McCluskey provides yet another clear explanation of the typesafe enum construct in this Java Developer Connection (JDC) Tech Tip, "Using Enumerations in Java Programming," (August 2001).
James Gosling and Henry McGilton originally advised Java programmers on how to cope with Java's enum absence in "The Java Language Environment" whitepaper (see the "2.2.3 No Enums" section).
"Using Enumerations in Java Programming," Java Developer Connection (JDC) Tech Tips, August 7, 2001.
"Using readResolve," Java Developer Connection (JDC) Tech Tips, February 5, 2002.
"Java Q&A: Constants, I do declare, Readers describe how they employ constants," by Tony Sintes.
"Use constant types for safer and cleaner code," by Thomas E. Davis.
John I. Moore, Jr. is Principal Consultant for SoftMoore Consulting.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.