It was
possible to have one enumerated type extend another; using the language
feature, it is not. This is no accident. For the most part, extensibility of
enums turns out to be a bad idea. It is confusing that elements of an extension
type are instances of the base type and not vice versa. There is no good way to
enumerate over all of the elements of a base type and its extension. Finally,
extensibility would complicate many aspects of the design and implementation.
That said,
there is at least one compelling use case for extensible enumerated types,
which is operation codes, also known as opcodes. An opcode is an enumerated type whose elements represent
operations on some machine, such as the Operation
type
in Item 30, which represents the functions on a simple calculator.
Luckily, there
is a nice way to achieve this effect using enum types. For example, here is an
extensible version of Operation type from Item
30:
// Emulated extensible enum using an interface
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) { return x - y;
}
},
TIMES("*") {
public double apply(double x, double y) { return x * y;
}
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y;
}
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
While the enum
type (BasicOperation) is not
extensible, the interface type (Operation) is, and it
is the interface type that is used to represent operations in APIs. Suppose you
want to define an extension to the operation type above, consisting of the
exponentiation and remainder operations. All you have to do is write an enum
type that implements the Operation interface:
// Emulated extension enum
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
}
You can use
your new operations anywhere you could use the basic operations, provided that
APIs are written to take the interface type (Operation), not the
implementation (BasicOperation).
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T
extends Enum<T> & Operation> void
test(
Class<T> opSet, double x, double y) {
for (Operation op : opSet.getEnumConstants())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
Note that the
class literal for the extended operation type (ExtendedOperation.
class)
is passed from main to test
to
describe the set of extended operations. The class literal serves as a bounded type token (Item 29).
A second
alternative is to use Collection<? extends
Operation>, which is a bounded wildcard
type (Item
28), as the type for the opSet parameter:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet,
double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
The resulting
code is a bit less complex, and the test
method
is a bit more flexible: it allows the caller to combine operations from
multiple implementation types. On the other hand, you forgo the ability to use EnumSet (Item 32) and EnumMap
(Item
33) on the specified operations, so you are probably better off with the
bounded type token unless you need the flexibility to combine operations of
multiple implementation types.
Both programs
above will produce this output when run with command line arguments 2 and 4:
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000
A minor
disadvantage of the use of interfaces to emulate extensible enums is that
implementations cannot be inherited from one enum type to another.
In summary, while you cannot
write an extensible enum type, you can emulate it by writing an interface to go
with a basic enum type that implements the interface. This allows
clients to write their own enums that implement the interface. These enums can
then be used wherever the basic enum type can be used, assuming APIs are
written in terms of the interface.
Reference: Effective Java 2nd Edition by Joshua Bloch