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



class to (3)

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

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();

У меня проблемы с предупреждением в 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 (и любой другой строго типизированный язык) не может выразить тип значения вашей карты в статически безопасном виде.

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


EDIT : Начиная с Mockito 1.10.x, типы дженериков, встроенные в класс, теперь используются Mockito для глубоких заглушек. то есть.

public interface A<T extends Observer & Comparable<? super T>>  {
  List<? extends B> bList();
  T observer();
}

B b = deep_stubbed.bList().iterator().next(); // returns a mock of B ; mockito remebers that A returns a List of B
Observer o = deep_stubbed.observer(); // mockito can find that T super type is Observer
Comparable<? super T> c = deep_stubbed.observer(); // or that T implements Comparable

Mockito изо всех сил пытается получить информацию о типе, которую использует компилятор, но когда применяется стирание, mockito не может ничего сделать, кроме как вернуть макет Object .

Оригинал : Ну, это больше проблема с дженериками, чем с Mockito. Для дженериков вы должны прочитать, что написала на них Анджелика Лангер . И для текущей темы, то есть подстановочных знаков, прочтите этот section .

Но для краткости, то, что вы можете использовать, это другой синтаксис Mockito, который поможет вам в вашей текущей ситуации:

doReturn(interfaces).when(classAMock).getMyInterfaces();

Или с псевдонимами BDD:

willReturn(interfaces).given(classAMock).getMyInterfaces();

Тем не менее, вы можете написать обертки, которые более универсальны. Это поможет будущим разработчикам работать с одним и тем же сторонним API.

В качестве побочного примечания: вы не должны издеваться над типом, которым вы не владеете, это может привести ко многим ошибкам и проблемам. Вместо этого у вас должна быть оболочка. DAO и репозитории, например, представляют такую ​​идею, они будут издеваться над интерфейсом DAO или репозитория, но не с JDBC / JPA / hibernate. Об этом много сообщений в блоге:





java generics casting