A Foray Into Java Metaprogramming
I just learned that "foray" is a rather violent word: "a sudden attack into enemy territory." I was under the impression that it meant an experiment or exploration of something completely new. Now that I think about it, I'm happy with this more aggressive definition. I like to think that I'm aggressive about writing less code, especially after reading "Code's Worst Enemy" by Steve Yegge .
I was writing an HTTP endpoint in Java with the following type signature:
uuid, field to sort by, sort order -> [ list of data objects in order ]
As a concrete example, suppose we are working with students and schools. This endpoint would allow us to do things like fetch a list of all the students at a given school in alphabetical order by last name. Such a query might look like this:
school$4$g1fted$and$talent3d$5tudents$uuid, lastName, ascending
and return something like this:
[Student:[lastName:Aebersold
firstName:Jamey
school_id:school$4$gifted$and$talented$students$uuid]
... ]
The question I ran into was how to define what the sort by
parameter is. One
option would be to define a whitelist of sortable fields represented in Java as
an enum
and then implement Java Comparator
s for each one. When all the
boilerplate was said and done, each comparator would amount to various
incarnations of
return student1.getLastName().compareTo(student2.getLastName());
for each sort by
field. Instead, I decided that I wanted to be more aggressive
and try out this Java reflection thing that I've heard so much about. This is
what I wound up with:
/**
* Returns a Comparator on {@code T} given the field to sort by as a string
* and the orientation: positive for ascending and negative for descending.
*/
private <T> Comparator<T> getComparator(final String sortBy,
final int orientation, Class<T> clazz)
throws NoSuchFieldException {
final Field field = clazz.getDeclaredField(sortBy);
field.setAccessible(true);
return new Comparator<T>() throws Exception {
@Override
public int compare(T status1, T status2) {
try {
Object f1 = field.get(status1);
Object f2 = field.get(status2);
if (f1 instanceof Comparable<?> && f2 instanceof Comparable<?>) {
Comparable c1 = (Comparable) f1;
Comparable c2 = (Comparable) f2;
return orientation * c1.compareTo(c2);
} else {
throw new RuntimeException("Unsortable field: <" + sortBy + ">");
}
} catch (IllegalAccessException e) {
// TODO: throw a more specific exception.
throw new Exception("Unsortable field: <" + sortBy + ">");
}
throw new UnsupportedOperationException();
}
};
This was surprisingly fun and satisfying. Looking at it now, I can make a number of observations:
- It's really cool how
Field
objects take object objects as parameters, e.g.field.get(status1)
. - The implementation of
orientation
is bad Java and bad programming in general. It should be anEnum
which has meaning to humans and not just the computer, or mathematicians who have a particular understanding of what orientation means. - Having types makes me feel safe. There's something nice about having a return
type of
Comparator<T>
where the typeT
is being enforced throughout the method.
Of course, if you are willing to give up some type safety and write this in plain old untyped Clojure, you can get a similar procedure in five lines of code:
(defn get-comparator
[sortby]
(fn [x1 x2]
(compare (get x1 sortby)
(get x2 sortby))))
I think the deeper lesson here is that the complexity of the Java method springs from the fact that in Java, data is often hidden behind a particular class' getter/setter DSL. Though, is that better than hard setting hash map keywords that is common in Clojure? Dunno.