Избегайте непроверенного задания на карте с несколькими типами значений?

У меня возникают проблемы с предупреждением в Java 7:

Unchecked assignment: 'java.lang.Class' to 'java.lang.Class<T>'

Я получаю его на линии Class<T> type = typeMap.get(key); в функции get ниже.

В основном, что я пытаюсь сделать здесь, я хочу хранить кучу пар ключ/значение неизвестных типов (но все они являются потомками Object, за исключением null), но не теряют тип. Поэтому я создал класс со следующим контентом с использованием дженериков. Он имеет две карты (один для хранения данных и один для хранения типа класса:

    private Map<String, Object>  dataMap = new HashMap<>();
    private Map<String, Class> typeMap = new HashMap<>();

    public  <T> void put(String key, T instance){
        dataMap.put(key, instance);
        if (instance == null){
            typeMap.put(key,null);
        }
        else {
            typeMap.put(key, instance.getClass());
        }
    }

    public <T> T get(String key){
        Class<T> type = typeMap.get(key);
        if (type == null){
            return null;
        }
        return type.cast(dataMap.get(key));
    }

Он работает отлично, но предупреждение раздражает меня. Есть ли способ заставить Java делать это приведение без жалобы (кроме подавления)? Или есть лучший способ добиться того, что я пытаюсь сделать? Как насчет в Java 8, поскольку у меня еще не было возможности погрузиться в нее?

Спасибо!

Ответы

Ответ 1

Причина, по которой вы показали, небезопасна, заключается в том, что с этим назначением:

Class<T> type = typeMap.get(key);

T не должен иметь ничего общего с Class, извлеченным с карты. T всегда выводится из окружающего контекста вызова get. Например, я могу выполнить эту последовательность вызовов:

// T is inferred from the arguments as String (which is fine)
example.put("k", "v");
// T is inferred from the return value target type as Integer
Integer i = example.get("k");

Внутри метода get String.class корректно извлекается из карты типов, но неконтролируемое преобразование выполняется в Class<Integer>. Вызов type.cast(...) не выбрасывается, потому что значение, полученное из карты данных, равно String. Неявное проверочное литье фактически происходит с возвращаемым значением, отбрасывая его на Integer и ClassCastException.

Это странное взаимодействие связано с стиранием типа.


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

1. Мы можем отказаться от компиляции, если нет возможности их выполнить.

Сохранение Class здесь бессмысленно, потому что, как я показал выше, он не выполняет полезную проверку. Таким образом, мы могли бы перепроектировать карту по следующим строкам:

class Example {
    private final Map<String, Object> m = new HashMap<>();

    void put(String k, Object v) {
        m.put(k, v);
    }

    Object getExplicit(String k) {
        return m.get(k);
    }

    @SuppressWarnings("unchecked")
    <T> T getImplicit(String k) {
        return (T) m.get(k);
    }
}

getExplicit и getImplicit делают аналогичную вещь, но:

String a = (String) example.getExplicit("k");
// the generic version allows an implicit cast to be made
// (this is essentially what you're already doing)
String b = example.getImplicit("k");

В обоих случаях мы просто полагаемся на наше собственное осознание как программиста, чтобы не ошибаться.

Подавление предупреждений не обязательно плохо, просто важно понять, что они на самом деле означают и каковы последствия.

2. Передайте a Class в get, чтобы возвращаемое значение было действительным.

Вот как я обычно это видел.

class Example {
    private final Map<String, Object> m = new HashMap<>();

    void put(String k, Object v) {
        m.put(k, v);
    }

    <T> T get(String k, Class<T> c) {
        Object v = m.get(k);
        return c.isInstance(v) ? c.cast(v) : null;
    }
}

example.put("k", "v");
// returns "v"
String s = example.get("k", String.class);
// returns null
Double d = example.get("k", Double.class);

Но, конечно, это означает, что нам нужно передать два параметра в get.

3. Параметрируйте клавиши.

Это новый, но более продвинутый, и он может быть или не быть более удобным.

class Example {
    private final Map<Key<?>, Object> m = new HashMap<>();

    <V> Key<V> put(String s, V v) {
        Key<V> k = new Key<>(s, v);
        put(k, v);
        return k;
    }

    <V> void put(Key<V> k, V v) {
        m.put(k, v);
    }

    <V> V get(Key<V> k) {
        Object v = m.get(k);
        return k.c.isInstance(v) ? k.c.cast(v) : null;
    }

    static final class Key<V> {
        private final String k;
        private final Class<? extends V> c;

        @SuppressWarnings("unchecked")
        Key(String k, V v) {
            // this cast will always be safe unless
            // the outside world is doing something fishy
            // like using raw types
            this(k, (Class<? extends V>) v.getClass());
        }

        Key(String k, Class<? extends V> c) {
            this.k = k;
            this.c = c;
        }

        @Override
        public int hashCode() {
            return k.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            return (o instanceof Key<?>) && ((Key<?>) o).k.equals(k);
        }
    }
}

Так, например:

Key<Float> k = example.put("k", 1.0f);
// returns 1.0f
Float f = example.get(k);
// returns null
Double d = example.get(new Key<>("k", Double.class));

Это может иметь смысл, если записи известны или предсказуемы, поэтому мы можем иметь что-то вроде:

final class Keys {
    private Keys() {}
    static final Key<Foo> FOO = new Key<>("foo", Foo.class);
    static final Key<Bar> BAR = new Key<>("bar", Bar.class);
}

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

Foo foo = example.get(Keys.FOO);

4. У вас нет карты, в которую можно положить какой-либо объект, используйте какой-либо полиморфизм.

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

Простым примером может быть следующее:

// bunch of stuff
Map<String, Object> map = ...;

// store some data
map.put("abc", 123L);
map.put("def", 456D);

// wait awhile
awhile();

// some time later, consume the data
// being particular about types
consumeLong((Long) map.remove("abc"));
consumeDouble((Double) map.remove("def"));

И мы могли бы заменить что-то вроде этого:

Map<String, Runnable> map = ...;

// store operations as well as data
// while we know what the types are
map.put("abc", () -> consumeLong(123L));
map.put("def", () -> consumeDouble(456D));

awhile();

// consume, but no longer particular about types
map.remove("abc").run();
map.remove("def").run();

Ответ 2

Вы пытаетесь присвоить элемент типа Class переменной типа Class<T>. Конечно, это непроверенное задание. Кажется, вы реализуете разнородную карту. Java (и любой другой строго типизированный язык) не может выразить тип значения вашей карты статически-безопасным способом.

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