Creating Java classes at runtime for evaluating numerical expressions

by Ulf Dittmer

This article demonstrates two techniques that are frequently asked about: creating Java classes at runtime, and evaluating a mathematical expression that is given as a string. While these two don't have much to do with one another at first sight, the former can be used to implement a well-performing solution to the latter. Performance matters in this case, because usually formulas must be evaluated repeatedly for a range of parameters, at which point approaches that are based on interpreters quickly become a limiting factor. On the other hand, approaches that compile the formula to a Java class –like the one presented below– will show much better performance.

We start out with creating a basic class that evaluates a formula given a single parameter. Step by step the class is extended to be more developer-friendly and/or more powerful, until at the end it handles multiple parameters while being easy to program with, and easy to extend, at the same time.

The accompagnying zip file contains all example codes and the Javassist library, which is used for dynamically creating classes. Nothing else is needed to get the examples to run.

The Beginning

The following code shows the basic steps to create classes at runtime using the Javassist library. Javassist is part of JBoss these days, but is otherwise independent of it; it can be downloaded from SourceForge.

1) ClassPool pool = ClassPool.getDefault();

2) CtClass evalClass = pool.makeClass("Eval");

3) evalClass.addMethod(
        CtNewMethod.make(
            "public double eval (double x) { return (" + args[0] + ") ; }",
            evalClass));

4) Class clazz = evalClass.toClass();

5) Object obj = clazz.newInstance();

6) Class[] formalParams = new Class[] { double.class };
Method meth = clazz.getDeclaredMethod("eval", formalParams);

7) Object[] actualParams = new Object[] { new Double(17) };
double result = ((Double) meth.invoke(obj, actualParams)).doubleValue();
System.out.println(result);

Let's go through the steps one by one and look briefly at what they do. The following examples are based on it, so it's important to understand the basic concepts now.

1) Javassists's ClassPool represents the set of all classes in a JVM. It needs to be retrieved once, but nothing further is done with it except from adding the new class to it.

2) This creates a new class named 'Eval'. CtClass is the most important of Javassist's classes. It's the equivalent to java.lang.Class, and is used to carry out just about all modifications one might want to apply to a new or existing class.

If the newly created class should be part of a package, then its name must contain the fully qualified class name, like it would be used in an import statement.

3) A method is created and added to the class. The body of the method is

public double eval (double x) { return (" + args[0] + ") ; }

as if it was part of a regular source file. The formula to be evaluated is passed in as an args parameter, and becomes part of the method. The method takes a double parameter and returns a double result.

4) Once we're done creating the class, we can retrieve its java.lang.Class object from the CtClass object. This concludes the use of Javassist - we can now use the new class like we would use any other class that was loaded dynamically.

5) We instantiate it, ...

6) ... use reflection to get a handle to the eval method, ...

7) ... and finally call the method with a parameter of 17 and print the result.

The example can be run using

    make run1

(if you have make available), or directly via

    java -classpath .:javassist-3.4.jar Example1 "x + 42"

The expression to be evaluated is passed to the code as a parameter. If you use this formula, the result should be 59 (= 17 + 42).

Case closed? Of course not. The reflection code is ugly, tedious to write, and –if executed many times– not very performant. We want to write code like result = obj.eval(17) instead. The problem with that is that we can't use the class Eval in the code directly, because the class doesn't yet exist when the code is compiled.

Making It Nice

Interfaces to the rescue! The class we create has only a single method we care about (eval), and we know what its method signature will be. So we can move the method into an interface, and have the to-be-created class implement this interface. Then we can cast the object to the interface in step 5, and call the eval method directly. This is the interface we'll implement:

public interface Evaluator {
    public double eval (double x);
}

This is easy to do with Javassist. The following line declares that our class will implement Evaluator:

evalClass.setInterfaces(
        new CtClass[] { pool.makeClass("Evaluator") });

Then we can cast to it, and call eval:

Evaluator obj = (Evaluator) clazz.newInstance();
double result = obj.eval(17);
System.out.println(result);

This is what Example2 does. It is run in the same way as Example1, except that it also takes the numeric argument from the command line, so that you can evaluate the formula using other values besides 17.

One small additional change is that a timestamp is appended to the classname. This doesn't change anything visibly –because we don't really care about the class name– but it serves as a minimal guard against threading problems in case the class-constructing code is ever run in a multi-threaded environment.

Adding Convenience

After using this code for a while you might want to evaluate more complex formulas, containing constants and functions. No problem – the following lets us use "pi" instead of having to type "3.141592..." in our formulas all the time:

evalClass.addField(
        CtField.make("private double pi = Math.PI;", evalClass));

We can add random numbers by making use of the java.util.Random class. The following lines add a field for a Random object, a constructor that initializes it, and a rnd() method that returns a random number between 0 and 1.

evalClass.addField(
        CtField.make("private java.util.Random rnd;", evalClass));

CtConstructor constr = CtNewConstructor.defaultConstructor(evalClass);
constr.setBody("{ rnd = new java.util.Random(System.currentTimeMillis()); }");
evalClass.addConstructor(constr);

evalClass.addMethod(
        CtNewMethod.make("public double rnd() { return rnd.nextDouble(); }", evalClass));

Example3 shows this in action. Note that both adding a field and adding a body to the default constructor works in much the same way as adding methods: you supply the source code directly to the appropriate make method, and add the result to evalClass.

Even More Convenience

Once you've added a few more functions and constants in the way described above, you're probably wondering if there isn't a more convenient way to do this. Dealing with Javassist's various classes, and having to maintain source code as part of a string inside of other source code is not exactly fun.

Subclassing to the rescue! So far, the code has implemented the Evaluator interface, which didn't allow us to supply any methods to go along with the eval method. But nothing stops us from creating a class that extends some other class that has implementations of all kinds of nifty functions. Look at the source code of the Expr class – it does just that.

Example4 implements this. We just need to get a CtClass object for the Expr class:

CtClass exprClass = pool.get("Expr");

And then make that the superclass of our expression-evaluating class:

evalClass.setSuperclass(exprClass);

That was easy. Now any additional functions can easily be maintained as part of the Expr class. An alternative to implementing Evaluator in this scenario would be to make eval an abstract method of Expr instead. Whether one or the other makes more sense depends on how the class will be used; functionally the approaches are equivalent.

Further Possibilities

Expression evaluation isn't limited to functions taking a single parameter, of course. Example5 shows a program that takes either 1 or 2 parameters, and generates the class accordingly.

These examples have only scratched the surface of what Javassist can do. In addition to creating new classes, it's also possible to modify existing ones, and to remove fields or methods from a class.

The CtClass class also has methods for writing the bytecode of a class to disk or obtaining it in a byte[], in case it's necessary to preserve it beyond its one-time use.

Sometimes a class depends on other classes; in those cases there are ways to tell the ClassPool about what to use as classpath.

There's also a low-level API for dealing with bytecode directly.

Conclusion

This article showed how to create Java classes dynamically, based on information available at runtime. It then used those classes to evaluate mathematical expressions that were not known beforehand. Both these are problems one doesn't encounter frequently, but they do occur, and neither is trivial to solve. But knowing that it's not all that hard, additional ways to make use of them probably suggest themselves.