【问题标题】:Map with multiple value types with advantages of generics具有泛型优势的多值类型映射
【发布时间】:2012-05-12 20:11:21
【问题描述】:

我想创建一个地图,该地图将提供泛型的好处,同时支持多种不同类型的值。我认为以下是泛型集合的两个主要优势:

  • 将错误内容放入集合时的编译时警告
  • 从集合中取出东西时无需强制转换

所以我想要的是一张地图:

  • 支持多个值对象,
  • 检查放入映射的值(最好在编译时)
  • 知道从地图获取对象的值是什么。

使用泛型的基本情况是:

Map<MyKey, Object> map = new HashMap<MyKey, Object>();
// No type checking on put();
map.put(MyKey.A, "A");  
map.put(MyKey.B, 10);
// Need to cast from get();
Object a = map.get(MyKey.A); 
String aStr = (String) map.get(MyKey.A);

我找到了解决第二个问题的方法,方法是创建一个 AbstractKey,它由与此键关联的值类生成:

public interface AbstractKey<K> {
}
public enum StringKey implements AbstractKey<String>{
  A,B;  
}
public enum IntegerKey implements AbstractKey<Integer>{
  C,D;
}

然后我可以创建一个 TypedMap,并覆盖 put() 和 get() 方法:

public class TypedMap extends HashMap<AbstractKey, Object> {
  public <K> K put(AbstractKey<K> key, K value) {
    return (K) super.put(key, value);
  }
  public <K> K get(AbstractKey<K> key){
    return (K) super.get(key);
  }
}

这允许以下操作:

TypedMap map = new TypedMap();
map.put(StringKey.A, "A");
String a = map.get(StringKey.A);

但是,如果我为键输入了错误的值,我不会收到任何编译错误。相反,我在 get() 上获得了运行时 ClassCastException

map.put(StringKey.A, 10); // why doesn't this cause a compile error?
String a = map.get(StringKey.A); // throws a ClassCastException

如果这个 .put() 可以给出编译错误,那将是理想的。 作为当前的第二好,我可以在 put() 方法中抛出运行时 ClassCastException

// adding this method to the AbstractKey interface:
public Class getValueClass();

// for example, in the StringKey enum implementation:
public Class getValueClass(){
  return String.class;
}

// and override the put() method in TypedMap:
public <K> K put(AbstractKey<K> key, K value){
  Object v = key.getValueClass().cast(value);
  return (K) super.put(key, v);
}

现在,ClassCastException 在放入地图时会被抛出,如下所示。这是更可取的,因为它允许更轻松/更快的调试来识别错误的键/值组合已放入 TypedMap 的位置。

map.put(StringKey.A, 10); // now throws a ClassCastException

所以,我想知道:

  • 为什么map.put(StringKey.A, 10) 不会导致编译错误?
  • 如果值不是键的关联泛型类型,我如何调整此设计以在 put 上获得有意义的编译错误?

  • 这是否适合实现我想要的设计(见顶部)? (任何其他想法/cmets/警告也将不胜感激......)

  • 我可以使用替代设计来实现我想要的吗?

编辑 - 澄清:

  • 如果您认为这是一个糟糕的设计,您能解释一下原因吗?
  • 我使用 String 和 Integer 作为示例值类型 - 实际上我有许多不同的键/值类型对,我希望能够使用它们。我想在一张地图中使用这些 - 这就是目标。

【问题讨论】:

  • 真有趣,我昨天花了所有时间都在进行与您在上面阐明的完全相同的探索(略有偏差)

标签: java generics map casting compiler-errors


【解决方案1】:

您正在以一种糟糕的方式处理泛型和重载。您正在扩展HashMap&lt;AbstractKey, Object&gt;,因此您的类继承了方法Object put(AbstractKey k, Object v)。在您的类中,您定义了另一个具有不同签名的 put 方法,这意味着您只是重载了 put 方法,而不是覆盖它。

当您编写map.put(StringKey.A, 10) 时,编译器会尝试查找符合参数类型put(StringKey, Integer) 的方法。您的方法的签名不适用,但继承的 put 的签名适用 -- StringKeyAbstractKey 兼容,IntegerObject 兼容。所以它将代码编译为对HashMap.put的调用。

解决此问题的方法:将 put 重命名为某个自定义名称,例如 typedPut

顺便说一句,从经验中谈起你的方法非常有趣且引人入胜,但在现实生活中,这不值得麻烦。

【讨论】:

  • 否 - 我的 put() 方法的返回值为 K。这符合继承的方法 public V put(K key, V value)。
  • 我将putSet.add 混淆了。但重点还是一样的。
  • you put() 方法不继承自 Map.put() 编译器在这里看到两个方法: Map.put(AbstractKey key, Object obj) 和 TypedMap.put(AbstractKey key, K 值)。尝试在 netbeans 上实现,它不会显示覆盖提示注释
  • 我明白了。您认为您的方法确实覆盖了超类方法吗?使用@Override 对其进行注释以进行检查。您正在扩展 HashMap&lt;AbstractKey, Object&gt; 并定义 &lt;K&gt; K put(AbstractKey&lt;K&gt; key, K value)。这与Object put(AbstractKey key, Object value) 的方法签名不同。
  • @amaidment TypedMap 扩展了 HashMap。该类有一个 put 方法,它接受任何类型的 AbstractKey 和任何类型的 Object 作为参数。如果你在做 put(AbstractKey, String),编译器会明白 TypedMap 中的 put 实现是适用的,并使用那个方法。如果您正在执行 put(AbstractKey, Integer),编译器将理解 TypedMap.put(...) 不适用。相反,编译器将使用 Hashmap.put(...) 接受任何 AbstractKey 而无需关心参数化类型和任何 Object。
【解决方案2】:

第 29 条:考虑类型安全的异构容器。Joshua Bloch, Effective Java, Second Edition, Chapter 5: Generics

【讨论】:

【解决方案3】:

恕我直言,每个问题都来自原始设计的味道:想要将不同类型的值放入地图中。我会将您的 Integer 和 String 值包装成一个常见的 Value 类型。像这样的:

public class Value {
    private enum Type {
        STRING, INTEGER;
    }

    private Type type;
    private Object value;

    private Value(Object value, Type type) {
        this.value = value;
        this.type = type;
    }

    public static Value fromString(String s) {
        return new Value(s, Type.STRING);
    }

    public static Value fromInteger(Integer i) {
        return new Value(i, Type.INTEGER);
    }

    public Type getType() {
        return this.type;
    }

    public String getStringValue() {
        return (String) value;
    }

    public Integer getIntegerValue() {
        return (Integer) value;
    }

    // equals, hashCode
}

这样,您只需要一个Map&lt;SomeKey, Value&gt;,就可以安全地从地图中获取值:

Value v = map.get(someKey);
if (v.getType() == Type.STRING) {
    String s = v.getStringValue();
}
else if (v.getType() == Type.INTEGER) {
    Integer i = v.getIntegerValue();
}

【讨论】:

  • 您将此描述为“原创设计气味”——您能解释一下您认为该设计有什么问题吗?
  • 将不同类型的值放入映射中意味着您必须对这些值使用 instanceof 才能对这些值执行某些操作。一种更 OO 的做法是对映射中的所有可能值有一个公共接口或超类,并使用这个公共接口或超类作为映射的值类型。如果这不可能,这可能意味着您应该使用多个地图。
  • 另外 - 我不认为这个解决方案真的有帮助。您所做的只是找到了另一种方法来确定映射中值的类型。我什至会建议这不如我的方法,因为您必须知道 Type a Value 将具有什么,然后才能转换为它。我看不出这对确保放入地图时的类型安全有任何作用。
  • 但是我不必做 instanceof,因为我得到的值的对象类型是由 AbstractKey 的泛型决定的——这就是我设计的重点。
  • 它提高了类型安全性,因为可以放入映射中的唯一可能的东西是 Value 实例。而那些只能包装一个字符串或一个整数。在您的问题中,您从一个 Map 开始,它可以包含任何类型的对象。也许您应该在更高的层次上向我们解释为什么需要将 Integer 和 String 值存储到唯一的映射中。如果您有两个映射(一个用于整数值,一个用于字符串值,则不必创建任何特定的类。
【解决方案4】:

考虑一下:(感谢 Effective java)

public class TypedMap {

    private Map<AbstractKey<?>, Object> map = new HashMap<AbstractKey<?>, Object>();

    public <T> T get(AbstractKey<T> key) {
        return key.getType().cast(map.get(key));
    }

    public <T> T put(AbstractKey<T> key, T value) {
        return key.getType().cast(map.put(key, key.getType().cast(value)));
    }

    public static interface AbstractKey<K> {

        Class<K> getType();
    }

    public static enum StringKey implements AbstractKey<String> {

        A, B;

        public Class<String> getType() {
            return String.class;
        }
    }

    public static enum IntegerKey implements AbstractKey<Integer> {

        C, D;

        public Class<Integer> getType() {
            return Integer.class;
        }
    }
  }

这会产生编译时错误

TypedMap map = new TypedMap();
TypedMap.AbstractKey<Integer> intKey = TypedMap.IntegerKey.C;
TypedMap.AbstractKey<String> strKey = TypedMap.StringKey.A;
map.put(strKey, "A");
map.put(intKey, 10);
map.put(strKey, 10); // this cause a compile error?

【讨论】:

  • 通常你可以用 Class 替换 AbstractKey 除非抽象键包含的数据不仅仅是一个键?
  • 这看起来只是 Marko 答案的派生,除了因为 TypedMap 不扩展 Map,因此可以有 put() 和 get() 方法(而不是 typedPut())。但是,我想扩展 Map - 部分是为了获得 Map 的功能(迭代器等),部分是为了可以将其作为 Map 处理(例如在其他自定义渲染器中)。我不确定附加值是什么......?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-23
  • 1970-01-01
  • 1970-01-01
  • 2016-08-11
  • 2012-03-04
  • 1970-01-01
相关资源
最近更新 更多