38.拡張可能なenumをインターフェースで模倣すべし
たいていの場合、列挙型を拡張するような考えはよくないことであるが、operation code
を書くときだけは切実に必要となる。
これをenumを使って実現する良い方法がある。enumはインターフェースを実装できるので、例えば以下のようにできる。
// 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;
}
}
enum型は拡張不可であるが、インターフェースは拡張可能であり、オペレーションのAPIとして使われるのはインターフェースである。このインターフェースを使用して、以下のように新しくoperation codeを定義することもできる。
// 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;
}
}
基盤となるenumのインスタンスが期待されている場所であれば、拡張enumのインスタンスは使用できる。それだけでなく、基盤enumの全要素を渡すような場面でも、代わりに拡張enumの全要素を渡すことができる。以下のソースコードでは、引数に取った2つの値のExtendedOperation
による全演算の結果を表示する。
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> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
このコードにおいては、testメソッドの第一引数にExtendedOperation.class
が取られている。これは境界付きの型トークン(Item33)である。
<T extends Enum<T> & Operation>
とあるが、これによってtestの第一引数に与えられるクラスは、enumであり、かつ、Operation
のサブクラスであることが保証される。
境界付きワイルドカード型(Item31)を使って、以下のように記述することもできる。
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%s", x, op, y, op.apply(x, y));
}
}
このコードは少し簡潔で柔軟性が増しているが、EnumSet
(Item36)とEnumMap
(Item37)の使用はあきらめなければならない。
上記2つのコードは、引数に4 2
を与えたら、以下のように出力する。
4.000000 ^ 2.000000 = 16.000000
4.000000 % 2.000000 = 0.000000
この拡張enumのマイナーな欠点は、オペレーションに紐づくシンボルを保存するコードと、検索するコードはBasicOperation
とExtendedOperation
の双方で保持しなければならない点である。あまりに多くの共通機能が出てきた場合には、ヘルパークラスなどを用意して、コードの重複を除く必要がある。
この章で語られたパターンはjava.nio.file.LinkOption
でも使われている。