2017年12月10日日曜日

5: 定数グループの定数群の名前や値を列挙可能にする方法

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

本文 START

定数群の名前や値が列挙可能でなければならない定数グループのベースクラスを作ろう

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

なぜただEnumを使わないのか

-Hypothesizer

以前の記事で我々はenumの制限について話した。

-Rebutter

enumは拡張することができない、それがJavaのEnumであろうが、自ら作ったenumであろうが、enumの本質を損なうことなしには。

-Hypothesizer

enumの本質は、少なくとも我々にとっては、メソッドの引数をあらかじめ定められた値の集合に制限することだ。その制限が要らないのであれば、ただ定数グループを使えばよいだけだ。そうすれば、それらを好きなように拡張できる。

-Rebutter

我々が「定数グループ」と呼んでいるのは、典型的には、以下のようなインターフェースのことだ。

@Java Source Code
public interface AConstantsGroup {
 String c_value1 = "Value1";
 String c_value2 = "Value2";
}

-Hypothesizer

そう。クラスにすることもできるが、そうしたら、多重継承が使えないだろう。だから、特に理由がなければ、我々はそれをインターフェースにする。

-Rebutter

ははあ。

-Hypothesizer

enumを受け取るメソッドを作る場合、確かに、enumは便利だ。引数のタイプが特定(ただのStringとかではない)だから、引数に何を渡せるかが明確であり、テストしなくても、あらかじめ定められた値だけが渡されることが保証される。

しかし、値を、既存のメソッド(例えば、JDKの標準ライブラリのメソッド)の、例えば、String引数に渡そうとする場合、enumを使うメリットがあまりない。

-Rebutter

そのメソッドを多くの場所で呼ぶ際に、リテラルを散乱させたくない場合のことを君は言っているのだろう。

-Hypothesizer

メソッドそのものは、引数を可能な値の有限数のオプションに制限しないのだが、我々のプログラムは、値の有限数の集合のみを使い、その集合を、我々は定数グループとして管理したい。

-Rebutter

オーケー。

-Hypothesizer

そうした場合にenumを使うと、それを拡張できないというハンディキャップを、必要もなく負うことになってしまう。実際、場合により、我々はそれを拡張したいのだ。

-Rebutter

すると、そうした場合に、我々は、定数グループを使うわけだ。

-Hypothesizer

しかしながら、場合により、定数グループに含まれる定数群の値や名前を列挙したいということがある。

-Rebutter

それは、上記のような単純なインターフェースではできない。

-Hypothesizer

そこで、ここでは、それを可能にしよう。

我々のデザインはどのようなものか?

-Hypothesizer

ベース抽象クラスを作り、それを拡張するクラスに含まれる定数群の名前と値を収集する機能を持たせる。

実際には、'abstract public class BaseEnumerableConstantsGroup <T>'というクラスを(パッケージ、'thebiasplanet.coreutilities.constantsgroups'に)作る。

-Rebutter

'T'は何を表わすのか?

-Hypothesizer

定数値群のクラスだ。

-Rebutter

なるほど。

-Hypothesizer

このベースクラスを以下のように使う。

@Java Source Code
package test.enumerableconstantsgrouptest1;

public interface Test1ConstantsGroup1 {
 String c_value1 = "Value1";
 String c_value2 = "Value2";
}

package test.enumerableconstantsgrouptest1;

import thebiasplanet.coreutilities.constantsgroups.BaseEnumerableConstantsGroup;

public final class Test1EnumerableConstantsGroup1 extends BaseEnumerableConstantsGroup <String> implements Test1ConstantsGroup1 {
 public static final Test1EnumerableConstantsGroup1 c_instance = new Test1EnumerableConstantsGroup1 ();
 
 private Test1EnumerableConstantsGroup1 () {
 }
}

-Rebutter

定数グループ、'Test1ConstantsGroup1'を通常どおりインターフェースとして作り、この定数グループを実装した、先ほどのベースクラスの具象クラスとして、列挙可能な定数グループを作るわけだ。

-Hypothesizer

そうだ。列挙可能定数グループは拡張せず、インターフェースである定数グループを拡張し、それら定数グループの列挙可能バージョンを作ることにする。そうすれば、多重継承が使える。

以下が例だ。

@Java Source Code
package test.enumerableconstantsgrouptest1;

public interface Test1ConstantsGroup2 {
 String c_value3 = "Value3";
 String c_value4 = "Value4";
}

package test.enumerableconstantsgrouptest1;

public interface Test1ExtendedConstantsGroup12 extends Test1ConstantsGroup1, Test1ConstantsGroup2 {
 String c_value1 = "Value1 overwritten";
 String c_value4 = "Value4 overwritten";
 String c_value5 = "Value5";
 String c_value6 = "Value6";
}

package test.enumerableconstantsgrouptest1;

import thebiasplanet.coreutilities.constantsgroups.BaseEnumerableConstantsGroup;

public final class Test1EnumerableExtendedConstantsGroup12 extends BaseEnumerableConstantsGroup <String> implements Test1ExtendedConstantsGroup12 {
 public static final Test1EnumerableExtendedConstantsGroup12 c_instance = new Test1EnumerableExtendedConstantsGroup12 ();
 
 private Test1EnumerableExtendedConstantsGroup12 () {
 }
}

-Rebutter

ふーむ、具象クラスがしないといけないのは、抽象クラスを拡張し、インターフェースを実装し、シングルトンインスタンスを定義し、コンストラクタをプライベートにすることか。

-Hypothesizer

そうだ。たいした負担でなければよいが。

-Rebutter

定数群の名前や値はどうやって得るのか?

-Hypothesizer

以下のようにして得られる。

@Java Source Code
  LinkedHashSet <String> l_names = Test1EnumerableExtendedConstantsGroup12.c_instance.getNames ();
  ArrayList <String> l_values = Test1EnumerableExtendedConstantsGroup12.c_instance.getValues ();

我々の実装はどのようなものか?

-Rebutter

どのように実装する?

-Hypothesizer

ベースクラスには、定数群の名前と値を名前−to−値のLinkedHashMapとして格納するフィールドを持たせる。コンストラクタは、定数群の名前と値を収集し、そのフィールドに格納する。パブリックメソッド、'getNames'と'getValues'が名前群と値群をそれぞれ公開する。

-Rebutter

名前群と値群はどのように収集するのか?

-Hypothesizer

Javaのリフレクション機能を使う。

-Rebutter

ふーん、リフレクション機能は、パフォーマンスの懸念の元ともなりうるが . . .

-Hypothesizer

それが、これをシングルトンにしなければならない理由だ。シングルトンなので、初期化は、基本的には、1度だけ動き、実際上、パフォーマンスの心配の種にはならないだろう。

-Rebutter

まあ、それであれば、おそらく、パフォーマンス上の問題はないだろう。

-Hypothesizer

'BaseEnumerableConstantsGroup'のコンストラクタで、定数群の名前や値を'this'から得るが、java.lang.Classクラスの'getFields'メソッドは使えない。

-Rebutter

なぜ使えないのか?

-Hypothesizer

このメソッドは、シャドーイングされたフィールドも取ってしまうから。

-Rebutter

それでは、このメソッドは、'Test1ExtendedConstantsGroup12.c_value1'だけでなく、'Test1ConstantsGroup1.c_value1'も取得するのか?

-Hypothesizer

そうだ。それに、このメソッドが戻すフィールドの並び順が都合よくない。サブインターフェースのフィールドが先になる。

-Rebutter

すると、上の例では、フィールドを以下の並び順で得ることになる。

  • 'Test1ExtendedConstantsGroup12.c_value1'
  • 'Test1ExtendedConstantsGroup12.c_value4'
  • 'Test1ExtendedConstantsGroup12.c_value5'
  • 'Test1ExtendedConstantsGroup12.c_value6'
  • 'Test1ConstantsGroup1.c_value1'
  • 'Test1ConstantsGroup1.c_value2'
  • 'Test1ConstantsGroup2.c_value3'
  • 'Test1ConstantsGroup2.c_value4'
-Hypothesizer

我々は、以下の並び順で欲しい。

  • 'Test1ExtendedConstantsGroup12.c_value1'
  • 'Test1ConstantsGroup1.c_value2'
  • 'Test1ConstantsGroup2.c_value3'
  • 'Test1ExtendedConstantsGroup12.c_value4'
  • 'Test1ExtendedConstantsGroup12.c_value5'
  • 'Test1ExtendedConstantsGroup12.c_value6'
-Rebutter

スーパーインターフェースのフィールドが先に来るが、フィールドが上書きされた場合は、そのフィールドは、元の位置のまま置き換えられる。

-Hypothesizer

それが我々の要求だ。

そこで、ユーティリティクラス、'thebiasplanet.coreutilities.reflectionshandling.ReflectionHandler'に、我々の要求を満たすstaticメソッドを作った。このメソッドのシグネチャは以下のとおりだ。

@Java Source Code
 public static LinkedHashMap <String, Object> getFieldNamesAndValues (Class <?> a_class, Object a_instance, boolean a_publicOnly, boolean a_setAccessible, List <Class <?>> a_assignableClassesOfFields, List <Class <?>> a_notAssignableClassesOfFields, boolean a_recursive) throws IllegalAccessException

-Rebutter

'a_setAccessible'というのは何だ?

-Hypothesizer

この引数を'true'にセットすると、メソッドは、呼び出し元から見えないフィールド(例えば'private'フィールド)も取得しようとする。しかし、そうできる保証はない。セキュリティ設定に依存するから。

-Rebutter

本件では、これを'true'にする必要はない。すべてのフィールドが'public'だから。

-Hypothesizer

このメソッドがこの引数を持っているのは、本件のためだけでなく、一般的な用途にと考えているためだ。

-Rebutter

'a_assignableClassesOfFields'と'a_notAssignableClassesOfFields'は何だ?

-Hypothesizer

それらは、取得するフィールドを選択するため。'a_assignableClassesOfFields'の中のあるクラスにキャストでき、'a_notAssignableClassesOfFields'内のどのクラスにもキャストできないフィールドだけが取得される。そうした選択が必要なのは、さもなければ、フィールド、'c_instance'が不必要に取得されてしまうから。

-Rebutter

ははあ。

'a_recursive'は、メソッドが、スーパークラス、スーパーインターフェース、実装されたインターフェースへと再帰的にフィールドを探すかどうかを指定するものだと思うが。

-Hypothesizer

そう。

このメソッドを、'BaseEnumerableConstantsGroup'のコンストラクタで以下のように呼ぶ。

@Java Source Code
   i_nameToValueMap = ReflectionHandler.getFieldNamesAndValues (this.getClass (), null, true, false, null, ListFactory. <Class <?>>createArrayList (this.getClass ()), true);

-Rebutter

'ListFactory'というのは何だ?

-Hypothesizer

ああ、それは、Listのインスタンスを生成する別のユーティリティクラスだ。Listをインスタンス化するのは配列をインスタンス化するほど便利ではないから、それを作った。

-Rebutter

'a_instance'に'null'を渡したのは、スタティックフィールドのみを取得する場合にはインスタンスは必要ないからのようだ。

-Hypothesizer

そう。'a_instance'に'null'を渡すと、スタティックフィールドのみを取得するという意味になる。

これがコードだ

-Hypothesizer

ここで、上記ユーティリティクラス群と'BaseEnumerableConstantsGroup'のコードを示す。

@Java Source Code
package thebiasplanet.coreutilities.collectionshandling;

import java.util.ArrayList;
import java.util.Arrays;

public class ListFactory {
 @SafeVarargs
 @SuppressWarnings("unchecked")
 public static <T> ArrayList <T> createArrayList (Object ... a_items) {
  ArrayList <T> l_arrayList = new ArrayList <T> ();
  if (a_items != null) {
   Arrays.stream (a_items).forEach (a_item -> {
    l_arrayList.add ( (T) a_item);
   });
  }
  return l_arrayList;
 }
 
 // This doesn't accept any array as an expandable item; pass Iterables instead.
 @SafeVarargs
 @SuppressWarnings("unchecked")
 public static <T> ArrayList <T> createArrayListExpandingItems (Object ... a_items) {
  ArrayList <T> l_arrayList = new ArrayList <T> ();
  if (a_items != null) {
   Arrays.stream (a_items).forEach (a_item -> {
    if (a_item instanceof Iterable) {
     for (Object l_element: (Iterable) a_item) {
      l_arrayList.add ( (T) l_element);
     }
    }
    else {
     l_arrayList.add ( (T) a_item);
    }
   });
  }
  return l_arrayList;
 }
}

package thebiasplanet.coreutilities.collectionshandling;

import java.util.LinkedHashSet;
import java.util.Arrays;

public class SetFactory {
 @SafeVarargs
 @SuppressWarnings("unchecked")
 public static <T> LinkedHashSet <T> createLinkedHashSet (Object ... a_items) {
  LinkedHashSet <T> l_linkedHashSet = new LinkedHashSet <T> ();
  if (a_items != null) {
   Arrays.stream (a_items).forEach (a_item -> {
    l_linkedHashSet.add ( (T) a_item);
   });
  }
  return l_linkedHashSet;
 }
 
 // This doesn't accept any array as an expandable item; pass Iterables instead.
 @SafeVarargs
 @SuppressWarnings("unchecked")
 public static <T> LinkedHashSet <T> createLinkedHashSetExpandingItems (Object ... a_items) {
  LinkedHashSet <T> l_linkedHashSet = new LinkedHashSet <T> ();
  if (a_items != null) {
   Arrays.stream (a_items).forEach (a_item -> {
    if (a_item instanceof Iterable) {
     for (Object l_element: (Iterable) a_item) {
      l_linkedHashSet.add ( (T) l_element);
     }
    }
    else {
     l_linkedHashSet.add ( (T) a_item);
    }
   });
  }
  return l_linkedHashSet;
 }
}

package thebiasplanet.coreutilities.collectionshandling;

import java.util.Map;
import java.util.LinkedHashMap;

public class MapFactory {
 @SafeVarargs
 @SuppressWarnings("unchecked")
 public static <T, U> LinkedHashMap <T, U> createLinkedHashMap (Object ... a_alternatelyKeyAndValue) {
  LinkedHashMap <T, U> l_linkedHashMap = new LinkedHashMap <T, U> ();
  if (a_alternatelyKeyAndValue != null) {
   T l_key = null;
   for (Object l_keyOrValue: a_alternatelyKeyAndValue) {
    if (l_key == null) {
     l_key = (T) l_keyOrValue;
    }
    else {
     l_linkedHashMap.put (l_key, (U) l_keyOrValue);
     l_key = null;
    }
   }
  }
  return l_linkedHashMap;
 }
 
 @SafeVarargs
 @SuppressWarnings("unchecked")
 public static <T, U> LinkedHashMap <T, U> createLinkedHashMapExpandingItems (Object ... a_alternatelyKeyAndValueOrMaps) {
  LinkedHashMap <T, U> l_linkedHashMap = new LinkedHashMap <T, U> ();
  if (a_alternatelyKeyAndValueOrMaps != null) {
   T l_key = null;
   for (Object l_keyOrValueOrMap: a_alternatelyKeyAndValueOrMaps) {
    if (l_keyOrValueOrMap instanceof Map) {
     for (Map.Entry <?, ?> l_mapEntry: ((Map <?, ?>) l_keyOrValueOrMap).entrySet ()) {
      l_linkedHashMap.put ((T) l_mapEntry.getKey (), (U) l_mapEntry.getValue ());
      l_key = null;
     }
    }
    else {
     if (l_key == null) {
      l_key = (T) l_keyOrValueOrMap;
     }
     else {
      l_linkedHashMap.put (l_key, (U) l_keyOrValueOrMap);
      l_key = null;
     }
    }
   }
  }
  return l_linkedHashMap;
 }
}

package thebiasplanet.coreutilities.reflectionshandling;

import java.util.List;
import java.util.LinkedHashMap;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class ReflectionHandler {
 // Fields are stored in the order of from the super class to the sub class and from the implemented interfaces to the implementing class.
 // Duplicate named fields are replaced by the sub class's.
 // If all the gotten fields are static, set a_instance to be null.
 public static LinkedHashMap <String, Object> getFieldNamesAndValues (Class <?> a_class, Object a_instance, boolean a_publicOnly, boolean a_setAccessible, List <Class <?>> a_assignableClassesOfFields, List <Class <?>> a_notAssignableClassesOfFields, boolean a_recursive) throws IllegalAccessException {
  LinkedHashMap <String, Object> l_fieldNameToValueMap = new LinkedHashMap <String, Object> ();
  if (a_recursive) {
   LinkedHashMap <String, Object> l_superClassOrImplementedInterfaceFieldNameToValueMap = null;
   Class<?> l_superClass = a_class.getSuperclass();
   if (l_superClass != null) {
    l_superClassOrImplementedInterfaceFieldNameToValueMap = getFieldNamesAndValues (l_superClass, a_instance, a_publicOnly, a_setAccessible, a_assignableClassesOfFields, a_notAssignableClassesOfFields, true);
    l_fieldNameToValueMap.putAll (l_superClassOrImplementedInterfaceFieldNameToValueMap);
   }
   Class<?>[] l_implementedInterfaces = null;
   l_implementedInterfaces = a_class.getInterfaces();
   for (Class<?> l_implementedInterface: l_implementedInterfaces) {
    l_superClassOrImplementedInterfaceFieldNameToValueMap = getFieldNamesAndValues (l_implementedInterface, null, a_publicOnly, a_setAccessible, a_assignableClassesOfFields, a_notAssignableClassesOfFields, true);
    l_fieldNameToValueMap.putAll (l_superClassOrImplementedInterfaceFieldNameToValueMap);
   }
  }
  Field [] l_fields = l_fields = a_class.getDeclaredFields ();
  FieldsLoop: for (Field l_field: l_fields) {
   int l_fieldModifiers = l_field.getModifiers ();
   if (a_publicOnly && !Modifier.isPublic (l_fieldModifiers)) {
    continue;
   }
   if (a_instance == null && !Modifier.isStatic (l_fieldModifiers)) {
    continue;
   }
   if (a_assignableClassesOfFields != null) {
    boolean l_assignable = false;
    for (Class <?> l_assignableClassOfFields: a_assignableClassesOfFields) {
     if (!l_assignableClassOfFields.isAssignableFrom (l_field.getType ())) {
      l_assignable = true;
      break;
     }
    }
    if (!l_assignable) {
     continue;
    }
   }
   if (a_notAssignableClassesOfFields != null) {
    for (Class <?> l_notAssignableClassOfFields: a_notAssignableClassesOfFields) {
     if (l_notAssignableClassOfFields.isAssignableFrom (l_field.getType ())) {
      continue FieldsLoop;
     }
    }
   }
   boolean l_isAccessible = l_field.isAccessible ();
   if (a_setAccessible && !l_isAccessible) {
    l_field.setAccessible (true);
   }
   l_fieldNameToValueMap.put (l_field.getName (), l_field.get (a_instance));
   if (a_setAccessible && !l_isAccessible) {
    l_field.setAccessible (l_isAccessible);
   }
  }
  return l_fieldNameToValueMap;
 }
}

package thebiasplanet.coreutilities.constantsgroups;

// ## Comments that start with the mark, ##, are instructions for extending this class, where XXX is the class name of the extended class.
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import thebiasplanet.coreutilities.reflectionshandling.ReflectionHandler;
import thebiasplanet.coreutilities.collectionshandling.ListFactory;
import thebiasplanet.coreutilities.collectionshandling.SetFactory;
import thebiasplanet.coreutilities.collectionshandling.MapFactory;

// ## The class has to be public, extend BaseConstantsGroup, and implement some constants group interfaces of T constants
abstract public class BaseEnumerableConstantsGroup <T> {
 private LinkedHashMap <String, T> i_nameToValueMap;
 // ## Define this.
 // ## public static final XXX c_instance = new XXX ();
 
 // ## Define the private constructor.
 
 protected BaseEnumerableConstantsGroup () {
  try {
   i_nameToValueMap = MapFactory. <String, T>createLinkedHashMapExpandingItems (ReflectionHandler.getFieldNamesAndValues (this.getClass (), null, true, false, null, ListFactory. <Class <?>>createArrayList (this.getClass ()), true));
  }
  catch (IllegalAccessException l_exception) {
   throw new RuntimeException (l_exception);
  }
 }
 
 public LinkedHashSet <String> getNames () {
  return SetFactory. <String>createLinkedHashSetExpandingItems (i_nameToValueMap.keySet ());
 }
 
 public ArrayList <T> getValues () {
  return ListFactory. <T>createArrayListExpandingItems (i_nameToValueMap.values ());
 }
}

実は、ソースファイル群(テスト用のものを含む)はここにある。これらのプロジェクトはGradleまたはAntを使っている。開発環境を構築する方法についての記述は、Linux用はここWindows用はここにある(本件に関係ない記述は無視すればよい)。zipファイルを、ディレクトリ構造を維持して展開し、各プロジェクトにカレントディレクトリを移して、コマンド、'gradle'または'ant'を実行すれば、プロジェクトがビルドされるはずだ。テストするには、'coreUtilitiesTestToDisclose'ディレクトリに移動して、コマンド、'gradle test'または'ant test'を実行すればいいはずだ。

本文 END

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