generic - java type conversion




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

У меня проблемы с предупреждением в 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, поскольку у меня еще не было возможности погрузиться в него?

Спасибо!


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

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


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

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(...) не type.cast(...) , потому что значение, полученное из карты данных, является String . Затем неявное проверенное приведение фактически происходит с возвращаемым значением , ClassCastException его к 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. Передайте 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();




casting