Named parameters in Java

I was recently writing some Java factory code that would have benefited from named parameters. Java unfortunately does not support named parameters. I searched around the web for some solutions that could provide a work-around but did not find anything.

I was trying to solve a problem that seems common when you are implementing a factory. Imagine a factory that creates objects but does not or should not know what parameters these objects take for initialization. The factory interface might look like this in Java:

public interface Factory {
    public Result create(Object... parameters);
}

The above has two issues. Object... (and its equivalent Object[]) does not provide any type safety. And parameters depend on the position in the array, which is very error-prone. Code using the above would look like this:

Type1 parameter1 = (Type1)parameters[0];
Type2 parameter2 = (Type2)parameters[1]

You might ask yourself why I do not just use something like "String... parameters" for type safety? That is fine as long as you can be sure that all your objects are only initialized with Strings and no parameters of any other type that is not a subtype of String. And you still need to be very careful that all parameters are always passed in the correct order.

The obvious improvement over using an array or a list to pass arbitrary parameters is the use of a Map. If you use parameter names as map keys and the actual parameter as the map values, you are somewhat closer to real named parameters already. This is how the factory interface could look like then:

public interface Factory {
    public Result create(Map<String, Object> parameters);
}

The drawback with the above is that any code using the parameters must cast from Object to the actual type like this:

Type1 parameter1 = (Type1)parameters.get("parameter name 1");
Type2 parameter2 = (Type2)parameters.get("parameter name 2");

When we reviewed this code, someone suggested a solution using Generics instead:

public interface Factory {
    public <T> Result create(Map<String, T> parameters);
}

Unfortunately, that approach does not work because it forces you to use the same type T for all parameters. In other words, it is the Generics equivalent to the "String... parameters" approach discussed earlier.

We can take the Map approach one step further if we introduce one additional layer of indirection. Instead of using a Map with the same type T for all values, we can implement a wrapper that allows to use an individual type for each parameter:

public class NamedParameters {

    private final HashMap<String, Object> nameToInstance = new HashMap<String, Object>();

    /\*\*
     \* Add parameter with the given name.
     \*
     \* @param <T> The type of the parameter.
     \* @param name The name of the parameter.
     \* @param parameter The parameter.
     \* @return This instance of NamedParameters (so that you can chain multiple put calls).
     \*/
    public <T> NamedParameters put(String name, T parameter) {
        this.nameToInstance.put(name, parameter);
        return this;
    }

    /\*\*
     \* Get parameter with the given name and type. Returns null if a parameter
     \* with the name exists but has a different type.
     \*
     \* @param <T> The type of the parameter.
     \* @param name The name of the parameter.
     \* @return The parameter with the given name or null.
     \*/
    @SuppressWarnings("unchecked")
    public <T> T get(String name) {
        try {
            return (T) this.nameToInstance.get(name);
        } catch (ClassCastException e) {
            return null;
        }
    }

}

The factory would now look like this:

public interface Factory {
    public Result create(NamedParameters parameters);
}

And you would use it like this:

Type1 parameter1 = parameters.get("parameter name 1");
Type2 parameter2 = parameters.get("parameter name 2");

You might have noticed the @SuppressWarnings("unchecked") annotation on the NamedParameters get method. There may be ways to avoid using this annotation but ultimately you cannot hide that Java type erasure strikes and the type information of the stored value object is lost. That is why we explicitly have to cast and return a null value if the types do not match. This example demonstrates the issue:

NamedParameters parameters = (new NamedParameters()).put("parameter name 1", "text")
                             .put("parameter name 2", new Integer(5));
String parameter1 = parameters.get("parameter name 1");
// parameter2 will be null:
Boolean parameter2 = parameters.get("parameter name 2");

Depending on your exception handling strategy, you might want to throw an exception instead of returning null in the get method.

Tags:

Comments:

Post a Comment:
Comments are closed for this entry.
About

ritzmann

Search

Categories
Archives
« April 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today