您所显示的内容不安全的原因是通过此作业:
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
。然后隐式检查强制转换actually发生在返回值,将其投射到Integer
and a ClassCastException
被抛出。
这种奇怪的相互作用是由于类型擦除 https://docs.oracle.com/javase/tutorial/java/generics/erasure.html.
因此,当我们在单个数据结构中存储多种类型时,有多种方法可以实现它,具体取决于我们的需求。
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
and 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
to 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);
}
}
}
So e.g.:
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();