Reflection vs. code generation

Avoid runtime reflection when marshaling data

1 2 Page 2
Page 2 of 2
Runtime Reflection Exception:
java.lang.NumberFormatException: itemName
at java.lang.Integer.parseInt(Integer.java:409)
at java.lang.Integer.parseInt(Integer.java:458)
at ...util.SimpleFileIterator.nextInt(SimpleFileIterator.java:82)
at ...dataLoader.SimpleFileLoadManager.load(SimpleFileLoadManager.java:44)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:129)
at ...dataLoader.ReflectiveObjectLoader.constructObject(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:134)
at ...dataLoader.ReflectiveObjectLoader.constructObjectArray(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeArray(ReflectiveObjectLoader.java:39)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:123)
at ...dataLoader.ReflectiveObjectLoader.constructObject(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:134)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:103)

Here is the generated code exception:

java.lang.NumberFormatException: itemName
at java.lang.Integer.parseInt(Integer.java:409)
at java.lang.Integer.parseInt(Integer.java:458)
at ...util.SimpleFileIterator.nextInt(SimpleFileIterator.java:82)
at ....example.generated.PurchaseOrderLoader.loadint(PurchaseOrderLoader.java:32)
at ....example.generated.PurchaseOrderLoader.loadLineItem(PurchaseOrderLoader.java:22)
at ....example.generated.PurchaseOrderLoader.loadLineItemArray(PurchaseOrderLoader.java, Compiled Code)
at ....example.generated.PurchaseOrderLoader.loadPurchaseOrder(PurchaseOrderLoader.java:27)

For runtime reflection, heavy logging is required to isolate the problem. The heavy use of logging during the load procedure is a code smell (a term used by those in XP circles to identify code that may need a healthy dose of refactoring) that indicates that perhaps a different approach may be necessary. Using reflection, you can make the stack trace more meaningful, but this further complicates an already complicated situation. In the generated code approach, the resulting code just logs how runtime reflection would handle the situation.

The two implementations provided an ample testing ground for performance. (Thanks to Josh MacKenzie for pointing this out as a potential concern.) I was surprised to find that my example load was typically four to seven times slower using runtime reflection.

A typical run produces results like the following:

java com.thoughtworks.rettig.example.TestPerformance
Number of Iterations: 100000
Generated
Total time: 14481
Max Memory Used: 1337672
Reflection
Total time: 89219
Max Memory Used: 1407944

This delay can be attributed to the time reflection requires to discover the class attributes during runtime. The generated code simply consists of explicit calls. Runtime reflection uses slightly, but not significantly, more memory. Of course, the reflection could probably be better optimized, but this optimization's complexity would be enormous, and probably still lag far behind an explicit solution.

Conversely, the generated code was a breeze to optimize. With a similar code generator on a previous project, I optimized the load procedure to operate in a smaller memory footprint. I was able to make the changes in the code generator in just a few minutes. The optimization introduced a bug, but the stack trace pointed me directly to the offending method generation procedure and I fixed it within minutes. I would not try this same optimization with runtime reflection because it would require mental backflips.

Running the source code

If you look over the source code, you may be able to better grasp some of the topics covered here. To compile and run the source code, just unzip the contents to an empty folder, then run ant Install at the command line. This will use the Ant build script to generate, compile, and unit test all the source code. (This installation assumes you already have Ant and JUnit 3.7 installed.)

I created a highly contrived example using a simple purchase order that contains several types of objects. The JUnit test cases demonstrate how you create a purchase order object from a file using each method. The test cases then validate the objects' contents to ensure that the data is properly loaded. You can find the contents of the tests and all supporting classes in the following packages:

  • com.thoughtworks.rettig.example
  • com.thoughtworks.rettig.example.reflection
  • com.thoughtworks.rettig.example.generated

The most notable difference between the two test cases is that runtime reflection requires no supporting code to load the data. This is the magic of reflection. It only requires the class definition and location of the source data to load the data. The generated code example relies on a generated loading class before it can create the test case.

The object creation process is very similar for both implementations. Here is the reflection code:

SimpleFileIterator iter = new SimpleFileIterator(fileLocation);
LoadManager manager = new SimpleFileLoadManager(iter);
PurchaseOrder obj = (PurchaseOrder) ReflectiveObjectLoader.initializeInstance(PurchaseOrder.class, manager);

Here is the generated code:

SimpleFileIterator iter = new SimpleFileIterator(file);
PurchaseOrder po = PurchaseOrderLoader.loadPurchaseOrder(iter);

Rule of thumb

The benefits of reflection are obvious. When coupled with code generation it becomes an invaluable and, more importantly, a safe tool. There is often no other way to escape many seemingly redundant tasks. As for code generation: the more I work with it, the more I like it. With every refactoring and increase in functionality, the code becomes clearer and more understandable. However, runtime reflection has the opposite effect. The more I increase its functionality, the more it increases in complexity.

So, in the future, if you feel you need to conquer a complicated problem using reflection, just remember one rule: don't do it at runtime.

Michael J. Rettig currently works as a software developer at ThoughtWorks, a consulting firm specializing in enterprise-transforming software. He holds a BS in computer science from Valparaiso University. Michael is a Java 2 Certified Programmer and Java 2 Certified Developer. He would like to thank Martin Fowler, whose guidance made this article possible.

Learn more about this topic

1 2 Page 2
Page 2 of 2