<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>
Enumを使うべきところ、定数グループを使うべきところを知る
話題: Javaプログラミング言語
ああ、Enumは拡張できないんだな。そうか . . .
それが問題か?
私の計画にとっては、そうだ。基底となるEnum、'Test1BaseEnum'、そして、拡張したEnum、'Test1ExtendedEnum1'および'Test1ExtendedEnum2'を以下のように作ろうと思ったんだ。
public enum Test1BaseEnum {
Value1 ("Value1"),
Value2 ("Value2");
String value = null;
Test1BaseEnum (String p_value) {
value = p_value;
}
public String getValue () {
return value;
}
}
public enum Test1ExtendedEnum1 extends Test1BaseEnum {
Value3 ("Value3"),
Value4 ("Value4");
Test1ExtendedEnum1 (String p_value) {
super (p_value)
}
}
public enum Test1ExtendedEnum2 extends Test1BaseEnum {
Value3 ("Value3"),
Value5 ("Value5");
Test1ExtendedEnum2 (String p_value) {
super (p_value)
}
}
しかし、これはうまくいかない。
なぜ?
Enumの継承は禁止されている。
禁止されている理由があると思うが。悪意やけちで禁止したわけではないだろう?
キーワード、'enum'は、そのクラスがEnumクラスを継承することを意味するらしい。多重継承はJavaでは許されないので、別のスーパークラスを指定することはできない。
それはJavaのenumの仕様の問題だ。自分で独自のenumクラスを作ったらどうなる?
うーん、以下のように考えてみよう。
public class Test2BaseEnum {
String value = null;
public static final Test2BaseEnum Value1 = new Test2BaseEnum ("Value1");
public static final Test2BaseEnum Value2 = new Test2BaseEnum ("Value2");
Test2BaseEnum (String p_value) {
value = p_value;
}
public String getValue () {
return value;
}
}
public final class Test2ExtendedEnum1 extends Test2BaseEnum {
public static final Test2ExtendedEnum1 Value3 = new Test2ExtendedEnum1 ("Value3");
public static final Test2ExtendedEnum1 Value4 = new Test2ExtendedEnum1 ("Value4");
private Test2ExtendedEnum1 (String p_value) {
super (p_value);
}
}
public final class Test2ExtendedEnum2 extends Test2BaseEnum {
public static final Test2ExtendedEnum2 Value3 = new Test2ExtendedEnum2 ("Value3");
public static final Test2ExtendedEnum2 Value5 = new Test2ExtendedEnum2 ("Value5");
private Test2ExtendedEnum2 (String p_value) {
super (p_value);
}
}
これはコンパイルが成功する。. . . うーん、しかし、これで、Enumに継承がなぜ許されないが分かった。このコードはうまくいかない。
どううまくいかないのか?
'Test2ExtendedEnum1'の値をあるメソッドの引数として、以下のように受けるとしよう。
public static void testTest2 (Test2ExtendedEnum1 p_enumValue) {
System.out.println (p_enumValue.getValue ());
}
'Test2ExtendedEnum1.Value1'はこのメソッドに渡すことができない。なぜなら、'Test2ExtendedEnum1.Value1'は、'Test2BaseEnum'のインスタンスであって、'Test2ExtendedEnum1'やそのサブクラスのインスタンスではないから。
すると、それがEnumを拡張できない理由なわけだ。
もしも引数を'Test2BaseEnum'にしたら、メソッドに'Test2ExtendedEnum1.Value1'を渡せるだろうが、しかし . . .
. . . それでは意味がないだろう。
enumの本質は、メソッドの引数をあらかじめ定められた値の集合、今の場合には'Test2ExtendedEnum1'に格納された値の集合、に制限することだ。もし引数の型を'Test2BaseEnum'にしたら、'Test2ExtendedEnum2.Value5'を渡すことができるようになるが、それが我々が禁止しなければならないことだ。
その制限がまさに、我々がenumを使おうとする理由だ。
その必要なしに、ただいくつかの定数をリストしたいだけならば、メンバー変数を持ったインターフェースでこと足りる、以下のように。
interface Test3BaseConstantsGroup {
String Value1 = "Value1";
String Value2 = "Value2";
}
interface Test3ExtendedConstantsGroup1 extends Test3BaseConstantsGroup {
String Value3 = "Value3";
String Value4 = "Value4";
}
interface Test3ExtendedConstantsGroup2 extends Test3BaseConstantsGroup {
String Value3 = "Value3";
String Value5 = "Value5";
}
こうすれば、定数グループを好きなように拡張できるし、'getValue'などというメソッドを呼ぶ必要もない。
しかし、テストメソッドは以下のようにならざるをえないだろう。
public static void testTest3 (String p_constant) {
System.out.println (p_constant);
}
どんなStringインスタンスでも渡せてしまう。
もちろん、渡された値をメソッドの中で実行時にチェックすることはできるが、それでは、実行して激しくテストしないと違反を見つけられない。
ソースファイルをスキャンすることでも見つけられるかもしれないが、どちらにしろ、面倒だろう。
enumを使えば、コンパイラがただ違反を検出してくれる。これは大きな違いだ。
それに、メソッドの引数をenumと定義すれば、IDEが何らかの助けを与えてくれるかもしれない。
そう。IDEは、引数に何を渡すべきかを検出して推奨してくれるかもしれない。これは大きな助けになる。
それに、enumには別の恩恵もある。定義された値を列挙できる。
ああ、 . . . しかし、それについては、定数グループにもそういう機能を実装することはできると思う。
コンパイル時に渡される値を制限できて、許可される値のグループの階層を維持できる解決策はないのだろうか?
何か考えがあるか?
うーん、 . . . こういうのはどうか?
public class Test4BaseEnum {
public static final Test4BaseEnumValue Value1 = new Test4BaseEnumValue ("Value1");
public static final Test4BaseEnumValue Value2 = new Test4BaseEnumValue ("Value2");
public static class Test4BaseEnumValue extends Test4ExtendedEnum1.Test4ExtendedEnum1Value {
Test4BaseEnumValue (String p_value) {
super (p_value);
}
}
}
public class Test4ExtendedEnum1 extends Test4BaseEnum {
public static final Test4ExtendedEnum1Value Value3 = new Test4ExtendedEnum1Value ("Value3");
public static final Test4ExtendedEnum1Value Value4 = new Test4ExtendedEnum1Value ("Value4");
public static class Test4ExtendedEnum1Value {
String value = null;
Test4ExtendedEnum1Value (String p_value) {
value = p_value;
}
public String getValue () {
return value;
}
}
}
public static void testTest4 (Test4ExtendedEnum1.Test4ExtendedEnum1Value p_enumValue) {
System.out.println (p_enumValue.getValue ());
}
ああ、逆方向の値クラス階層を作ったわけだ、しかし . . . それはうまくいかない、君にも簡単に分かるように。
簡単に分かる?. . . うーん、どうやって?
'Test4ExtendedEnum2'も作ろうとしてみればよい。
うーん、 . . . 分かった。'Test4BaseEnumValue'は2つのクラスを継承できない。. . . それでは、インターフェースを使ったらどうか、このように?
interface BaseEnumValueInterface {
String getValue ();
}
abstract public class BaseEnumValue implements BaseEnumValueInterface {
String value = null;
BaseEnumValue (String p_value) {
value = p_value;
}
@Override
public String getValue () {
return value;
}
}
public class Test5BaseEnum {
public static final Test5BaseEnumValue Value1 = new Test5BaseEnumValue ("Value1");
public static final Test5BaseEnumValue Value2 = new Test5BaseEnumValue ("Value2");
public interface Test5BaseEnumValueInterface extends Test5ExtendedEnum1.Test5ExtendedEnum1ValueInterface {
}
public static class Test5BaseEnumValue extends BaseEnumValue implements Test5BaseEnumValueInterface {
private Test5BaseEnumValue (String p_value) {
super (p_value);
}
}
}
public class Test5ExtendedEnum1 extends Test5BaseEnum {
public static final Test5ExtendedEnum1Value Value3 = new Test5ExtendedEnum1Value ("Value3");
public static final Test5ExtendedEnum1Value Value4 = new Test5ExtendedEnum1Value ("Value4");
public interface Test5ExtendedEnum1ValueInterface extends BaseEnumValueInterface {
}
public static final class Test5ExtendedEnum1Value extends BaseEnumValue implements Test5ExtendedEnum1ValueInterface {
private Test5ExtendedEnum1Value (String p_value) {
super (p_value);
}
}
}
public static void testTest5 (Test5ExtendedEnum1.Test5ExtendedEnum1ValueInterface p_enumValue) {
System.out.println (p_enumValue.getValue ());
}
ああ、それではだめだ、君にも分かっているとおり。
分かっている?. . . うーん、分かっているようでもない。実際、動作するようだし . . .
コンパイル時にも実行時にもエラーはでないかもしれないが、それは、我々が上で話したenumの本質を実現しているか?
どういう意味だ?
以下のようにできるだろう。
testTest5 (new Test5ExtendedEnum1.Test5ExtendedEnum1ValueInterface () {public String getValue () {return "Well, your restriction is flawed . . .";}});
だめ!それはやるな!それは禁止されている!
「それは禁止されている」と言われても、コード上禁止されていない。
それはやらないで、お願いだから . . .
お願いして問題が解決するのなら、始めからただお願いすれば済んだだろう。
まあ、考えてみると、このコードではIDEの助けも期待できない。'Test5ExtendedEnum1.Test5ExtendedEnum1ValueInterface'は値の選択肢を与えないから。
根本的に、制限が働くには、テストメソッドの引数のタイプはファイナルクラスでなければならない。
さもなければ、引数のクラスを拡張するか引数のインターフェースを実装する勝手なクラスを作ることで制限を回避できてしまう。
実のところ、'test4'はその意味で欠陥があった、たとえ、Javaで多重継承が許されていたとしても。
すると、結論として、スーパークラスの値もサブクラスの値も使うことができない。値は、引数クラスそのもののインスタンスとして定義しなければならない。これは、Javaの現在の仕様に基づいて拡張可能なenumを作ることはできないということを意味しているようだ。
可能な値の制限を強制する必要がないのであれば、ただ定数グループを使えばよい。値を列挙する必要があるのであれば、我々は列挙機能を実装しよう(将来の記事でそうする)。そうすれば、そうしたグループのどんな階層構造も構築できる。
可能な値の制限を強制する必要があるのであれば、enumを使わざるをえない。その場合は、値がenum間で共有されているのであれば、値の定義をいくつかのenumへコピーしなければならないだろう。そうした無作法な定義コピーを無くしたい場合、Java内でそうすることはできず、JavaのEnumを作るプリプロセッサを作る必要があるだろう。Enumの階層構造を定義するXMLファイルからEnumのJavaソース群を作るプリプロセッサを作るのは難しいことではないだろう。