Menu

Reflection Machines

Conventional wisdom says that reflection is slow and that it should be avoided. As always, the truth is more nuanced.

Let’s start with some code.

class Movie {  
  static final List<Movie> BEST_MOVIES = Arrays.asList(
      new Movie("Back to the Future", 1985),
      new Movie("Back to the Future 2", 1989),
      new Movie("Jurassic Park", 1993),
      new Movie("Starship Troopers", 1997));

  final String name;
  final int releaseYear;

  public Movie(String name, int releaseYear) {
    this.name = name;
    this.releaseYear = releaseYear;
  }
}

We’ve got a movie class. Next we’ll write some gratuitous reflection. How about a logging API with reflection-based string interpolation?

  for (Movie movie : Movie.BEST_MOVIES) {
    FancyLogger.log("I loved $name. Did it win best picture in $releaseYear?!", movie);
  }

The hasty implementation is easy:

public final class FancyLogger {  
  public static void log(String message, Object value) {
    try {
      for (Field field : value.getClass().getDeclaredFields()) {
        field.setAccessible(true);
        String fieldValue = String.valueOf(field.get(value));
        message = message.replaceAll("\\$" + field.getName(), fieldValue);
      }
      System.out.println(message);
    } catch (IllegalAccessException e) {
      throw new AssertionError(e);
    }
  }
}

Unfortunately, the code above is why reflection is slow. The problem isn’t that we’re doing reflection, but that we’re doing reflection every single time we log. Calling getDeclaredFields() is quick, but it’s not so quick that we should do it over and over again. That’s unnecessary work and unnecessary work is what earned reflection its bad reputation.

Instead, we make an API change to all of the reflection up front, only once. Our calling code now looks like this:

  static final FancyLogger<Movie> movieLogger = new FancyLogger<>(Movie.class);

  ...

  for (Movie movie : Movie.BEST_MOVIES) {
    movieLogger.log("I loved $name, it was the best movie in $releaseYear!", movie);
  }

With this, the implementation can now lookup Movie’s fields once and reuse them on each call.

public final class FancyLogger<T> {  
  private final Field[] declaredFields;

  public FancyLogger(Class<T> type) {
    this.declaredFields = type.getDeclaredFields();
    for (Field field : declaredFields) {
      field.setAccessible(true);
    }
  }

  void log(String message, Object value) {
    try {
      for (Field field : declaredFields) {
        String fieldValue = String.valueOf(field.get(value));
        message = message.replaceAll("\\$" + field.getName(), fieldValue);
      }
      System.out.println(message);
    } catch (IllegalAccessException e) {
      throw new AssertionError(e);
    }
  }
}

By moving the reflection up and out of the loop, we end up building a little machine that is optimized for a specific Java type.

This pattern is the key to the design of Moshi, Square’s library that uses reflection to convert between Java objects and JSON. The Moshi class builds little JsonAdapter machines, each of which knows how to handle a single Java class. If you ask Moshi for a JsonAdapter<Movie>, it’ll reflect on the Movie class to construct a JSON adapter that you can use over and over again.

Regex Machines

Naturally, this approach isn’t limited to reflection. I cringe when I see regular expressions being constructed and garbage collected when they should be reused. This happens when you use String.matches(), String.split(), and String.replaceAll().

  for (Movie movie : Movie.BEST_MOVIES) {
    if (movie.name.matches(".*\\s+\\d+$")) {
      movieLogger.log("$name proves that sequels can be great!", movie);
    }
  }

The fix here is the same thing. Build the machine once and reuse it.

  static final Pattern SEQUEL_PATTERN = Pattern.compile(".*\\s+\\d+$");

  ...

  for (Movie movie : Movie.BEST_MOVIES) {
    if (SEQUEL_PATTERN.matcher(movie.name).matches()) {
      movieLogger.log("$name proves that sequels can be great!", movie);
    }
  }

Reusable Machines

Often the design of APIs lures us into doing the same work multiple times. For example, with Gson the Gson.toJson() API needs to inspect your type to build a cache key every time you encode a value.

In Moshi, the API encourages you to get a JsonAdapter once and reuse it. We want to avoid offering convenience APIs that make your code slower if you use them.

When you’re designing APIs, consider generating reusable machines. They can make your code faster!