ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Enums in Java (One More Time)

by John I. Moore, Jr.
04/23/2003

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.

Enumerated Types

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
By David Jordan, Craig Russell

Different programming languages take slightly different approaches, but the most desirable characteristics of an enumerated type facility in a programming language are:

  1. Type safety.
  2. Compact, efficient declaration of enumerated values.
  3. Seamless integration with other language features.
  4. Runtime efficiency.

Let's consider these characteristics one at a time.

1. Type Safety

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.

2. Compact, Efficient Declaration of Enumerated Values

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.

3. Seamless Integration with Other Language Features

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.

4. Runtime Efficiency

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 Alternatives for Enumerated Types

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.

A Mini-Language for Enums

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 };

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
By Will Iverson

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:

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).

Resources

John I. Moore, Jr. is Principal Consultant for SoftMoore Consulting.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.