Wednesday, December 14, 2005

Reinventing the enum in Java

I'm working on a Java project which uses JDK 1.4.2, so I do not have the useful enum keyword available to me. What to do? Reinvent the enum, of course. I try following the pattern set by JDK5, suitable for JDK 1.4.2. Merry Christmas:

/**
 * <code>AbstractEnum</code> models <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Enum.html">the
 * JDK5 <code>enum</code> feature</a>.
 * <p/>
 * Extending classes should follow this example: <pre>
 * public class Suit
 *         extends AbstractEnum {
 *     public static final Suit CLUBS = new Suit("CLUBS");
 *     public static final Suit DIAMONDS = new Suit("DIAMONDS");
 *     public static final Suit HEARTS = new Suit("HEARTS");
 *     public static final Suit SPADES = new Suit("SPADES");
 *
 *     public static Suit[] values() {
 *          return (Suit[]) values0(Suit.class, new Suit[count(Suit.class)]);
 *     }
 *
 *     public static Suit valueOf(final String name) {
 *         return (Suit) valueOf0(Suit.class, name);
 *     }
 *
 *     private Suit(final String name) {
 *         super(name);
 *     }
 * }</pre>
 *
 * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
 * @version $Id$
 * @since Dec 14, 2005 6:32:23 PM
 */
public abstract class AbstractEnum
        implements Comparable {
    private static final Map ALL_VALUES_BY_NAME = new HashMap(4);
    private static final Map ALL_VALUES_BY_ORDINAL = new HashMap(4);
    private static final Map ALL_COUNTS = new HashMap(4);

    /**
     * The ordinal of this enumeration constant (its position in the enum
     * declaration, where the initial constant is assigned an ordinal of zero).
     * <p/>
     * Most programmers will have no use for this field.  It is designed for use
     * by sophisticated enum-based data structures.
     */
    private final int ordinal = createNextOrdinal(getClass());

    /**
     * The name of this enum constant, as declared in the enum declaration. Most
     * programmers should use the {@link #toString} method rather than accessing
     * this field.
     */
    private final String name;

    /**
     * Gets all enum values of type <var>clazz</var> into <var>array</var>.  Use
     * {@link #count(Class)} to size the array parameter.
     * <p/>
     * Concrete subclasses <em>should</em> provide <code>values()</code> which
     * follows the example:
     * <pre>
     * public static Suit[] values() {
     *     return (Suit[]) valuesOf(Suit.class, new Suit[count(Suit.class)]);
     * }</pre>
     *
     * @param clazz the enum type
     * @param array the array
     *
     * @return the enum values
     *
     * @throws ClassCastException if <var>clazz</var> is not an enum class
     */
    protected static AbstractEnum[] values0(final Class clazz,
            final AbstractEnum[] array) {
        checkClassIsEnum(clazz);

        int i = 0;

        for (final Iterator valueIt = getValuesByOrdinal(clazz).iterator();
                valueIt.hasNext(); ++i)
            array[i] = (AbstractEnum) valueIt.next();

        return array;
    }

    /**
     * Gets the count of enums of type <var>clazz</var>, zero (0) if none.
     *
     * @param clazz the enum type
     *
     * @return the enum count
     *
     * @throws ClassCastException if <var>clazz</var> is not an enum class
     */
    protected static int count(final Class clazz) {
        checkClassIsEnum(clazz);

        return ALL_COUNTS.containsKey(clazz)
                ? ((Integer) ALL_COUNTS.get(clazz)).intValue()
                : 0;
    }

    /**
     * Gets an enum with the given <var>name</var>.
     * <p/>
     * Concrete subclasses <em>should</em> provide <code>valuesOf(String)</code>
     * which follows the example:
     * <pre>
     * public static Suit valueOf(final String name) {
     *     return (Suit) valueOf(Suit.class, name);
     * }</pre>
     *
     * @param clazz the enum class
     * @param name the enum name
     *
     * @return the corresponding enum value
     *
     * @throws ClassCastException if <var>clazz</var> is not an enum class
     * @throws IllegalArgumentException if <var>name</var> is not an enum name
     */
    protected static AbstractEnum valueOf0(final Class clazz,
            final String name)
            throws IllegalArgumentException {
        checkClassIsEnum(clazz);

        final Map values = getValuesByName(clazz);

        if (values.containsKey(name))
            return (AbstractEnum) values.get(name);

        throw new IllegalArgumentException(name);
    }

    /**
     * Constructs a new <code>AbstractEnum</code> with the given
     * <var>name</var>.
     *
     * @param name the name of this enum constant, which is the identifier used
     * to declare it
     */
    protected AbstractEnum(final String name) {
        this.name = name;

        getValuesByName(getClass()).put(name, this);
        getValuesByOrdinal(getClass()).add(this);
    }

    /**
     * Returns the name of this enum constant, exactly as declared in its enum
     * declaration.
     * <p/>
     * <b>Most programmers should use the {@link #toString} method in preference
     * to this one, as the toString method may return a more user-friendly
     * name.</b>  This method is designed primarily for use in specialized
     * situations where correctness depends on getting the exact name, which
     * will not vary from release to release.
     *
     * @return the name of this enum constant
     */
    public String name() {
        return name;
    }

    /**
     * Returns the ordinal of this enumeration constant (its position in its
     * enum declaration, where the initial constant is assigned an ordinal of
     * zero).
     * <p/>
     * Most programmers will have no use for this method.  It is designed for
     * use by sophisticated enum-based data structures.
     *
     * @return the ordinal of this enumeration constant
     */
    public int ordinal() {
        return ordinal;
    }

    // Object

    /**
     * Throws CloneNotSupportedException.  This guarantees that enums are never
     * cloned, which is necessary to preserve their "singleton" status.
     *
     * @return (never returns)
     *
     * @throws CloneNotSupportedException if called
     */
    protected Object clone()
            throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

    /**
     * Returns the name of this enum constant, as contained in the declaration.
     * This method may be overridden, though it typically isn't necessary or
     * desirable.  An enum type should override this method when a more
     * "programmer-friendly" string form exists.
     *
     * @return the name of this enum constant
     */
    public String toString() {
        return name;
    }

    // Comparable

    /**
     * Compares this enum with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.
     * <p/>
     * Enum constants are only comparable to other enum constants of the same
     * enum type.  The natural order implemented by this method is the order in
     * which the constants are declared.
     */
    public int compareTo(final Object o) {
        if (!getClass().equals(o.getClass()))
            throw new ClassCastException(o.getClass().toString());
        return ordinal - ((AbstractEnum) o).ordinal;
    }

    private static Map getValuesByName(final Class clazz) {
        final Map values;

        if (ALL_VALUES_BY_NAME.containsKey(clazz))
            values = (Map) ALL_VALUES_BY_NAME.get(clazz);
        else
            ALL_VALUES_BY_NAME.put(clazz, values = new HashMap(8));

        return values;
    }

    private static SortedSet getValuesByOrdinal(final Class clazz) {
        final SortedSet values;

        if (ALL_VALUES_BY_ORDINAL.containsKey(clazz))
            values = (SortedSet) ALL_VALUES_BY_ORDINAL.get(clazz);
        else
            ALL_VALUES_BY_ORDINAL.put(clazz, values = new TreeSet());

        return values;
    }

    private static int createNextOrdinal(final Class clazz) {
        final int count = count(clazz);

        ALL_COUNTS.put(clazz, new Integer(count + 1));

        return count;
    }

    private static void checkClassIsEnum(final Class clazz) {
        if (!AbstractEnum.class.isAssignableFrom(clazz))
            throw new ClassCastException(clazz.toString());
    }
}

5 comments:

Brian Oxley said...

Hey, Paul!

I did not know about it -- thanks for the pointer.

Doug said...

The best solution is retroweaver. Actually retroweaver-ng, the original retroweaver guy stopped doing any work on it. It allows you to do java 1.5 stuff in java 1.4.

Anonymous said...

I guess Retrotranslator (http://retrotranslator.sourceforge.net/) is even better. It has support for _runtime_ annotation handling and provides a larger list of transformed classes/methods. I think a new release based on ASM 3.0_beta with some bug fixes is planned for the near future.

Brian Oxley said...

Thanks for the pointers!

Anonymous said...

OR......

public class DemoEnum
{
public static final int enumval0 = 0;
public static final int enumval1 = 1;
public static final int enumval2 = 2;
}


Nothing like unnecessary code!