Search code examples
javagenericsunbounded-wildcard

Generics with optional multiple bounds, e.g. List<? extends Integer OR String>


I have a method that should only accept a Map whose key is of type String and value of type Integer or String, but not, say, Boolean.

For example,

map.put("prop1", 1); // allowed
map.put("prop2", "value"); // allowed
map.put("prop3", true); // compile time error

It is not possible to declare a Map as below (to enforce compile time check).

void setProperties(Map<String, ? extends Integer || String> properties)

What is the best alternative other than declaring the value type as an unbounded wildcard and validating for Integer or String at runtime?

void setProperties(Map<String, ?> properties)

This method accepts a set of properties to configure an underlying service entity. The entity supports property values of type String and Integer alone. For example, a property maxLength=2 is valid, defaultTimezone=UTC is also valid, but allowDuplicate=false is invalid.


Solution

  • You can’t declare a type variable to be either of two types. But you can create a helper class to encapsulate values not having a public constructor but factory methods for dedicated types:

    public static final class Value {
        private final Object value;
        private Value(Object o) { value=o; }
    }
    public static Value value(int i) {
        // you could verify the range here
        return new Value(i);
    }
    public static Value value(String s) {
        // could reject null or invalid string contents here
        return new Value(s);
    }
    // these helper methods may be superseded by Java 9’s Map.of(...) methods
    public static <K,V> Map<K,V> map(K k, V v) { return Collections.singletonMap(k, v); }
    public static <K,V> Map<K,V> map(K k1, V v1, K k2, V v2) {
        final HashMap<K, V> m = new HashMap<>();
        m.put(k1, v1);
        m.put(k2, v2);
        return m;
    }
    public static <K,V> Map<K,V> map(K k1, V v1, K k2, V v2, K k3, V v3) {
        final Map<K, V> m = map(k1, v1, k2, v2);
        m.put(k3, v3);
        return m;
    }
    public void setProperties(Map<String, Value> properties) {
        Map<String,Object> actual;
        if(properties.isEmpty()) actual = Collections.emptyMap();
        else {
            actual = new HashMap<>(properties.size());
            for(Map.Entry<String, Value> e: properties.entrySet())
                actual.put(e.getKey(), e.getValue().value);
        }
        // proceed with actual map
    
    }
    

    If you are using 3rd party libraries with map builders, you don’t need the map methods, they’re convenient for short maps only. With this pattern, you may call the method like

    setProperties(map("mapLength", value(2), "timezone", value("UTC")));
    

    Since there are only the two Value factory methods for int and String, no other type can be passed to the map. Note that this also allows using int as parameter type, so widening of byte, short etc. to int is possible here.