<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>
定数群の名前や値が列挙可能でなければならない定数グループのベースクラスを作ろう
話題: Javaプログラミング言語
以前の記事で我々はenumの制限について話した。
enumは拡張することができない、それがJavaのEnumであろうが、自ら作ったenumであろうが、enumの本質を損なうことなしには。
enumの本質は、少なくとも我々にとっては、メソッドの引数をあらかじめ定められた値の集合に制限することだ。その制限が要らないのであれば、ただ定数グループを使えばよいだけだ。そうすれば、それらを好きなように拡張できる。
我々が「定数グループ」と呼んでいるのは、典型的には、以下のようなインターフェースのことだ。
public interface AConstantsGroup {
String c_value1 = "Value1";
String c_value2 = "Value2";
}
そう。クラスにすることもできるが、そうしたら、多重継承が使えないだろう。だから、特に理由がなければ、我々はそれをインターフェースにする。
ははあ。
enumを受け取るメソッドを作る場合、確かに、enumは便利だ。引数のタイプが特定(ただのStringとかではない)だから、引数に何を渡せるかが明確であり、テストしなくても、あらかじめ定められた値だけが渡されることが保証される。
しかし、値を、既存のメソッド(例えば、JDKの標準ライブラリのメソッド)の、例えば、String引数に渡そうとする場合、enumを使うメリットがあまりない。
そのメソッドを多くの場所で呼ぶ際に、リテラルを散乱させたくない場合のことを君は言っているのだろう。
メソッドそのものは、引数を可能な値の有限数のオプションに制限しないのだが、我々のプログラムは、値の有限数の集合のみを使い、その集合を、我々は定数グループとして管理したい。
オーケー。
そうした場合にenumを使うと、それを拡張できないというハンディキャップを、必要もなく負うことになってしまう。実際、場合により、我々はそれを拡張したいのだ。
すると、そうした場合に、我々は、定数グループを使うわけだ。
しかしながら、場合により、定数グループに含まれる定数群の値や名前を列挙したいということがある。
それは、上記のような単純なインターフェースではできない。
そこで、ここでは、それを可能にしよう。
ベース抽象クラスを作り、それを拡張するクラスに含まれる定数群の名前と値を収集する機能を持たせる。
実際には、'abstract public class BaseEnumerableConstantsGroup <T>'というクラスを(パッケージ、'thebiasplanet.coreutilities.constantsgroups'に)作る。
'T'は何を表わすのか?
定数値群のクラスだ。
なるほど。
このベースクラスを以下のように使う。
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 () {
}
}
定数グループ、'Test1ConstantsGroup1'を通常どおりインターフェースとして作り、この定数グループを実装した、先ほどのベースクラスの具象クラスとして、列挙可能な定数グループを作るわけだ。
そうだ。列挙可能定数グループは拡張せず、インターフェースである定数グループを拡張し、それら定数グループの列挙可能バージョンを作ることにする。そうすれば、多重継承が使える。
以下が例だ。
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 () {
}
}
ふーむ、具象クラスがしないといけないのは、抽象クラスを拡張し、インターフェースを実装し、シングルトンインスタンスを定義し、コンストラクタをプライベートにすることか。
そうだ。たいした負担でなければよいが。
定数群の名前や値はどうやって得るのか?
以下のようにして得られる。
LinkedHashSet <String> l_names = Test1EnumerableExtendedConstantsGroup12.c_instance.getNames ();
ArrayList <String> l_values = Test1EnumerableExtendedConstantsGroup12.c_instance.getValues ();
どのように実装する?
ベースクラスには、定数群の名前と値を名前−to−値のLinkedHashMapとして格納するフィールドを持たせる。コンストラクタは、定数群の名前と値を収集し、そのフィールドに格納する。パブリックメソッド、'getNames'と'getValues'が名前群と値群をそれぞれ公開する。
名前群と値群はどのように収集するのか?
Javaのリフレクション機能を使う。
ふーん、リフレクション機能は、パフォーマンスの懸念の元ともなりうるが . . .
それが、これをシングルトンにしなければならない理由だ。シングルトンなので、初期化は、基本的には、1度だけ動き、実際上、パフォーマンスの心配の種にはならないだろう。
まあ、それであれば、おそらく、パフォーマンス上の問題はないだろう。
'BaseEnumerableConstantsGroup'のコンストラクタで、定数群の名前や値を'this'から得るが、java.lang.Classクラスの'getFields'メソッドは使えない。
なぜ使えないのか?
このメソッドは、シャドーイングされたフィールドも取ってしまうから。
それでは、このメソッドは、'Test1ExtendedConstantsGroup12.c_value1'だけでなく、'Test1ConstantsGroup1.c_value1'も取得するのか?
そうだ。それに、このメソッドが戻すフィールドの並び順が都合よくない。サブインターフェースのフィールドが先になる。
すると、上の例では、フィールドを以下の並び順で得ることになる。
- 'Test1ExtendedConstantsGroup12.c_value1'
- 'Test1ExtendedConstantsGroup12.c_value4'
- 'Test1ExtendedConstantsGroup12.c_value5'
- 'Test1ExtendedConstantsGroup12.c_value6'
- 'Test1ConstantsGroup1.c_value1'
- 'Test1ConstantsGroup1.c_value2'
- 'Test1ConstantsGroup2.c_value3'
- 'Test1ConstantsGroup2.c_value4'
我々は、以下の並び順で欲しい。
- 'Test1ExtendedConstantsGroup12.c_value1'
- 'Test1ConstantsGroup1.c_value2'
- 'Test1ConstantsGroup2.c_value3'
- 'Test1ExtendedConstantsGroup12.c_value4'
- 'Test1ExtendedConstantsGroup12.c_value5'
- 'Test1ExtendedConstantsGroup12.c_value6'
スーパーインターフェースのフィールドが先に来るが、フィールドが上書きされた場合は、そのフィールドは、元の位置のまま置き換えられる。
それが我々の要求だ。
そこで、ユーティリティクラス、'thebiasplanet.coreutilities.reflectionshandling.ReflectionHandler'に、我々の要求を満たすstaticメソッドを作った。このメソッドのシグネチャは以下のとおりだ。
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
'a_setAccessible'というのは何だ?
この引数を'true'にセットすると、メソッドは、呼び出し元から見えないフィールド(例えば'private'フィールド)も取得しようとする。しかし、そうできる保証はない。セキュリティ設定に依存するから。
本件では、これを'true'にする必要はない。すべてのフィールドが'public'だから。
このメソッドがこの引数を持っているのは、本件のためだけでなく、一般的な用途にと考えているためだ。
'a_assignableClassesOfFields'と'a_notAssignableClassesOfFields'は何だ?
それらは、取得するフィールドを選択するため。'a_assignableClassesOfFields'の中のあるクラスにキャストでき、'a_notAssignableClassesOfFields'内のどのクラスにもキャストできないフィールドだけが取得される。そうした選択が必要なのは、さもなければ、フィールド、'c_instance'が不必要に取得されてしまうから。
ははあ。
'a_recursive'は、メソッドが、スーパークラス、スーパーインターフェース、実装されたインターフェースへと再帰的にフィールドを探すかどうかを指定するものだと思うが。
そう。
このメソッドを、'BaseEnumerableConstantsGroup'のコンストラクタで以下のように呼ぶ。
i_nameToValueMap = ReflectionHandler.getFieldNamesAndValues (this.getClass (), null, true, false, null, ListFactory. <Class <?>>createArrayList (this.getClass ()), true);
'ListFactory'というのは何だ?
ああ、それは、Listのインスタンスを生成する別のユーティリティクラスだ。Listをインスタンス化するのは配列をインスタンス化するほど便利ではないから、それを作った。
'a_instance'に'null'を渡したのは、スタティックフィールドのみを取得する場合にはインスタンスは必要ないからのようだ。
そう。'a_instance'に'null'を渡すと、スタティックフィールドのみを取得するという意味になる。
ここで、上記ユーティリティクラス群と'BaseEnumerableConstantsGroup'のコードを示す。
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'を実行すればいいはずだ。