Обобщения с необязательными множественными границами, например, List<? расширяет целочисленное или строковое значение>

У меня есть метод, который должен принимать только Map чей ключ имеет тип String и значение типа Integer или же Stringно не, скажем, Boolean,

Например,

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

Невозможно объявить карту, как показано ниже (для проверки времени компиляции).

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

Какова лучшая альтернатива, кроме объявления типа значения как unbounded wildcard и проверка для Integer или же String во время выполнения?

void setProperties(Map<String, ?> properties)

Этот метод принимает набор свойств для настройки базового объекта службы. Сущность поддерживает значения свойств типа String а также Integer в одиночестве. Например, свойство maxLength=2 является действительным, defaultTimezone=UTC также действует, но allowDuplicate=false является недействительным.

5 ответов

Решение

Вы не можете объявить переменную типа как один из двух типов. Но вы можете создать вспомогательный класс для инкапсуляции значений, не имеющих публичного конструктора, но фабричных методов для выделенных типов:

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

}

Если вы используете сторонние библиотеки со сборщиками карт, вам не нужно map методы, они удобны только для коротких карт. С помощью этого шаблона вы можете вызвать метод как

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

Так как есть только два Value фабричные методы для int а также Stringни один другой тип не может быть передан на карту. Обратите внимание, что это также позволяет использовать int как тип параметра, так что расширение byte, short и т. д. int здесь возможно

Другое решение будет на заказ Map реализация и переопределения put а также putAll методы проверки данных:

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: используйте Map реализация, которая соответствует вашим потребностям в качестве базового класса

Поскольку Integer а также String ближайший общий предок в иерархии классов Object вы не можете достичь того, что вы пытаетесь сделать - вы можете помочь компилятору сузить тип до Object только.

Вы также можете

  • оберните ваше значение в класс, который может содержать Integer или же String, или же
  • простираться Map как в ответе @RC, или

  • завернуть 2 Mapс в классе

Определите две перегрузки:

void setIntegerProperties(Map<String, Integer> properties)

void setStringProperties(Map<String, String> properties)

Их нужно называть разными вещами, потому что нельзя использовать два метода с одним и тем же стиранием.

Я вполне уверен, что если какой-либо язык запретит использование нескольких допустимых типов для значения, это будет Java. Если вам действительно нужны такие возможности, я бы посоветовал поискать другие языки. Python определенно может это сделать.

Какой смысл использовать и целые числа, и строки в качестве значений для вашей карты? Если мы действительно имеем дело только с целыми числами и строками, вам придется либо:

  1. Определите объект-оболочку, который может содержать строку или целое число. Я бы посоветовал против этого, потому что это будет очень похоже на другое решение ниже.
  2. Выберите String или Integer в качестве значения (String кажется более простым выбором), а затем просто выполните дополнительную работу за пределами карты для работы с обоими типами данных.
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

Это очень уродливое решение, но, вероятно, это единственное разумное решение, которое может быть предоставлено с использованием Java, чтобы поддерживать ощущение строгой типизации ваших значений.

Другие вопросы по тегам