Sunday, January 01, 2006

More enum reinvention in Java

I posted earlier on reinventing the enum in Java for pre-JDK5 code. But I am also annoyed by one limitation in JDK5 enums, than they are not extendable at runtime or otherwise. So I copied the Enum class from the JDK and modified it to support non-final subclasses.

An example class and its extension from the unit tests, also demonstrating the abstract feature present in the JDK enum:

  1     private abstract static class TestEnumA
  2             extends ExtensibleEnum<TestEnumA> {
  3         public static final TestEnumA A = new TestEnumA("A") {
  4             void foo() {
  5             }
  6         };
  7         public static final TestEnumA B = new TestEnumA("B") {
  8             void foo() {
  9             }
 10         };
 11 
 12         static {
 13             new TestEnumA("C") {
 14                 void foo() {
 15                 }
 16             };
 17         }
 18 
 19         abstract void foo();
 20 
 21         public static TestEnumA[] values() {
 22             return ExtensibleEnum.values(TestEnumA.class);
 23         }
 24 
 25         public static TestEnumA valueOf(final String name) {
 26             return ExtensibleEnum.valueOf(TestEnumA.class, name);
 27         }
 28 
 29         protected TestEnumA(final String name) {
 30             super(name);
 31         }
 32     }
 33 
 34     private static abstract class TestEnumB
 35             extends TestEnumA {
 36         public static final TestEnumB D = new TestEnumB("D") {
 37             void foo() {
 38             }
 39         };
 40 
 41         public static TestEnumB[] values() {
 42             return ExtensibleEnum.values(TestEnumB.class);
 43         }
 44 
 45         public static TestEnumB valueOf(final String name) {
 46             return ExtensibleEnum.valueOf(TestEnumB.class, name);
 47         }
 48 
 49         protected TestEnumB(final String name) {
 50             super(name);
 51         }
 52     }
 53 }

Note that if you ask for values() from TestEnumA, they include TestEnumB.D—a TestEnumA by subclassing.

Here is the base class. I am also trying out the "Code as HTML" plugin for IDEA—I hope the colorizing is not too much:

  1 /*
  2  * Copyright (c) 2006 B. K. Oxley (binkley) <binkley@alumni.rice.edu> under the
  3  * terms of the Artistic License, including Clause 8.  See
  4  * http://www.opensource.org/licenses/artistic-license.php.
  5  */
  6 
  7 import java.io.Serializable;
  8 import java.lang.reflect.Array;
  9 import java.util.ArrayList;
 10 import java.util.EnumMap;
 11 import java.util.HashMap;
 12 import java.util.List;
 13 import java.util.Map;
 14 
 15 /**
 16  * {@code ExtensibleEnum} <strong>needs documentation</strong>.
 17  *
 18  * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
 19  * @version $Id$
 20  * @todo {@code RuntimeExtensibleEnum} needs documentation
 21  * @noinspection FinalizeDoesntCallSuperFinalize
 22  * @since Dec 31, 2005 8:38:03 AM
 23  */
 24 public abstract class ExtensibleEnum<E extends ExtensibleEnum<E>>
 25         implements Comparable<E>, Serializable {
 26     private static final Map<Class<? extends ExtensibleEnum<?>>,
 27             Map<String, ? extends ExtensibleEnum<?>>> BY_NAME
 28             = new HashMap<Class<? extends ExtensibleEnum<?>>,
 29             Map<String, ? extends ExtensibleEnum<?>>>(4);
 30     private static final Map<Class<? extends ExtensibleEnum<?>>,
 31             List<? extends ExtensibleEnum<?>>> BY_ORDINAL
 32             = new HashMap<Class<? extends ExtensibleEnum<?>>,
 33             List<? extends ExtensibleEnum<?>>>(4);
 34     /**
 35      * The JDK5 enum has ordinals in stepwise order; this implementation relaxes
 36      * the constraint to simply monotonically increasing.
 37      *
 38      * @noinspection StaticNonFinalField
 39      */
 40     private static int ORDINAL;
 41 
 42     /**
 43      * The name of this enum constant, as declared in the enum declaration. Most
 44      * programmers should use the {@link #toString} method rather than accessing
 45      * this field.
 46      */
 47     private final String name;
 48 
 49     /**
 50      * Returns the name of this enum constant, exactly as declared in its enum
 51      * declaration.
 52      * <p/>
 53      * <b>Most programmers should use the {@link #toString} method in preference
 54      * to this one, as the toString method may return a more user-friendly
 55      * name.</b>  This method is designed primarily for use in specialized
 56      * situations where correctness depends on getting the exact name, which
 57      * will not vary from release to release.
 58      *
 59      * @return the name of this enum constant
 60      */
 61     public final String name() {
 62         return name;
 63     }
 64 
 65     /**
 66      * The ordinal of this enumeration constant (its position in the enum
 67      * declaration, where the initial constant is assigned an ordinal of zero).
 68      * <p/>
 69      * Most programmers will have no use for this field.  It is designed for use
 70      * by sophisticated enum-based data structures, such as {@link
 71      * java.util.EnumSet} and {@link EnumMap}.
 72      */
 73     private final int ordinal = ORDINAL++;
 74 
 75     /**
 76      * Returns the ordinal of this enumeration constant (its position in its
 77      * enum declaration, where the initial constant is assigned an ordinal of
 78      * zero).
 79      * <p/>
 80      * Most programmers will have no use for this method.  It is designed for
 81      * use by sophisticated enum-based data structures, such as {@link
 82      * java.util.EnumSet} and {@link EnumMap}.
 83      *
 84      * @return the ordinal of this enumeration constant
 85      */
 86     public final int ordinal() {
 87         return ordinal;
 88     }
 89 
 90     /**
 91      * Sole constructor.  Programmers cannot invoke this constructor. It is for
 92      * use by code emitted by the compiler in response to enum type
 93      * declarations.
 94      *
 95      * @param name - The name of this enum constant, which is the identifier
 96      * used to declare it.
 97      */
 98     protected ExtensibleEnum(final String name) {
 99         if (null == name)
100             throw new NullPointerException("Missing name");
101 
102         this.name = name;
103 
104         new EnumInstall(getDeclaringClass(), name, this).install();
105     }
106 
107     private static class EnumInstall<T extends ExtensibleEnum<T>, U extends T> {
108         private final Class<T> enumType;
109         private final String name;
110         private final U instance;
111 
112         EnumInstall(final Class<T> enumType, final String name,
113                 final U instance) {
114             this.enumType = enumType;
115             this.name = name;
116             this.instance = instance;
117         }
118 
119         void install() {
120             if (!ExtensibleEnum.class.isAssignableFrom(enumType))
121                 return;
122 
123             final EnumInstall<? super T, ? super U> parentInstall
124                     = new EnumInstall(enumType.getSuperclass(), name, instance);
125 
126             parentInstall.install();
127 
128             try {
129                 putEnum(enumType, name, instance);
130 
131             } catch (final IllegalArgumentException e) {
132                 parentInstall.uninstall();
133 
134                 throw e;
135             }
136         }
137 
138         void uninstall() {
139             unputEnum(enumType, name);
140         }
141     }
142 
143     private static <T extends ExtensibleEnum<T>, U extends T> void putEnum(
144             final Class<T> enumType, final String name, final U instance) {
145         if (!BY_NAME.containsKey(enumType)) {
146             BY_NAME.put(enumType, new HashMap<String, T>(4));
147             BY_ORDINAL.put(enumType, new ArrayList<T>(4));
148         }
149 
150         final Map<String, T> nameMap = byName(enumType);
151 
152         if (nameMap.containsKey(name))
153             throw new IllegalArgumentException("Duplicate name: " + name);
154 
155         nameMap.put(name, instance);
156         byOrdinal(enumType).add(instance);
157     }
158 
159     private static <T extends ExtensibleEnum<T>> void unputEnum(
160             final Class<T> enumType, final String name) {
161         if (!BY_NAME.containsKey(enumType))
162             return;
163 
164         byName(enumType).remove(name);
165 
166         final List<T> list = byOrdinal(enumType);
167 
168         list.remove(list.size() - 1);
169     }
170 
171     /**
172      * Returns the name of this enum constant, as contained in the declaration.
173      * This method may be overridden, though it typically isn't necessary or
174      * desirable.  An enum type should override this method when a more
175      * "programmer-friendly" string form exists.
176      *
177      * @return the name of this enum constant
178      */
179     @Override
180     public String toString() {
181         return name;
182     }
183 
184     /**
185      * Returns true if the specified object is equal to this enum constant.
186      *
187      * @param other the object to be compared for equality with this object.
188      *
189      * @return true if the specified object is equal to this enum constant.
190      */
191     @Override
192     public final boolean equals(final Object other) {
193         return this == other;
194     }
195 
196     /**
197      * Returns a hash code for this enum constant.
198      *
199      * @return a hash code for this enum constant.
200      */
201     @Override
202     public final int hashCode() {
203         return System.identityHashCode(this);
204     }
205 
206     /**
207      * Throws CloneNotSupportedException.  This guarantees that enums are never
208      * cloned, which is necessary to preserve their "singleton" status.
209      *
210      * @return (never returns)
211      */
212     @Override
213     protected final Object clone()
214             throws CloneNotSupportedException {
215         throw new CloneNotSupportedException();
216     }
217 
218     /**
219      * Compares this enum with the specified object for order.  Returns a
220      * negative integer, zero, or a positive integer as this object is less
221      * than, equal to, or greater than the specified object.
222      * <p/>
223      * ExtensibleEnum constants are only comparable to other enum constants of
224      * the same enum type.  The natural order implemented by this method is the
225      * order in which the constants are declared.
226      *
227      * @noinspection ObjectEquality
228      */
229     public final int compareTo(final E o) {
230         if (getClass() != o.getClass() // optimization
231                 && getDeclaringClass() != o.getDeclaringClass())
232             throw new ClassCastException();
233 
234         return new Integer(ordinal).compareTo(o.ordinal);
235     }
236 
237     public static <T extends ExtensibleEnum> T[] values(
238             final Class<T> enumType) {
239         final List<T> values = byOrdinal(enumType);
240 
241         return values.toArray((T[]) Array.newInstance(enumType, values.size()));
242     }
243 
244     /**
245      * Returns the enum constant of the specified enum type with the specified
246      * name.  The name must match exactly an identifier used to declare an enum
247      * constant in this type.  (Extraneous whitespace characters are not
248      * permitted.)
249      *
250      * @param enumType the <tt>Class</tt> object of the enum type from which to
251      * return a constant
252      * @param name the name of the constant to return
253      *
254      * @return the enum constant of the specified enum type with the specified
255      *         name
256      *
257      * @throws IllegalArgumentException if the specified enum type has no
258      * constant with the specified name, or the specified class object does not
259      * represent an enum type
260      * @throws NullPointerException if <tt>enumType</tt> or <tt>name</tt> is
261      * null
262      * @since 1.5
263      */
264     public static <T extends ExtensibleEnum> T valueOf(final Class<T> enumType,
265             final String name) {
266         // IDEA says cast is useless; JDK5 javac says it is necessary
267         //noinspection RedundantCast
268         final T result = (T) byName(enumType).get(name);
269 
270         if (null != result)
271             return result;
272         if (null == name)
273             throw new NullPointerException("Name is null");
274 
275         throw new IllegalArgumentException(
276                 "No enum const " + enumType + '.' + name);
277     }
278 
279     /**
280      * enum classes cannot have finalize methods.
281      */
282     @Override
283     protected final void finalize() { }
284 
285     protected Class<?> getDeclaringClass() {
286         final Class<?> enumType = getClass();
287 
288         return enumType.isAnonymousClass()
289                 ? enumType.getSuperclass()
290                 : enumType;
291     }
292 
293     private static <T extends ExtensibleEnum<T>> Map<String, T> byName(
294             final Class<T> enumType) {
295         return (Map<String, T>) BY_NAME.get(enumType);
296     }
297 
298     private static <T extends ExtensibleEnum<T>> List<T> byOrdinal(
299             final Class<T> enumType) {
300         return (List<T>) BY_ORDINAL.get(enumType);
301     }
302 }

Happy New Year!

No comments: