ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Lisp and Java
Pages: 1, 2, 3

Approximating First-Class Functions in Java

The only parts of our JDBC example that would change if we were collecting a different type of object would be:
  1. The connection, which was obtained outside of the block shown.
  2. The string used in the query.
  3. The body of the while loop.

The connection and the string are easy, since we can pass them in to a method that captures the pattern. The body of the while loop is tougher, because it's well, a chunk of code, which is hard to manipulate in Java. In general, if you want to pass around code in Java, you have to create an instance of a class. Thanks to interfaces and anonymous classes, we can create an anonymous class just for the code in question.



To start our translation of the Lisp map idiom into Java, we define the following interface:

interface RowFunc {
  public Object of(ResultSet rs) throws SQLException;
}

An object that implements RowFunc is sort of like a function in Lisp, in that it can be called (by invoking the of method), passed to another function, returned from a function, and stored in a data structure. Of course, we have to explicitly apply it, but it's a start. We call RowFunc's method of so that you can read an invocation of a RowFunc named f as f "of" whatever it's applied to. I have a mathematical background (and a possibly too-great love of concision). In any event, here's how we use the interface to abstract our "collect all of the objects in the ResultSet" pattern:

static List rsMapQuery(Connection conn,
                       RowFunc f,
                       String query) throws SQLException {
  Statement stmt = conn.createStatement();
  ResultSet rs = stmt.executeQuery(query);
  ArrayList result = new ArrayList();
  while(rs.next()) {
    result.add(f.of(rs));
  }
  return result;  
}

You'll note that we don't first pull all of the results into a List and then map over that. Although that would be a perfectly valid way to work, JDBC ResultSets are common enough (and there's enough interesting ResultSetMetaData in them), that it seemed worthwhile to have a special purpose method to map from a ResultSet to a List. Later in the article, we develop a nice way to go from a ResultSet to a List of HashMaps, which is about as general as you can get when dealing with unknown data.

With these general definitions in place, we can now rewrite our earlier specific query using an anonymous inner class as:

RowFunc makeUser = new RowFunc() {
    public Object of(ResultSet rs) throws SQLException {
      String fname = rs.getString("first_name");
      String lname = rs.getString("last_name");
      String uid = rs.getString("user_id");
      return new User(fname, lname, uid);
    }
  };

List users =
  rsMapQuery(conn,
             makeUser,
             "SELECT first_name, last_name, user_id " +
             "FROM users");

Now, in this one case, we haven't saved much in the way of typing, though it is certainly nice that the JDBC boilerplate has disappeared from our code. However, if we need to create a list of users again somewhere else, we can just pass in makeUser again:

List newUsers =
  rsMapQuery(conn,
             makeUser,
             "SELECT first_name, last_name, user_id " +
             "FROM users WHERE type = 'new' ");

If "create a User from a row of a ResultSet" turns out to be a useful concept, having abstracted it into a function that can be easily manipulated will prove its value over time. Speaking of easy manipulation, wouldn't it be nice if we stored the makeUser function with the User class? As mentioned above, we can store these function-like objects in data structures:

class User {

  public static RowFunc make = new RowFunc() {
    public Object of(ResultSet rs) throws SQLException {
      return new User(rs);
    }
  };

  public User(ResultSet rs) throws SQLException {
    this.fname = rs.getString("first_name");
    this.lname = rs.getString("last_name");
    this.uid = rs.getString("user_id");
  }
}

Note that before, User had a constructor that took three params, whereas now we're just directly passing in the ResultSet. This eliminates the possibility of confusing the order of the args. Now our call looks like this:

List users =
  rsMapQuery(conn,
             User.make,
             "SELECT first_name, last_name, user_id " +
             "FROM users");

Let's keep improving things. Every class we're going to fit into this paradigm is going to have a nearly identical make static method. That seems a bit wasteful. What if we could write a function that returned a function to do it for us? We can do that, using a class constructor and a bit of reflection:

public class RFMaker implements RowFunc {
  private Constructor _cons;

  public RFMaker(Class c) {
    try {
      _cons =
        c.getConstructor(new Class[] { ResultSet.class });
    }
    catch(NoSuchMethodException e) {
      throw new IllegalArgumentException(
        "RFMaker must be called with a class " +
        "which has a ResultSet constructor");
    }
  }

  public Object of(ResultSet rs) throws SQLException {
    try {
      return _cons.newInstance(new Object[] { rs });
    }
    catch(InstantiationException ie) {
      throw new RuntimeException(
        "RFMaker failed due to: " + ie);
    }
    catch(IllegalAccessException iae) {
      throw new RuntimeException(
        "RFMaker failed due to: " + iae);
    }
    catch(InvocationTargetException ite) {
      try {
        SQLException e =
         (SQLException) ite.getTargetException();     
        throw e;
      }
      catch(ClassCastException cce) {
        throw new RuntimeException(
          "RFMaker failed due to: " + ite);
      }
    }
  }
}

The complexity of using reflection here is a clear demonstration of why it's too tricky to use in day-to-day programming. As part of an infrastructure (which is how we can think of RFMaker), it's worth the effort, but the above multi-exception monstrosity should never appear in normal code. It's not clear exactly how to best handle the host of checked exceptions. We opt for a fail-fast runtime exception, which seems reasonable.

Pages: 1, 2, 3

Next Pagearrow