如何正确地懒加载 Map of Map of Map?

5

这可能是一种不好的做法,但我还没有找到更好的解决方案来解决我的问题。所以我有这个地图。

// Map<state, Map<transition, Map<property, value>>>
private Map<String, Map<String, Map<String, String>>> properties;

我希望初始化它以避免出现NullPointerException

properties.get("a").get("b").get("c");

我尝试了这个方法,但它没有起作用(显然)。

properties = new HashMap<String, Map<String, Map<String,String>>>();

我尝试过其他方法,但都无法编译。

如果您有任何想法可以避免这种嵌套的映射,请告诉我,谢谢。


“这可能是一种不好的做法,但我还没有找到更好的解决方案来解决我的问题。” “你是正确的。它几乎肯定是不好的做法。如果你写了一个新的问题概述问题(特别是数据结构的要求),有人可能会建议你无法想出的更好的解决方案。” - Stephen C
9个回答

9

我觉得您需要创建自己的Key类:

public class Key {
   private final String a;
   private final String b;
   private final String c;
   public Key(String a, String b, String c) {
      // initialize all fields here
   }

   // you need to implement equals and hashcode. Eclipse and IntelliJ can do that for you
}

如果您实现自己的键类,那么您的映射将如下所示:
Map<Key, String> map = new HashMap<Key, String>();

当在地图中查找某物时,您可以使用以下方法:

map.get(new Key("a", "b", "c"));

上述方法不会抛出NullPointerException异常。
请记住,要使此解决方案起作用,您需要在Key类中覆盖equals和hashcode方法。这里有帮助文档(链接)。如果您不重写equals和hashcode方法,则具有相同元素的新键将无法与映射中的现有键匹配。
还有其他可能的解决方案,但在我看来,实现自己的键是一个非常干净的解决方案。如果您不想使用构造函数,可以使用静态方法初始化您的键并使用以下内容:
Key.build(a, b, c)

这取决于你。


1
我发现有一些通用的配对和三元容器类(分别用于保存两个或三个对象)是非常有用的,尤其是在这种情况下。除此之外还有很多其他的应用场景。 - ach

5
您需要在您的地图中放置地图。字面意思是:
properties = new HashMap<String, Map<String, Map<String,String>>>();
properties.put("a", new HashMap<String, Map<String,String>>());
properites.get("a").put("b", new HashMap<String,String>());

如果您的目标是实现懒加载且避免NPE,您需要创建自己的map:

private static abstract class MyMap<K, V> extends HashMap<K, V> {
    @Override
    public V get(Object key) {
        V val = super.get(key);
        if (val == null && key instanceof K) {
            put((K)key, val = create());
        }
        return val;
    }

    protected abstract V create();
}


public void initialize() {
    properties = new MyMap<String, Map<String, Map<String, String>>>() {
        @Override
        protected Map<String, Map<String, String>> create() {
            return new MyMap<String, Map<String, String>>() {
                @Override
                protected Map<String, String> create() {
                    return new HashMap<String, String>();
                }
            };
        }
    };

}

但我不想一开始就放任何值在那里。这意味着这里唯一的解决方案是在访问期间检查是否为空,如果为空则创建下一个级别? - user219882
Guava有一个CacheBuilder可以创建延迟初始化的Map。 - krlmlr

5
您可以使用一个实用方法:
  public static <T> T get(Map<?, ?> properties, Object... keys) {
    Map<?, ?> nestedMap = properties;
    for (int i = 0; i < keys.length; i++) {
      if (i == keys.length - 1) {
        @SuppressWarnings("unchecked")
        T value = (T) nestedMap.get(keys[i]);
        return value;
      } else {
        nestedMap = (Map<?, ?>) nestedMap.get(keys[i]);
        if(nestedMap == null) {
          return null;
        }
      }
    }
    return null;
  }

这可以这样调用:

String result = get(properties, "a", "b", "c");

请注意,使用此功能时需要小心,因为它不是类型安全的。


1

我认为更好的解决方案是将对象作为值映射的唯一键。该键将由三个字段组成:statetransitionproperty

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Key {

    private String state;

    private String transition;

    private String property;

    public Key(String state, String transition, String property) {
        this.state = state;
        this.transition = transition;
        this.property = property;
    }

    @Override
    public boolean equals(Object other) {
        return EqualsBuilder.reflectionEquals(this, other);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

}

当您检查一个值时,如果该键没有与之相关联的值,映射将返回null
Map<Key, String> values = new HashMap<Key, String>();
assert values.get(new Key("a", "b", "c")) == null;

values.put(new Key("a", "b", "c"), "value");
assert values.get(new Key("a", "b", "c")) != null;
assert values.get(new Key("a", "b", "c")).equals("value");

为了高效且正确地将对象用作 Map 中的键,您应该重写方法 equals()hashCode()。我使用 Commons Lang 库的反射功能构建了这些方法。

答案没问题,但需要一个适当的(非基于反射的) equals() 和 hashCode() 实现。基于反射的实现既不高效 -- 对于一个映射键来说这很重要 -- 也不能向人们展示正确实现这些方法的基本原则。 - Thomas W

1

使用此结构唯一的方法是使用所有可能的键预初始化第1级和第2级映射。如果无法这样做,则无法使用普通映射实现您要求的内容。

作为替代方案,您可以构建一个更容易处理的自定义数据结构。例如,一个常见的技巧是让失败的键查找返回一个“空”结构而不是null,从而允许嵌套访问。


1

你不能一次性初始化它,因为通常你不知道预先有哪些键。

因此,你必须检查一个键的子映射是否为空,如果是,则可能添加一个空映射。最好只在向映射中添加条目时执行此操作,并在检索条目时,如果路径中的子映射之一不存在,则返回null。你可以将其包装在自己的映射实现中以方便使用。

作为替代方案,apache commons collections的MultiKeyMap可能提供你想要的功能。


1

如果你使用 properties.get("a").get("b").get("c");,那么很难避免 null 的出现,除非你自己创建一个 Map。实际上,你无法预测你的 Map 是否包含 "b" 键。 因此,尝试创建自己的类来处理嵌套的 get


0

我认为,以下是更简单的方法:

public static final Map<Integer, Map<Integer, Map<Integer, Double>>> A_Map = new HashMap<Integer, Map<Integer, Map<Integer, Double>>>()
{
    {
        put(0, new HashMap<Integer, Map<Integer, Double>>()
        {
            {
                put(0, new HashMap<Integer, Double>()
                {
                    {
                        put(0, 1 / 60.0);
                        put(1, 1 / 3600.0);
                    }
                });

                put(1, new HashMap<Integer, Double>()
                {
                    {
                        put(0, 1 / 160.0);
                        put(1, 1 / 13600.0);
                    }
                });
            }
        });

        put(1, new HashMap<Integer, Map<Integer, Double>>()
        {
            {
                put(0, new HashMap<Integer, Double>()
                {
                    {
                        put(0, 1 / 260.0);
                        put(1, 1 / 3600.0);
                    }
                });

                put(1, new HashMap<Integer, Double>()
                {
                    {
                        put(0, 1 / 560.0);
                        put(1, 1 / 1300.0);
                    }
                });
            }
        });
    }
};

0
使用computeIfAbsent/putIfAbsent使它变得简单:

private <T> void addValueToMap(String keyA, String keyB, String keyC, String value) {
    map.computeIfAbsent(keyA, k -> new HashMap<>())
        .computeIfAbsent(keyB, k -> new HashMap<>())
        .putIfAbsent(keyC, value);
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接