【问题标题】:Is there a Java equivalent of Python's defaultdict?是否有与 Python 的 defaultdict 等效的 Java?
【发布时间】:2010-12-19 15:54:18
【问题描述】:

在 Python 中,defaultdict 类提供了一种从key -> [list of values] 创建映射的便捷方式,在以下示例中,

from collections import defaultdict
d = defaultdict(list)
d[1].append(2)
d[1].append(3)
# d is now {1: [2, 3]}

在 Java 中是否有与此等价的功能?

【问题讨论】:

    标签: java python hashmap


    【解决方案1】:

    没有什么可以提供开箱即用的默认 dict 行为。然而,在 Java 中创建自己的默认 dict 并没有那么困难。

    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    
    public class DefaultDict<K, V> extends HashMap<K, V> {
    
        Class<V> klass;
        public DefaultDict(Class klass) {
            this.klass = klass;    
        }
    
        @Override
        public V get(Object key) {
            V returnValue = super.get(key);
            if (returnValue == null) {
                try {
                    returnValue = klass.newInstance();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                this.put((K) key, returnValue);
            }
            return returnValue;
        }    
    }
    

    这个类可以像下面这样使用:

    public static void main(String[] args) {
        DefaultDict<Integer, List<Integer>> dict =
            new DefaultDict<Integer, List<Integer>>(ArrayList.class);
        dict.get(1).add(2);
        dict.get(1).add(3);
        System.out.println(dict);
    }
    

    此代码将打印:{1=[2, 3]}

    【讨论】:

    • 不使用Class,您也可以尝试传递Guava Supplier -- 见docs.guava-libraries.googlecode.com/git-history/v10.0/javadoc/…
    • 或者,如果您不想要 Guava 依赖,只需在 DefaultDict 中定义自己的 Supplier&lt;V&gt; 接口即可。
    • 我更喜欢用我自己的价值构造DefaultDictpublic DefaultDict(V value) { this.value = value; }
    • 如果您将某个键的值null 存储到映射中,这会不会表现不正确?当您调用get() 时,它会表现得好像该键不存在(即使它确实 存在,由containsKey() 确定),它们会创建一个新值并覆盖现有的地图中的值。
    • 如果您的默认值类型不是集合,这将如何工作?例如一个值为 1 的Integer
    【解决方案2】:

    在您想要defaultdict 的最常见情况下,您会更喜欢设计合理的 Multimap 或 Multiset,这正是您真正想要的。 Multimap 是 key -> 集合映射(默认为空集合),Multiset 是 key -> int 映射(默认为零)。

    Guava 提供了对Multimaps and Multisets 的非常好的实现,几乎涵盖了所有用例。

    但是(这就是我发布新答案的原因)使用 Java 8,您现在可以使用任何现有的 Map 复制 defaultdict 的剩余用例。

    • getOrDefault(),顾名思义,如果存在则返回值,或者返回默认值。这在地图中存储默认值。
    • computeIfAbsent() 从提供的函数计算一个值(它总是可以返回相同的默认值),确实在返回之前将计算的值存储在映射中。

    如果你想封装这些调用可以使用 Guava 的ForwardingMap:

    public class DefaultMap<K, V> extends ForwardingMap<K, V> {
      private final Map<K, V> delegate;
      private final Supplier<V> defaultSupplier;
    
      /**
       * Creates a map which uses the given value as the default for <i>all</i>
       * keys. You should only use immutable values as a shared default key.
       * Prefer {@link #create(Supplier)} to construct a new instance for each key.
       */
      public static DefaultMap<K, V> create(V defaultValue) {
        return create(() -> defaultValue);
      }
    
      public static DefaultMap<K, V> create(Supplier<V> defaultSupplier) {
        return new DefaultMap<>(new HashMap<>(), defaultSupplier);
      }
    
      public DefaultMap<K, V>(Map<K, V> delegate, Supplier<V> defaultSupplier) {
        this.delegate = Objects.requireNonNull(delegate);
        this.defaultSupplier = Objects.requireNonNull(defaultSupplier);
      }
    
      @Override
      public V get(K key) {
        return delegate().computeIfAbsent(key, k -> defaultSupplier.get());
      }
    }
    

    然后像这样构造你的默认地图:

    Map<String, List<String>> defaultMap = DefaultMap.create(ArrayList::new);
    

    【讨论】:

    • 我没有投反对票。总之是个好主意。但是,我认为您不能将变量命名为 default,而且我认为 create(V) 只是要求用户在他们的脚下开枪。
    • 谢谢,你说得对。我同意create(V) 有点冒险,但我怀疑如果没有它会被错过。至少,我添加了一条评论来指出风险。
    【解决方案3】:

    除了 apache 集合,还要检查 google collections:

    类似于 Map 的集合,但可以将多个值与单个键相关联。如果您使用相同的键但值不同的两次调用 put(K, V),则多重映射包含从键到两个值的映射。

    【讨论】:

      【解决方案4】:

      您可以从Apache Commons 使用MultiMap

      【讨论】:

      【解决方案5】:

      在 Java 8+ 中,您可以使用:

      map.computeIfAbsent(1, k -> new ArrayList<Integer>()).add(2);
      

      【讨论】:

        【解决方案6】:

        仅使用 Java 运行时库,您可以使用 HashMap 并添加 ArrayList 在键尚不存在时保存您的值,或者在键存在时将值添加到列表中。

        【讨论】:

          【解决方案7】:

          @tendayi-mawushe 的解决方案不适用于原始类型(例如 InstantiationException Integer),这是一种适用于 Integer、Double、Float 的实现。我经常将 Maps 与这些一起使用并添加静态构造函数以方便使用

          import java.util.HashMap;
          import java.util.Map;
          
          /** Simulate the behaviour of Python's defaultdict */
          public class DefaultHashMap<K, V> extends HashMap<K, V> {
              private static final long serialVersionUID = 1L;
          
              private final Class<V> cls;
              private final Number defaultValue;
          
              @SuppressWarnings({ "rawtypes", "unchecked" })
              public DefaultHashMap(Class factory) {
                  this.cls = factory;
                  this.defaultValue = null;
              }
          
              public DefaultHashMap(Number defaultValue) {
                  this.cls = null;
                  this.defaultValue = defaultValue;
              }
          
              @SuppressWarnings("unchecked")
              @Override
              public V get(Object key) {
                  V value = super.get(key);
                  if (value == null) {
                      if (defaultValue == null) {
                          try {
                              value = cls.newInstance();
                          } catch (Exception e) {
                              e.printStackTrace();
                          }
                      } else {
                          value = (V) defaultValue;
                      }
                      this.put((K) key, value);
                  }
                  return value;
              }
          
              public static <T> Map<T, Integer> intDefaultMap() {
                  return new DefaultHashMap<T, Integer>(0);
              }
          
              public static <T> Map<T, Double> doubleDefaultMap() {
                  return new DefaultHashMap<T, Double>(0d);
              }
          
              public static <T> Map<T, Float> floatDefaultMap() {
                  return new DefaultHashMap<T, Float>(0f);
              }
          
              public static <T> Map<T, String> stringDefaultMap() {
                  return new DefaultHashMap<T, String>(String.class);
              }
          }
          

          还有一个测试,为了礼貌:

          import static org.junit.Assert.assertEquals;
          
          import java.util.ArrayList;
          import java.util.List;
          import java.util.Map;
          
          import org.junit.Test;
          
          public class DefaultHashMapTest {
          
              @Test
              public void test() {
                  Map<String, List<String>> dm = new DefaultHashMap<String, List<String>>(
                          ArrayList.class);
                  dm.get("nokey").add("one");
                  dm.get("nokey").add("two");
                  assertEquals(2, dm.get("nokey").size());
                  assertEquals(0, dm.get("nokey2").size());
              }
          
              @Test
              public void testInt() {
                  Map<String, Integer> dm = DefaultHashMap.intDefaultMap();
                  assertEquals(new Integer(0), dm.get("nokey"));
                  assertEquals(new Integer(0), dm.get("nokey2"));
                  dm.put("nokey", 3);
                  assertEquals(new Integer(0), dm.get("nokey2"));
                  dm.put("nokey3", 3);
                  assertEquals(new Integer(3), dm.get("nokey3"));
              }
          
              @Test
              public void testString() {
                  Map<String, String> dm = DefaultHashMap.stringDefaultMap();
                  assertEquals("", dm.get("nokey"));
                  dm.put("nokey1", "mykey");
                  assertEquals("mykey", dm.get("nokey1"));
              }
          }
          

          【讨论】:

            【解决方案8】:

            我写的库Guavaberry包含这样的数据结构:DefaultHashMap

            它经过高度测试和记录。您可以通过 Maven Central 轻松找到并集成它。

            主要优点是它使用 lambda 来定义工厂方法。因此,您可以添加任意定义的类实例(而不是依赖于默认构造函数的存在):

            DefaultHashMap<Integer, List<String>> map = new DefaultHashMap(() -> new ArrayList<>());
            map.get(11).add("first");
            

            希望对你有帮助。

            【讨论】:

            • 如果不是从HashMap 扩展,而是使用ForwardingMap 并允许调用者指定支持映射,它会更有用。更喜欢组合而不是继承。
            猜你喜欢
            • 2013-06-07
            • 1970-01-01
            • 1970-01-01
            • 2011-04-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-02-04
            相关资源
            最近更新 更多