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

1.9k Views Asked by At

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.

5

There are 5 best solutions below

3
On BEST ANSWER

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.

6
On

Since Integer and String closest common ancestor in the class hierarchy is Object you cannot achieve what you are trying to do - you can help compiler to narrow the type to Object only.

You can either

  • wrap your value into a class which can contain either Integer or String, or
  • extend Map as in the @RC's answer, or

  • wrap 2 Maps in a class

1
On

Define two overloads:

void setIntegerProperties(Map<String, Integer> properties)

void setStringProperties(Map<String, String> properties)

They have to be called different things, because you can't have two methods with the same erasure.

0
On

I'm fairly certain if any language was going to disallow multiple accepted types for a value, it would be Java. If you really need this kind of capability, I'd suggest looking into other languages. Python can definitely do it.

What's the use case for having both Integers and Strings as the values to your map? If we are really dealing with just Integers and Strings, you're going to have to either:

  1. Define a wrapper object that can hold either a String or an Integer. I would advise against this though, because it will look a lot like the other solution below.
  2. Pick either String or Integer to be the value (String seems like the easier choice), and then just do extra work outside of the map to work with both data types.
Map<String, String> map;
Integer myValue = 5;
if (myValue instanceof Integer) {
    String temp = myValue.toString();
    map.put(key, temp);
}

// taking things out of the map requires more delicate care.
try { // parseInt() can throw a NumberFormatException
    Integer result = Integer.parseInt(map.get(key)); 
}
catch (NumberFormatException e) {} // do something here

This is a very ugly solution, but it's probably one of the only reasonable solutions that can be provided using Java to maintain some sense of strong typing to your values.

2
On

Another solution would be a custom Map implementation and overrides of the put and putAll methods to validate the data:

public class ValidatedMap extends HashMap<String, Object> {
    @Override
    public Object put(final String key, final Object value) {
        validate(value);
        return super.put(key, value);
    }

    @Override
    public void putAll(final Map<? extends String, ?> m) {
        m.values().forEach(v -> validate(v));
        super.putAll(m);
    }

    private void validate(final Object value) {
        if (value instanceof String || value instanceof Integer) {
            // OK
        } else {
            // TODO: use some custom exception
            throw new RuntimeException("Illegal value type");
        } 
    }
}

NB: use the Map implementation that fits your needs as base class