【问题标题】:Java Hashmap store only value of a particular type in keyJava Hashmap 仅在键中存储特定类型的值
【发布时间】:2019-05-20 11:58:03
【问题描述】:

我正在考虑创建一个允许我存储键和值的 Hashmap 类。但是,只有匹配特定类型的值才能存储,并且类型取决于键的运行时值。例如,如果键是EMAIL(String.class),那么存储的值应该是String 类型。

我有以下自定义枚举:

public enum TestEnum {
    TDD,
    BDD,
    SHIFT_RIGHT,
    SHIFT_LEFT;
}

我创建了以下课程:

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

public class test {

    private static final Map<ValidKeys, Object> sessionData = new HashMap<>();

    public enum ValidKeys {
        EMAIL(String.class),
        PASSWORD(String.class),
        FIRST_NAME(String.class),
        LAST_NAME(String.class),
        CONDITION(TestEnum.class);

        private Class type;
        private boolean isList;
        private Pattern pattern;

        ValidKeys(Class<?> type, boolean isList) {
            this.type = type;
            this.isList = isList;
        }

        ValidKeys(Class<?> type) {
            this.type = type;
        }
    }

    public <T> void setData(ValidKeys key, T value) {
        sessionData.put(key,value);
    }


    public Object getData(ValidKeys key) {
        return key.type.cast(sessionData.get(key));
    }


    public static void main(String[] args) {
        test t = new test();
        t.setData(ValidKeys.CONDITION,TestEnum.TDD);
        System.out.println(t.getData(ValidKeys.CONDITION));
    }
}

我想使用setDatagetData 等方法并将值存储到sessionData。另外,我想确保该值是否是一个对象列表,那么也可以正确存储。

我也在努力避免 toString 基本上我需要一个通用的 getData ,它可以在没有类型转换的情况下工作。

【问题讨论】:

  • 你的问题是?
  • 那么,是什么阻止您将哈希映射声明为 HashMap&lt;Foo, Bar&gt;
  • 您有没有想过阅读Javadoc,或者了解generics
  • @AndreyTyukin,看起来他想要一个地图,可以容纳任何类型的混合键/值对,但稍后将在向该地图添加内容时执行类型检查。这就像 HashMap&lt;Object, Object&gt; 带有自定义“put”。
  • @JavaMan 啊,好的,我现在明白了。您正在尝试将运行时类型标签保存在哈希映射的键中,并且这些类型标签必须能够区分各种类型和其他类型的列表。类似于 Guice 的 TypeLiterals 正在做的事情。

标签: java reflection types


【解决方案1】:

我见过一种用于此类事情的特殊模式,它是 Bloch 的 Typesafe Heterogenous Container 模式的变体。我不知道它是否有自己的名字,但由于没有更好的名字,我将它称为 Typesafe Enumerated Lookup Keys。

基本上,我在各种情况下看到的一个问题是,您需要一组动态的键/值对,其中键的特定子集是“众所周知的”,具有预定义的语义。此外,每个键都与特定类型相关联。

“显而易见”的解决方案是使用枚举。例如,您可以这样做:

public enum LookupKey { FOO, BAR }

public final class Repository {
    private final Map<LookupKey, Object> data = new HashMap<>();

    public void put(LookupKey key, Object value) {
        data.put(key, value);
    }

    public Object get(LookupKey key) {
        return data.get(key);
    }
}

这工作得很好,但明显的缺点是现在你需要到处施法。例如,假设您知道LookupKey.FOO 始终具有String 值,而LookupKey.BAR 始终具有Integer 值。你如何执行?有了这个实现,你就不行了。

另外:在这个实现中,键的集合由枚举固定。您不能在运行时添加新的。对于某些应用程序来说这是一个优势,但在其他情况下,您确实希望在某些情况下允许使用新密钥。

这两个问题的解决方案基本相同:使LookupKey 成为一流的实体,而不仅仅是枚举。例如:

/**
 * A key that knows its own name and type.
 */
public final class LookupKey<T> {
    // These are the "enumerated" keys:
    public static final LookupKey<String> FOO = new LookupKey<>("FOO", String.class);
    public static final LookupKey<Integer> BAR = new LookupKey<>("BAR", Integer.class);

    private final String name;
    private final Class<T> type;

    public LookupKey(String name, Class<T> type) {
        this.name = name;
        this.type = type;
    }

    /**
     * Returns the name of this key.
     */
    public String name() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }

    /**
     * Cast an arbitrary object to the type of this key.
     * 
     * @param object an arbitrary object, retrieved from a Map for example.
     * @throws ClassCastException if the argument is the wrong type.
     */
    public T cast(Object object) {
        return type.cast(object);
    }

    // not shown: equals() and hashCode() implementations
}

这已经使我们大部分时间到达那里。您可以参考LookupKey.FOOLookupKey.BAR,它们的行为与您期望的一样,但它们也知道相应的查找类型。您还可以通过创建 LookupKey 的新实例来定义自己的密钥。

如果我们想实现一些不错的类似枚举的能力,比如静态values() 方法,我们只需要添加一个注册表。作为奖励,如果我们添加注册表,我们甚至不需要 equals()hashCode(),因为我们现在可以按身份比较查找键。

这是该类最终的样子:

/**
 * A key that knows its own name and type.
 */
public final class LookupKey<T> {
    // This is the registry of all known keys.
    // (It needs to be declared first because the create() function needs it.)
    private static final Map<String, LookupKey<?>> knownKeys = new HashMap<>();

    // These are the "enumerated" keys:
    public static final LookupKey<String> FOO = create("FOO", String.class);
    public static final LookupKey<Integer> BAR = create("BAR", Integer.class);


    /**
     * Create and register a new key. If a key with the same name and type
     * already exists, it is returned instead (Flywheel Pattern).
     *
     * @param name A name to uniquely identify this key.
     * @param type The type of data associated with this key.
     * @throws IllegalStateException if a key with the same name but a different
     *     type was already registered.
     */
    public static <T> LookupKey<T> create(String name, Class<T> type) {
        synchronized (knownKeys) {
            LookupKey<?> existing = knownKeys.get(name);
            if (existing != null) {
                if (existing.type != type) {
                    throw new IllegalStateException(
                            "Incompatible definition of " + name);
                }
                @SuppressWarnings("unchecked")  // our invariant ensures this is safe
                LookupKey<T> uncheckedCast = (LookupKey<T>) existing;
                return uncheckedCast;
            }
            LookupKey<T> key = new LookupKey<>(name, type);
            knownKeys.put(name, key);
            return key;
        }
    }

    /**
     * Returns a list of all the currently known lookup keys.
     */
    public static List<LookupKey<?>> values() {
        synchronized (knownKeys) {
            return Collections.unmodifiableList(
                    new ArrayList<>(knownKeys.values()));
        }
    }

    private final String name;
    private final Class<T> type;

    // Private constructor. Only the create method should call this.
    private LookupKey(String name, Class<T> type) {
        this.name = name;
        this.type = type;
    }

    /**
     * Returns the name of this key.
     */
    public String name() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }

    /**
     * Cast an arbitrary object to the type of this key.
     * 
     * @param object an arbitrary object, retrieved from a Map for example.
     * @throws ClassCastException if the argument is the wrong type.
     */
    public T cast(Object object) {
        return type.cast(object);
    }
}

现在LookupKey.values() 的工作方式或多或少类似于枚举。您也可以添加自己的密钥,然后values() 将返回它们:

LookupKey<Double> myKey = LookupKey.create("CUSTOM_DATA", Double.class);

一旦你有了这个 LookupKey 类,你现在就可以实现一个类型安全的存储库,它使用这些键进行查找:

/**
 * A repository of data that can be looked up using a {@link LookupKey}.
 */
public final class Repository {
    private final Map<LookupKey<?>, Object> data = new HashMap<>();

    /**
     * Set a value in the repository.
     *
     * @param <T> The type of data that is being stored.
     * @param key The key that identifies the value.
     * @param value The corresponding value.
     */
    public <T> void put(LookupKey<T> key, T value) {
        data.put(key, value);
    }

    /**
     * Gets a value from this repository.
     *
     * @param <T> The type of the value identified by the key.
     * @param key The key that identifies the desired value.
     */
    public <T> T get(LookupKey<T> key) {
        return key.cast(data.get(key));
    }
}

【讨论】:

  • 感谢@Daniel Pryden,看起来真的很酷,似乎已经解决了围绕对象/显式转换的问题。太棒了,干杯!!
  • 一个快速的,我不知道为什么我在 create 方法中看到 KnownKeys 的 NPE。我已经通过在 create 方法中再次初始化它来解决它,但不确定这是否正确。指针?
  • 我的错,我之前的布局不同,当我在这里发布代码时我重新组织了代码。 knownKeys 映射需要在“枚举”值之前声明,因为静态字段是按词法顺序初始化的。我将更新答案以显示这一点。 (正如您所指出的,解决此问题的另一种方法是在create 方法中延迟初始化它,但它不能是final。)
  • 非常感谢 Daniel Pryden 回复我的评论。我昨天应该在调试的时候把它捡起来,但并没有卡住我。太棒了,干杯!!!
猜你喜欢
  • 2017-04-07
  • 2016-04-21
  • 2012-06-18
  • 1970-01-01
  • 2014-12-03
  • 1970-01-01
  • 2023-03-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多