Ответ 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();