2017年9月16日土曜日

1: Enumを使うべきところと定数グループを使うべきところ

<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>

本文 START

Enumを使うべきところ、定数グループを使うべきところを知る

話題: Javaプログラミング言語

Enumは拡張できない、これがその理由

-Hypothesizer

ああ、Enumは拡張できないんだな。そうか . . .

-Rebutter

それが問題か?

-Hypothesizer

私の計画にとっては、そうだ。基底となるEnum、'Test1BaseEnum'、そして、拡張したEnum、'Test1ExtendedEnum1'および'Test1ExtendedEnum2'を以下のように作ろうと思ったんだ。

@Java Source Code
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)
 }
}

しかし、これはうまくいかない。

-Rebutter

なぜ?

-Hypothesizer

Enumの継承は禁止されている。

-Rebutter

禁止されている理由があると思うが。悪意やけちで禁止したわけではないだろう?

-Hypothesizer

キーワード、'enum'は、そのクラスがEnumクラスを継承することを意味するらしい。多重継承はJavaでは許されないので、別のスーパークラスを指定することはできない。

-Rebutter

それはJavaのenumの仕様の問題だ。自分で独自のenumクラスを作ったらどうなる?

-Hypothesizer

うーん、以下のように考えてみよう。

@Java Source Code
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に継承がなぜ許されないが分かった。このコードはうまくいかない。

-Rebutter

どううまくいかないのか?

-Hypothesizer

'Test2ExtendedEnum1'の値をあるメソッドの引数として、以下のように受けるとしよう。

@Java Source Code
 public static void testTest2 (Test2ExtendedEnum1 p_enumValue) {
  System.out.println (p_enumValue.getValue ());
 }

'Test2ExtendedEnum1.Value1'はこのメソッドに渡すことができない。なぜなら、'Test2ExtendedEnum1.Value1'は、'Test2BaseEnum'のインスタンスであって、'Test2ExtendedEnum1'やそのサブクラスのインスタンスではないから。

-Rebutter

すると、それがEnumを拡張できない理由なわけだ。

Enumの本質

-Rebutter

もしも引数を'Test2BaseEnum'にしたら、メソッドに'Test2ExtendedEnum1.Value1'を渡せるだろうが、しかし . . .

-Hypothesizer

. . . それでは意味がないだろう。

enumの本質は、メソッドの引数をあらかじめ定められた値の集合、今の場合には'Test2ExtendedEnum1'に格納された値の集合、に制限することだ。もし引数の型を'Test2BaseEnum'にしたら、'Test2ExtendedEnum2.Value5'を渡すことができるようになるが、それが我々が禁止しなければならないことだ。

-Rebutter

その制限がまさに、我々がenumを使おうとする理由だ。

-Hypothesizer

その必要なしに、ただいくつかの定数をリストしたいだけならば、メンバー変数を持ったインターフェースでこと足りる、以下のように。

@Java Source Code
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'などというメソッドを呼ぶ必要もない。

しかし、テストメソッドは以下のようにならざるをえないだろう。

@Java Source Code
 public static void testTest3 (String p_constant) {
  System.out.println (p_constant);
 }
-Rebutter

どんなStringインスタンスでも渡せてしまう。

-Hypothesizer

もちろん、渡された値をメソッドの中で実行時にチェックすることはできるが、それでは、実行して激しくテストしないと違反を見つけられない。

-Rebutter

ソースファイルをスキャンすることでも見つけられるかもしれないが、どちらにしろ、面倒だろう。

-Hypothesizer

enumを使えば、コンパイラがただ違反を検出してくれる。これは大きな違いだ。

-Rebutter

それに、メソッドの引数をenumと定義すれば、IDEが何らかの助けを与えてくれるかもしれない。

-Hypothesizer

そう。IDEは、引数に何を渡すべきかを検出して推奨してくれるかもしれない。これは大きな助けになる。

-Rebutter

それに、enumには別の恩恵もある。定義された値を列挙できる。

-Hypothesizer

ああ、 . . . しかし、それについては、定数グループにもそういう機能を実装することはできると思う。

完全な解決策はないのか?

-Hypothesizer

コンパイル時に渡される値を制限できて、許可される値のグループの階層を維持できる解決策はないのだろうか?

-Rebutter

何か考えがあるか?

-Hypothesizer

うーん、 . . . こういうのはどうか?

@Java Source Code
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 ());
 }
-Rebutter

ああ、逆方向の値クラス階層を作ったわけだ、しかし . . . それはうまくいかない、君にも簡単に分かるように。

-Hypothesizer

簡単に分かる?. . . うーん、どうやって?

-Rebutter

'Test4ExtendedEnum2'も作ろうとしてみればよい。

-Hypothesizer

うーん、 . . . 分かった。'Test4BaseEnumValue'は2つのクラスを継承できない。. . . それでは、インターフェースを使ったらどうか、このように?

@Java Source Code
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 ());
 }
-Rebutter

ああ、それではだめだ、君にも分かっているとおり。

-Hypothesizer

分かっている?. . . うーん、分かっているようでもない。実際、動作するようだし . . .

-Rebutter

コンパイル時にも実行時にもエラーはでないかもしれないが、それは、我々が上で話したenumの本質を実現しているか?

-Hypothesizer

どういう意味だ?

-Rebutter

以下のようにできるだろう。

@Java Source Code
  testTest5 (new Test5ExtendedEnum1.Test5ExtendedEnum1ValueInterface () {public String getValue () {return "Well, your restriction is flawed . . .";}});
-Hypothesizer

だめ!それはやるな!それは禁止されている!

-Rebutter

「それは禁止されている」と言われても、コード上禁止されていない。

-Hypothesizer

それはやらないで、お願いだから . . .

-Rebutter

お願いして問題が解決するのなら、始めからただお願いすれば済んだだろう。

-Hypothesizer

まあ、考えてみると、このコードではIDEの助けも期待できない。'Test5ExtendedEnum1.Test5ExtendedEnum1ValueInterface'は値の選択肢を与えないから。

-Rebutter

根本的に、制限が働くには、テストメソッドの引数のタイプはファイナルクラスでなければならない。

-Hypothesizer

さもなければ、引数のクラスを拡張するか引数のインターフェースを実装する勝手なクラスを作ることで制限を回避できてしまう。

-Rebutter

実のところ、'test4'はその意味で欠陥があった、たとえ、Javaで多重継承が許されていたとしても。

-Hypothesizer

すると、結論として、スーパークラスの値もサブクラスの値も使うことができない。値は、引数クラスそのもののインスタンスとして定義しなければならない。これは、Javaの現在の仕様に基づいて拡張可能なenumを作ることはできないということを意味しているようだ。

結論

-Hypothesizer

可能な値の制限を強制する必要がないのであれば、ただ定数グループを使えばよい。値を列挙する必要があるのであれば、我々は列挙機能を実装しよう(将来の記事でそうする)。そうすれば、そうしたグループのどんな階層構造も構築できる。

可能な値の制限を強制する必要があるのであれば、enumを使わざるをえない。その場合は、値がenum間で共有されているのであれば、値の定義をいくつかのenumへコピーしなければならないだろう。そうした無作法な定義コピーを無くしたい場合、Java内でそうすることはできず、JavaのEnumを作るプリプロセッサを作る必要があるだろう。Enumの階層構造を定義するXMLファイルからEnumのJavaソース群を作るプリプロセッサを作るのは難しいことではないだろう。

本文 END

<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>