Wednesday, March 12, 2008

Learning generics by collecting arrays

So you have an array of objects. These objects are more than likely things that have properties. And life would be great if you could just get an array of all the values of just one of the properties of those objects.

Why would this be great? If these objects happen to map to a database instance (like entity beans), chances are each one of those objects has a long identifier, with a field probably named id, with an accessor method getId. My goal would be to do something like this:

List objectList = //...
Collection ids = collect(objectList, "id");

As it turns out, this was not very difficult to pull off. And behold, there is something very similar in a project called the Java Generic Algorithms library. You can use the transform method with the GetProperty functor to achieve something very similar. The problem is that the GetProperty doesn't work quite right. So I rolled my own.

One first part of the API is answering the question, "what should I call"? I am assuming the following things:

  1. You are thinking in terms of a field name, not the accessor function format, so I would always think "I'm getting the id" not "I'm getting the Id", and the case sensitivity matters.
  2. There is likely a public accessor called getFoo, and if foo is a boolean, it might be called getFoo, or it might be called isFoo. Depends.
  3. If you didn't write an accessor, you're probably thinking this is some kind of C-style struct, so assume the field itself is accessable.

Here's my breakdown of this in a function I called reflectSimpleField

static private  T reflectSimpleField (Object object, String field, Class returnClass) throws NoSuchFieldException
{
Object reference = null;
Class klass = object.getClass();

// check for accessor method in the form getField or isField.
try
{
reference = reflectAccessorMethod(object, klass, field, "get");
}
catch (NoSuchMethodException ex)
{
try
{
reference = reflectAccessorMethod(object, klass, field, "is");
}
catch (NoSuchMethodException ex2)
{
reference = reflectField(object, klass, field);
}
}

return (T)reference;
}

Reflecting the individual methods and fields were pretty simple, you just have to figure out the name you're going to use, and if it isn't there, I decided I would throw NoSuchFieldException. This isn't a RuntimeException, which is a little weak, IMO. (In a production system, I might change that, but this is one of those coding excercises.) In general, there were too many damn exceptions being thrown anyway, which makes the API really messy:

static private Object reflectAccessorMethod (Object object, Class klass, String field, String accessor) throws NoSuchMethodException
{
Object reference = null;

try
{
String methodName = accessor + capitalize(field);
Method method = klass.getMethod(methodName, (Class[])null);
reference = method.invoke(object, (Object[])null);
}
catch (IllegalAccessException ex)
{
throw new NoSuchMethodException("rethrowing IllegalAccessException as NoSuchMethodException :" + ex.getMessage());
}
catch (InvocationTargetException ex)
{
throw new NoSuchMethodException("rethrowing InvocationTargetException as NoSuchMethodException :" + ex.getMessage());
}

return reference;
}

static private Object reflectField(Object object, Class klass, String fieldName) throws NoSuchFieldException
{
Object reference = null;

try
{
Field field = klass.getDeclaredField(fieldName);
reference = field.get(object);
}
catch (IllegalAccessException ex)
{
throw new NoSuchFieldException("rethrowing IllegalAccessException as NoSuchFieldException :" + ex.getMessage());
}

return reference;
}

private static String capitalize(String string)
{
return string.substring(0, 1).toUpperCase() + string.substring(1);
}

Finally, I came up with two variants of my original idea for collect. The first, collect was intended to also be usable in a for loop, like so:

for (Long id : collect(objects, "id", Long.class))
{
// do something with id
}

This required adding the return type as an argument.

Just for kicks, I also came up with a specialized function for returning a List instead of something Iterable. This saves you from actually having to copy values just in case you don't want them.

My wack at these two functions:

public static  Iterable collect (Collection collection, String field, Class returnClass) throws NoSuchFieldException
{
ArrayList list = new ArrayList();
for(T item : collection)
list.add(reflectSimpleField(item, field, returnClass));
return list;
}

public static List collectList (Collection collection, String field) throws NoSuchFieldException
{
Class returnClass = null;
ArrayList list = new ArrayList();
for(T item : collection)
list.add(reflectSimpleField(item, field, returnClass));
return list;
}

If you find this stuff interesting or useful, feel free to cargo cult all you want. I'm curious to see how it compares with other things out there in the wild, and mostly, it was a great way to get used to some Java generics.

No comments: