Motivation
Once upon a time I wanted to create enum that contains various operating system, i.e.
public enum OsType {
WindowsNTWorkstation,
WindowsNTServer,
Windows2000Server,
Windows2000Workstation,
WindowsXp,
WindowsVista,
Windows7,
Windows95,
Windows98,
WindowsNTWorkstation,
WindowsNTServer,
Windows2000Server,
Windows2000Workstation,
WindowsXp,
WindowsVista,
Windows7,
Windows95,
Windows98,
Fedora,
Ubuntu,
Knopix, SunOs,
HpUx,
}
I was not satisfied of this structure because I'd like to see a group of WindowsNT that contains WinNTWorkstation and WindNT server. All windows versions should be in super group of "windows". Fedora, Knopix and Ubuntu are distributions of Linux. All Linux distributions together with SunOs and HpUx are Unix systems. Obviously that all Windows systems have common properties. The same is about Unix systems. And I hate copy/paste programming.
Class per OS Solution
The obvious solution here is to create separate classes per operating system and several abstract classes. For example class Fedora extends class Linux that extends class Unix that extends class OperatingSystem. We can enjoy all advantages of inheritance, so all common properties of Windows OS are stored in class Windows and can be overridden by its subclasses.
But now we cannot see all operating systems together, iterate over them etc., i.e. very useful features of Java enum are missing.
No problem! Now we can create enum like previous that holds custom field of type Class:
public enum OsType {
WindowsNTWorkstation(WindowsNTWorkstation.class),
WindowsNTServer(WindowsNTServer.class),
Windows2000Server(Windows2000Server.class),
Windows2000Workstation(Windows2000Workstation.class),
WindowsXp(WindowsXp.class),
WindowsVista(WindowsVista.class),
Windows7(Windows7.class),
Windows95(Windows7.class),
Windows98(Windows98.class),
WindowsNTWorkstation(WindowsNTWorkstation.class),
WindowsNTServer(WindowsNTServer.class),
Windows2000Server(Windows2000Server.class),
Windows2000Workstation(Windows2000Workstation.class),
WindowsXp(WindowsXp.class),
WindowsVista(WindowsVista.class),
Windows7(Windows7.class),
Windows95(Windows7.class),
Windows98(Windows98.class),
Fedora(Fedora.class),
Ubuntu(Ubuntu.class),
Knopix(Knopix.class), SunOs(SunOs.class),
HpUx(HpUx.class),
;
private Class clazz;
OsType(Class clazz) {
this.clazz = clazz;
}
}
This solution is better but it still has disadvantages:
- Implementation of method that retrieves all "children" of specific OS (for example all Linux distributions) is hard and ineffective.
- Grouping is separate from enum.
- The solution is very verbose: each OS is represented by its own class even if the class has nothing to override.
Hierarchical Enum
To create hierarchy using enum we need custom field "parent" that is initialized by constructor:
public enum OsType {
OS(null),
Windows(OS),
WindowsNT(Windows),
WindowsNTWorkstation(WindowsNT),
WindowsNTServer(WindowsNT),
Windows2000(Windows),
Windows2000Server(Windows2000),
Windows2000Workstation(Windows2000),
WindowsXp(Windows),
WindowsVista(Windows),
Windows7(Windows),
Windows95(Windows),
Windows98(Windows),
Unix(OS) {
@Override
public boolean supportsXWindows() {
return true;
}
},
Linux(Unix),
AIX(Unix),
HpUx(Unix),
SunOs(Unix),
;
private OsType parent = null;
private OsType(OsType parent) {
this.parent = parent;
}
}
This structure allows implementation of method "is" that works like operator instanceof for classes and interfaces. For example Windows2000 is Windows, Fedora is Linux, Windows is not Unix etc.
if (other == null) {
return false;
}
for (OsType t = this; t != null; t = t.parent) {
if (other == t) {
return true;
}
}
return false;
}
Sometimes we need method that returns all "children" of current nodes, e.g. all Linux systems or all variants of Windows2000. The easiest way to implement this is to hold collection of children per element and fill it from constructor:
private List<OsType> children = new ArrayList<OsType>();
private OsType(OsType parent) {
this.parent = parent;
if (this.parent != null) {
this.parent.addChild(this);
}
}
Now method "children()" that returns direct node's children is trivial:
public OsType[] children() {
return children.toArray(new OsType[children.size()]);
}
It is not hard to implement recursive method "allChildren()" that returns all children of current node (see full source code).
But hierarchy term is always accompanied by inheritance that allows overriding methods of parent. This is the basic feature of classes in all object oriented languages. Is it possible to implement a kind of inheritance relationship for elements of one enum?
Overriding parent's method
Unix systems support X Window graphical environment. MS Windows does not. We would like to be able to ask OS whether is supports X Window.
We can define boolean flag "supportsX" and boolean method
public boolean supportsX() {return suppotsX;}
Now we have to add yet another argument to OsType constructor and pass true/false for each element of the enum. But it is too verbose. Is it possible to say that Unix supports X, Windows does not support X and be sure that Fedora's supportX() returns true while Winddows95's supportX() returns false?
The implementation is pretty simple. First for simplicity let's say that X Window is supported by all Unix systems and is not supported by others.
So, we can implement method supportsXWindowSystem() at enum level as following:
public boolean supportsXWindowSystem() {
return false;
}
Now we have to override it for all Unix systems. To implement this we change the default implementation to following:
public boolean supportsXWindowSystem() {
if (this == OsType.OS) {
return false;
}
for (OsType t = this; t != null; t = t.parent()) {
if(!t.getClass().equals(OsType.class)) {
try {
return (Boolean)t.getClass().
getDeclaredMethod(
"supportsXWindowSystem").
invoke(t);
} catch (SecurityException e) {
continue;
} catch (NoSuchMethodException e) {
continue;
} catch (IllegalArgumentException e) {
throw new IllegalStateException(e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
}
}
}
return OsType.OS.supportsXWindowSystem();
}
This method invokes itself using reflection starting from the current enum element and iterating over its parents until it succeeds. The trick here is in condition !t.getClass().equals(OsType.class): the reflection call happens only if enum elements really overrides the method. So, the method of first parent in hierarchy that implements the method will be used. If no one of parents and parents of parents does not implement this method itself we call method of root element.
Now we can say the following:
...
Unix(OS) {
@Override
public boolean supportsXWindowSystem() {
return true;
}
},
Linux(Unix),
AIX(Unix),
HpUx(Unix),
SunOs(Unix),
...
@Override
public boolean supportsXWindowSystem() {
return true;
}
},
Linux(Unix),
AIX(Unix),
HpUx(Unix),
SunOs(Unix),
...
The method is overridden for Unix element only and all its children will use this method.
The problem is solved. We got enum based hierarchical polymorphic structure! We can implement method in base element (using it like a super class) and then override it in any element we want.
The only disadvantage of this solution is that now we have to create similar implementation for each method we add to this enum and for all other enums that hold hierarchical structure.
Utility that helps to call hierarchical method
The utility is implemented as static method that makes more general implementation than supportsXWindowSystem(). The main difference between common utility and method implemented for specific enum is that
- we cannot use the enum's fields directly
- we cannot use hard-coded method name and return type.
- we need a way to get parent of current enum element.
public static <R> R callHierarchicalMethod(Enum<?> enumElem, Enum<?> rootElem, R rootValue, String parentAccessor)
Utility is parametrized that allows to support any return type.
The utility uses reflection. But how to discover the "real" method name that should be called? The Throwable.getStackTrace() helps us:
String methodName = new Throwable().getStackTrace()[1].getMethodName();
Element #1 is the a "real" enum method, i.e. the method that directly called the utility.
The next difference is where to use getClass() and where getDecalaringClass(). While we are iterating over the chain of elements from current to its parents we use getClass() that returns class of enum if the element does not redefine any method and anonymous inner class if element defines at least one method. The fallback code in the end of the utility uses getDecalaringClass() to invoke the business method that is implemented on enum level itself.
Moreover we cannot just invoke
elem.getClass().getDeclaredMethod(methodName).invoke(elem)
Although the method must be accessible (better public) the class itself may be not public. For example it happens if element is presented by anonymous inner class. In this case the method can be invoked only after calling setAccessible(true).
Method m = clazz.getDeclaredMethod(methodName);
m.setAccessible(true);
m.invoke(elem)
Finally we need a way to access parent element. I decided to use reflection for this purpose. It is easier than creating interface with one method Enum<?> getParent(Enum<?> elem). This is classical use-case for closures, so I am waiting for Java 7 to improve this code.
Now default implementation of our business method looks like:
public boolean supportsXWindowSystem() {
return EnumUtil.<Boolean>callHierarchicalMethod(
this, OsType.OS, false, "parent");
}
Full source code of utility can be found here. Please find OsType enum as an usage example. JUnit test case that can be used as code example as well is available also.
Conclusions
Although we are regular to use enums as some kind of static arrays they also can be used to present hierarchical tree-like data structures where each node can find its parent, its children and even inherit and override parent's method almost exactly as we do with class inheritance.
Nice idea!
ReplyDeleteBut your way of overriding parent's method is too complicated. Why not just define a top level method as
public boolean supportsXWindowSystem() {if (parent != null) return parent. supportsXWindowSystem(); return false;}
and then override the Unix's method as
@Override
public boolean supportsXWindowSystem() {return true;}
It is because in this case you will have to override this method for Unix and all it "sub entries": Linux, SunOs, HPUX etc.
ReplyDeleteIt is not what happens for regular class inheritance.
The point of my trick is that I found how to simulate behavior of JVM when it finds implementation of method in class hierarchy.
The version with "ask the parent" works in pretty the same way. A child that has a default implementation of a method would always ask its parent. An "event bubbling" stops at first concrete implementation. I just verified my solution with unit tests.
ReplyDeleteYour way could be more generic and powerful.
A native implementation is refactoring safe.
Thanks for an expiring idea!
It's always wise to avoid overriding enum methods - see http://bugs.sun.com/view_bug.do?bug_id=5020490
ReplyDeleteThe "ask the parent" version has also been my first idea. And it seems to work just fine...
ReplyDeleteI have created a simpler (at least in my eyes) solution:
ReplyDeletehttp://blog.cedarsoft.com/2010/10/hierarchical-structures-with-java-enums/
Johanes, I think you are right. Your approach is better than mine.
ReplyDeleteGuys, thank you for your interest. As often discussion can be very valuable and helps to find better solution.
Thanks a lot AlexR, never thought Java Enum can be used like that. Indeed great tip.
ReplyDeleteJavin
10 tips on java debugging with eclipse
Nice work AlexR and Johanes, now suppose I fork some Unix to create a new window system (call it 'NoXNix') and remove support for XWindows? A little tweak to allow overriding the parent:
ReplyDeletepublic enum OsType {
OS(null) {{supportsXWindows=false}},
Windows(OS),
WindowsNT(Windows),
...
Unix(OS) {{supportsXWindows=true}},
Linux(Unix),
...
NoXNix(Unix){{supportsXWindows=false}}
;
...
Boolean supportsXWindows = null;
private OsType(OsType parent) {
this.parent = parent;
}
...
public boolean supportsXWindows() {
if(supportsXWindows == null)
return parent.supportsXWindows();
return supportsXWindows;
}
}
Luke
If a node can have multiple parents, what approach would you suggest ?
ReplyDelete