In the last post I ranted about dependency injection framework magic, but even custom magic can cause problems. When objects are constructed and invoked through reflection, the execution flow is no longer apparent in the code. Yet, it still has its place in certain applications, typically mapping external input to object instances, be it language parsing or lexing, or DB output to object mapping. But how to best balance between the abstract world of reflected classes and concrete type-safe code?

The key lies in simplicity, and imposing certain constrains. Dynamically resolving classes and recursive reflective construction of a full object a hierarchy is probably not a good idea. Furthermore, as discussed previously, Java 8 method references now makes it cleaner to pass in factory methods, and avoid the Factory Factory Factory trap.

The following snippet shows the reflective construction of an object, given a few constraints: First off, the name of the input token can be mapped to typed classes. In this example that is done with a generated String to Class map, based on the concrete class references we’re dealing with. They have to be available at compile time, and at the time of writing the code. The advantage is that we avoid the Class.forName() look-up, and the package hierarchy confusion and possible mismatch of tokens and classes. If this is too strict, we can modify the map, e.g. by allowing for case-insensitive matching (by converting both map key and look-up to the same case). Or if there is not a literal mapping, an enum could define the relationship. Either way, the idea is that the code make it clear which classes we plan to deal with, in strongly typed manner.

The next assumption in this example is that the class to be created has only one constructor method, and that it is public. Or if that is not feasible, the constructor to be used could be marked with an Annotation. Just don’t go full-on Guice, and you’ll be fine.

Finally, we assume that the relevant constructor takes zero or more parameters, and if it does have parameters that they can themselves we represented and created from a single String object. These parameter objects are initialized in a helper method, discussed below.

  List<Class<?>> validClasses = Arrays.asList(Foo.class, Account.class);

  Map<String, Class<?>> classNameMap = validClasses.stream()
      .collect(Collectors.toMap(c -> c.getSimpleName(), Function.identity()));

  Object construct(String type, String... args) throws Exception {
    if (!classNameMap.containsKey(type)) {
      throw new IllegalArgumentException("Invalid class name: " + type);
    }

    Class<?> klass = classNameMap.get(type);
    Constructor<?>[] constructors = klass.getConstructors();
    Constructor<?> constructor = constructors[0];

    Class<?>[] parameterTypes = constructor.getParameterTypes();
    Object[] parameters = createParameters(parameterTypes, args);
    return constructor.newInstance(parameters);
  }

The construction of the parameter classes also contains several assumptions and restrictions. As with the first order classes, we limit ourselves to a pre-defined set of classes. This make it possible to define which constructor or factory methods should be used. In this example, the constructor which takes a single String is used, except for the Password class, where a factory method is used, again taking a single String.

  Map<Class<?>, Function<String, ?>> classConstructorMap = new HashMap<Class<?>, Function<String, ?>>() { {
    put(String.class, String::new);
    put(Integer.TYPE, Integer::new);
    put(Double.class, Double::new);
    put(Email.class, Email::new);
    put(Password.class, Password::hash);
  } };

  Object[] createParameters(Class<?>[] types, String[] args) {
    if (types.length != args.length) {
      throw new IllegalArgumentException("Expects: " + Arrays.asList(types));
    }

    Object[] result = new Object[types.length];
    for (int i = 0; i < types.length; i++) {
      Class<?> klass = types[i];
      Function<String, ?> constuctor = classConstructorMap.get(klass);
      if (constuctor == null) {
        throw new IllegalArgumentException("Constructor for " + klass + " not declared.");
      }
      result[i] = constuctor.apply(args[i]);
    }
    return result;
  }

That is all it takes to construct objects through reflection, including its parameter types. The examples above maintain some type-safety, and also restricts the supported types. Finally, some error handling and messaging is in place, and more could be added to make it very clear which classes and input is allowed.

The full code listing below shows the example classes to be constructed and test code to verify them.

ObjectConstructorTest.java
GitHub Raw
/* Copyright rememberjava.com. Licensed under GPL 3. See http://rememberjava.com/license */
package com.rememberjava.reflect;

import static org.junit.Assert.assertEquals;

import java.lang.reflect.Constructor;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.junit.Test;

public class ObjectConstructorTest {

  static class Foo {
    final String str;
    final int i;
    final double d;

    public Foo(String str, int i, Double d) {
      this.str = str;
      this.i = i;
      this.d = d;
    }
  }

  static class Account {
    final Email email;
    final Password password;

    public Account(Email email, Password password) {
      this.email = email;
      this.password = password;
    }
  }

  static class Email {
    final String email;

    Email(String email) {
      this.email = email;
    }
  }

  static class Password {
    final String hash;

    Password(String hash) {
      this.hash = hash;
    }

    static Password hash(String pw) {
      try {
        MessageDigest digest = MessageDigest.getInstance("SHA");
        digest.update(pw.getBytes());
        digest.update("some salt".getBytes());

        String base64 = Base64.getEncoder().encodeToString(digest.digest());

        return new Password(base64);
      } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
      }
      return null;
    }
  }

  List<Class<?>> validClasses = Arrays.asList(Foo.class, Account.class);

  Map<String, Class<?>> classNameMap = validClasses.stream()
      .collect(Collectors.toMap(c -> c.getSimpleName(), Function.identity()));

  Object construct(String type, String... args) throws Exception {
    if (!classNameMap.containsKey(type)) {
      throw new IllegalArgumentException("Invalid class name: " + type);
    }

    Class<?> klass = classNameMap.get(type);
    Constructor<?>[] constructors = klass.getConstructors();
    Constructor<?> constructor = constructors[0];

    Class<?>[] parameterTypes = constructor.getParameterTypes();
    Object[] parameters = createParameters(parameterTypes, args);
    return constructor.newInstance(parameters);
  }

  @SuppressWarnings("serial")
  Map<Class<?>, Function<String, ?>> classConstructorMap = new HashMap<Class<?>, Function<String, ?>>() { {
    put(String.class, String::new);
    put(Integer.TYPE, Integer::new);
    put(Double.class, Double::new);
    put(Email.class, Email::new);
    put(Password.class, Password::hash);
  } };

  Object[] createParameters(Class<?>[] types, String[] args) {
    if (types.length != args.length) {
      throw new IllegalArgumentException("Expects: " + Arrays.asList(types));
    }

    Object[] result = new Object[types.length];
    for (int i = 0; i < types.length; i++) {
      Class<?> klass = types[i];
      Function<String, ?> constuctor = classConstructorMap.get(klass);
      if (constuctor == null) {
        throw new IllegalArgumentException("Constructor for " + klass + " not declared.");
      }
      result[i] = constuctor.apply(args[i]);
    }
    return result;
  }

  @Test
  public void testFoo() throws Exception {
    Foo foo = (Foo) construct("Foo", "ABC", "123", "3.14");
    assertEquals("ABC", foo.str);
    assertEquals(123, foo.i);
    assertEquals(3.14, foo.d, 2);
  }

  @Test
  public void testAccount() throws Exception {
    Account account = (Account) construct("Account", "bob@example.com", "some password");
    assertEquals("V0PYfuPn4u9Gize+0DZ0nLgQQPk=", account.password.hash);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testUndefinedClass() throws Exception {
    construct("NotFound");
  }

  @Test(expected = NumberFormatException.class)
  public void testInvalidParameterType() throws Exception {
    construct("Foo", "ABC", "ABC", "3.14");
  }

  @Test(expected = IllegalArgumentException.class)
  public void testInvalidParameterCount() throws Exception {
    construct("Foo", "ABC");
  }
}