<The previous article in this series | The table of contents of this series | The next article in this series>
Let's Make a Base Class of Constants Groups Whose Constant Names or Values Have to Be Enumerable
About: Java Programming Language
We talked about an limitation of enums in a previous article.
We can't extend any enum, whether it is a Java Enum or an enum that we created ourselves, without ruining the essence of enums.
The essence of enums, at least for us, is to restrict an argument of a method to a set of predetermined values. If we don't need the restriction, we can just use constants groups. Then we can extend them as we like.
What we mean by 'constants group' is typically an interface like this.
public interface AConstantsGroup {
String c_value1 = "Value1";
String c_value2 = "Value2";
}
Yes. Although we could make it a class, then, we wouldn't be able to use multiple inheritance. So, if there is no particularly reason, we will make it an interface.
Ah-ha.
Certainly, an enum is handy when we create a method that receives the enum. As the argument type is specific, not just String or something, it is clear what we can pass to the argument, and it is guaranteed that only predetermined values will be passed to the argument, without any necessity to do tests.
However, if we are passing a value to, say, a String argument of an existing method (for example, a method of the JDK standard library), there is not much merit in using an enum.
I guess, you are talking about a case in which we don't want to use literals scattered in calling the method in many places.
The method itself doesn't restrict the argument to finite options of possible values, but our program uses only a finite set of values, and we want to manage the set as a constants group.
OK.
If we use an enum in such a case, we will needlessly assume a handicap of not being able to extend it. In fact, sometimes, we want to extend it.
So, we use constants groups in such cases.
However, in some cases, we want to enumerate the values or the names of the constants contained in the constants group.
That, we can't do with simple interfaces like the above one.
So, here, we will make that possible.
We will create a base abstract class that has the functionality of collecting the names and the values of the constants contained in the class that extends it.
In fact, we create the class, 'abstract public class BaseEnumerableConstantsGroup <T>' (in the package, 'thebiasplanet.coreutilities.constantsgroups').
What is 'T'?
It's the class of the constant values.
I see.
We use the base class like this.
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 () {
}
}
We create the constants group, 'Test1ConstantsGroup1', as an interface, as usual, then create the enumerable constants group as a concrete class of the base class, implementing the constants group.
Yes. We don't extend enumerable constants groups, but extend constants groups as interfaces and make enumerable versions of those constants groups. That way, we can use multiple inheritance.
This is an example.
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 () {
}
}
Well, what the concrete class has to do is to extend the abstract class, implement the interface, define the singleton instance, and make the constructor private.
Yes. I hope, that's not too much.
How do we get the names and the values of the constants?
We can get them like this.
LinkedHashSet <String> l_names = Test1EnumerableExtendedConstantsGroup12.c_instance.getNames ();
ArrayList <String> l_values = Test1EnumerableExtendedConstantsGroup12.c_instance.getValues ();
How do we implement it?
The base class has the field that contains the constant names and values as a name-to-value LinkedHashMap; the constructor collects the names and the values of the constants and store them into the field; the public methods, 'getNames' and 'getValues' disclose the names and the values respectively.
How do we collect the names and the values?
We use the reflection feature of Java.
Hmm, using the reflection feature can be a source of performance concerns, . . .
That's the reason we have to make it a singleton. As it is a singleton, the initialization will, basically, run only once, and won't be, practically, a performance concern.
Well, then, probably, there will be no performance problem.
We get the names and the values of the constants from 'this' in the constructor of 'BaseEnumerableConstantsGroup', but we can't use the 'getFields' method of the class, java.lang.Class.
Why not?
Because the method gets also shadowed fields.
So, the method gets not only 'Test1ExtendedConstantsGroup12.c_value1', but also 'Test1ConstantsGroup1.c_value1'?
Yes. Besides, we don't like the order of the fields in which the method returns them: the fields of the sub interface come first.
So, in the example above, we would get the fields in this order.
- 'Test1ExtendedConstantsGroup12.c_value1'
- 'Test1ExtendedConstantsGroup12.c_value4'
- 'Test1ExtendedConstantsGroup12.c_value5'
- 'Test1ExtendedConstantsGroup12.c_value6'
- 'Test1ConstantsGroup1.c_value1'
- 'Test1ConstantsGroup1.c_value2'
- 'Test1ConstantsGroup2.c_value3'
- 'Test1ConstantsGroup2.c_value4'
We want them in this order.
- 'Test1ExtendedConstantsGroup12.c_value1'
- 'Test1ConstantsGroup1.c_value2'
- 'Test1ConstantsGroup2.c_value3'
- 'Test1ExtendedConstantsGroup12.c_value4'
- 'Test1ExtendedConstantsGroup12.c_value5'
- 'Test1ExtendedConstantsGroup12.c_value6'
So, the fields of the super interface come first, but when a field is overwritten, the field will be replaced at the original position.
That's what we want.
So, I created a static method in a utility class, 'thebiasplanet.coreutilities.reflectionshandling.ReflectionHandler', that fulfills our requirement. The signature of the method is this.
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
What is 'a_setAccessible'?
If the argument is set 'true', the method tries to get also fields that are invisible from the caller (for example 'private' fields). However, it isn't guaranteed to be able to do so: it depends on the security configuration.
In this case, we don't need to set it 'true' because all the fields are 'public'.
The method has the argument because I mean it to be for general purposes, not only for this case.
What are 'a_assignableClassesOfFields' and 'a_notAssignableClassesOfFields'?
They are for selecting fields to be gotten: only fields that are assignable to a class in 'a_assignableClassesOfFields' and are not assignable to any class in 'a_notAssignableClassesOfFields' are gotten. Such a selection is necessary because, otherwise, the field, 'c_instance', would be needlessly gotten.
Ah-ha.
I presume that 'a_recursive' specifies whether the method will search fields recursively into super classes, super interfaces, and implemented interfaces.
Yes.
We call the method in the constructor of 'BaseEnumerableConstantsGroup' like this.
i_nameToValueMap = ReflectionHandler.getFieldNamesAndValues (this.getClass (), null, true, false, null, ListFactory. <Class <?>>createArrayList (this.getClass ()), true);
What is 'ListFactory'?
Ah, that's just another utility class that creates List instances: I created it because initiating a List isn't as handy as initiating an array.
We seem to have passed 'null' to 'a_instance' because the instance isn't necessary when we get only static fields.
Yes. Passing 'null' to 'a_instance' means getting only static fields.
Here, I cite the code of those utility classes and '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 ());
}
}
In fact, the source files (including ones for the test) are here. Those projects use Gradle or Ant. Descriptions about how to build the developing environment are here for Linux or here for Windows (ignore descriptions about what are irrelevant here). Expanding the zip file with the directories structure preserved, changing the current directory to each project, and executing the command 'gradle' or 'ant' should build the project. To do the test, changing to the 'coreUtilitiesTestToDisclose' directory and executing the command, 'gradle test' or 'ant test' should do.
<The previous article in this series | The table of contents of this series | The next article in this series>