避免在具有多个值类型的地图中进行未经检查的赋值?

2022-09-01 08:54:18

我在 Java 7 中遇到警告时遇到问题:

Unchecked assignment: 'java.lang.Class' to 'java.lang.Class<T>'

我在下面的 get 函数中得到了它。Class<T> type = typeMap.get(key);

基本上,我在这里尝试做的是,我想存储一堆未知类型的键/值对(但除了null之外,所有这些都是Object的后代),但不会丢失类型。因此,我使用泛型创建了一个包含以下内容的类。它有两个映射(一个用于存储数据,另一个用于存储类类型):

    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中怎么样,因为我还没有真正有机会深入研究它?

谢谢!


答案 1

您显示的内容不安全的原因是,使用此分配:

Class<T> type = typeMap.get(key);

T不需要与从地图中检索到的内容有任何关系。 总是从调用 的周围上下文中推断出来的。例如,我可以执行以下调用序列:ClassTget

// 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。getString.classClass<Integer>type.cast(...)StringIntegerClassCastException

这种奇怪的交互是由于类型擦除


因此,当我们在单个数据结构中存储多个类型时,有多种方法可以处理它,具体取决于我们的需求。

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 传递给,以便返回值必须有效。Classget

这就是我通常看到的方式。

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

答案 2

您正在尝试将类型的元素分配给类型的变量。当然,这是一个未经检查的任务。您似乎正在实现异构映射。Java(以及任何其他强类型语言)无法以静态类型安全的方式表达映射的值类型。ClassClass<T>

这是因为元素类型仅在运行时已知。期望编译器对尚不为人知的事情进行静态类型检查,要求太多了。编译器甚至无法进行合理的静态类型推断,因此期望它预测动态类型推断的未来实际上是一种延伸。


推荐