您显示的内容不安全的原因是,使用此分配:
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");
在方法内部,从类型映射中正确检索,但对 进行了未经检查的转换。调用 不会引发,因为从数据映射检索到的值是 .然后,隐式检查强制转换实际上发生在返回值上,将其强制转换为并抛出 a。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();