【问题标题】:Java: Composite key in hashmapsJava:哈希图中的复合键
【发布时间】:2012-07-26 21:22:49
【问题描述】:

我想将一组对象存储在 hashmap 中,其中的键应该是两个字符串值的组合。有没有办法做到这一点?

我可以简单地将两个字符串连接起来,但我确信有更好的方法来做到这一点。

【问题讨论】:

  • 脱离上下文,只是另一个例子,C# 似乎是一种比 Java 更实用的语言。

标签: java collections hash hashmap


【解决方案1】:

为什么不创建一个(比如)Pair 对象,其中包含两个字符串作为成员,然后使用 this 作为键?

例如

public class Pair {
   private final String str1;
   private final String str2;

   // this object should be immutable to reliably perform subsequent lookups
}

不要忘记equals()hashCode()。有关 HashMap 和键的更多信息,包括关于不变性要求的背景信息,请参阅 this blog entry。如果您的密钥不是不可变的,那么您可以更改其组件,随后的查找将无法找到它(这就是为什么不可变对象(例如 String)是密钥的良好候选者)

你说得对,串联并不理想。在某些情况下它会起作用,但它通常是一个不可靠且脆弱的解决方案(例如 AB/CA/BC 的键是否不同?)。

【讨论】:

  • 如果我们有很多条目(~77,500),我们可以发现自己有哈希冲突吗?
  • 冲突的发生与散列函数的范围、被散列的条目数量、条目值的分布、散列函数的表现以及可用空间成比例哈希映射。如果不了解更多这些细节,则无法确定 77,500 个条目是否会有很多或很少(或没有)冲突。请注意,即使使用非常好的哈希函数,也可能会发生冲突。对于典型的哈希映射实现,重要的不是是否发生任何冲突,而是与总条目的比例有多少。
【解决方案2】:

您可以有一个包含两个字符串的自定义对象:

class StringKey {
    private String str1;
    private String str2;
}

问题是,您需要确定两个此类对象的相等性测试和哈希码。

平等可以是两个字符串的匹配,哈希码可以是连接成员的哈希码(这是有争议的):

class StringKey {
    private String str1;
    private String str2;

    @Override
    public boolean equals(Object obj) {
        if(obj != null && obj instanceof StringKey) {
            StringKey s = (StringKey)obj;
            return str1.equals(s.str1) && str2.equals(s.str2);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return (str1 + str2).hashCode();
    }
}

【讨论】:

  • ABC,D 和 AB,CD 的哈希码相同是否有问题?还是等号不同可以解决这个问题?
  • @smackfu:这取决于。如果你有很多这样的字符串对,这只会是一个问题,因为它们会散列到表中的同一个槽,从而降低查找效率。
  • @Tudor 你能想到这个解决方案比 EdgeCase 提供的解决方案有什么优势吗? EdgeCase 基本上只是连接由波浪号分隔的两个字符串?
  • @Zak:唯一的区别是我使用连接的两个字符串,而他在它们之间使用波浪号。他的版本应该更好地传播成对的字符串,否则在连接时会给出相同的结果。无论如何,我只是举一个如何生成哈希码的例子,我并不想让它变得高效。
  • @Tudor,你不需要 obj != null 和 instanceOf 运算符,干杯!
【解决方案3】:

我也有类似的情况。我所做的就是连接两个由波浪号 ( ~ ) 分隔的字符串。

所以当客户端调用服务函数从地图中获取对象时,看起来是这样的:

MyObject getMyObject(String key1, String key2) {
    String cacheKey = key1 + "~" + key2;
    return map.get(cachekey);
}

这很简单,但很有效。

【讨论】:

  • 是的。 OP 明确规定了“两个字符串”。另一种复合键可能需要更复杂的东西。但这是用例的正确答案,恕我直言。但是,“加入”字符需要更多工作:OP 没有说这些字符串是“仅字母数字”,因此它们可能包含波浪号字符。否则,来自 Unicode 更狂野平面的一些非常奇特的东西可能会起作用:? 或 ♄ 可能。
【解决方案4】:
public static String fakeMapKey(final String... arrayKey) {
    String[] keys = arrayKey;

    if (keys == null || keys.length == 0)
        return null;

    if (keys.length == 1)
        return keys[0];

    String key = "";
    for (int i = 0; i < keys.length; i++)
        key += "{" + i + "}" + (i == keys.length - 1 ? "" : "{" + keys.length + "}");

    keys = Arrays.copyOf(keys, keys.length + 1);

    keys[keys.length - 1] = FAKE_KEY_SEPARATOR;

    return  MessageFormat.format(key, (Object[]) keys);}
公共静态字符串 FAKE_KEY_SEPARATOR = "~";

输入: fakeMapKey("keyPart1","keyPart2","keyPart3");
输出:keyPart1~keyPart2~keyPart3

【讨论】:

    【解决方案5】:
    public int hashCode() {
        return (str1 + str2).hashCode();
    }
    

    这似乎是一种糟糕的生成 hashCode 的方法:每次计算 hash Code 时都创建一个新的字符串实例太糟糕了! (即使生成一次字符串实例并缓存结果也是不好的做法。)

    这里有很多建议:

    How do I calculate a good hash code for a list of strings?

    public int hashCode() {
        final int prime = 31;
        int result = 1;
        for ( String s : strings ) {
            result = result * prime + s.hashCode();
        }
        return result;
    }
    

    对于一对字符串,就变成了:

    return string1.hashCode() * 31 + string2.hashCode();
    

    这是一个非常基本的实现。通过链接提供了很多建议,以提出更好的调整策略。

    【讨论】:

    • “每次计算哈希码时都会产生一个新的字符串实例”——哈哈哈,看得很清楚!
    • 如果哈希码是通过异或而不是*+ 计算的,是否有任何区别,因为字符串 hashCode 使用哈希码中的大多数位?
    • XOR 和其他按位运算符比乘法和加法更快。示例中差异是否显着取决于 String.hashCode() 的实现。此外,应该格外小心以确保新实现具有作为哈希函数的良好属性。
    【解决方案6】:

    我看到很多人使用嵌套地图。也就是说,要映射Key1 -&gt; Key2 -&gt; Value(我使用计算机科学/又名haskell curring notation 进行(Key1 x Key2) -&gt; Value 映射,它有两个参数并产生一个值),首先提供第一个键——这会返回一个(partial) map@ 987654324@,您将在下一步中展开。

    例如,

    Map<File, Map<Integer, String>> table = new HashMap(); // maps (File, Int) -> Distance
    
    add(k1, k2, value) {
      table2 = table1.get(k1);
      if (table2 == null) table2 = table1.add(k1, new HashMap())
      table2.add(k2, value)
    }
    
    get(k1, k2) {
      table2 = table1.get(k1);
      return table2.get(k2)
    }
    

    我不确定它是否比普通的复合键结构更好。您可以对此发表评论。

    【讨论】:

      【解决方案7】:

      阅读 spaguetti/cactus 堆栈时,我想出了一个可以用于此目的的变体,包括以任何顺序映射键的可能性,以便 map.lookup("a","b") 和 map.lookup("a","b") 。 lookup("b","a") 返回相同的元素。它也适用于任意数量的键,而不仅仅是两个。

      我将它用作试验数据流编程的堆栈,但这里有一个快速而肮脏的版本,可用作多键映射(应该改进:应该使用集合而不是数组来避免查找重复出现的键)

      public class MultiKeyMap <K,E> {
          class Mapping {
              E element;
              int numKeys;
              public Mapping(E element,int numKeys){
                  this.element = element;
                  this.numKeys = numKeys;
              }
          }
          class KeySlot{
              Mapping parent;
              public KeySlot(Mapping mapping) {
                  parent = mapping;
              }
          }
          class KeySlotList extends LinkedList<KeySlot>{}
          class MultiMap extends HashMap<K,KeySlotList>{}
          class MappingTrackMap extends HashMap<Mapping,Integer>{}
      
          MultiMap map = new MultiMap();
      
          public void put(E element, K ...keys){
              Mapping mapping = new Mapping(element,keys.length);
              for(int i=0;i<keys.length;i++){
                  KeySlot k = new KeySlot(mapping);
                  KeySlotList l = map.get(keys[i]);
                  if(l==null){
                      l = new KeySlotList();
                      map.put(keys[i], l);
                  }
                  l.add(k);
              }
          }
          public E lookup(K ...keys){
              MappingTrackMap tmp  = new MappingTrackMap();
              for(K key:keys){
                  KeySlotList l = map.get(key);
                  if(l==null)return null;
                  for(KeySlot keySlot:l){
                      Mapping parent = keySlot.parent;
                      Integer count = tmp.get(parent);
                      if(parent.numKeys!=keys.length)continue;
                      if(count == null){
                          count = parent.numKeys-1;
                      }else{
                          count--;
                      }
                      if(count == 0){
                          return parent.element;
                      }else{
                          tmp.put(parent, count);
                      }               
                  }
              }
              return null;
          }
          public static void main(String[] args) {
              MultiKeyMap<String,String> m = new MultiKeyMap<String,String>();
              m.put("brazil", "yellow", "green");
              m.put("canada", "red", "white");
              m.put("USA", "red" ,"white" ,"blue");
              m.put("argentina", "white","blue");
      
              System.out.println(m.lookup("red","white"));  // canada
              System.out.println(m.lookup("white","red"));  // canada
              System.out.println(m.lookup("white","red","blue")); // USA
          }
      }
      

      【讨论】:

        【解决方案8】:

        您无需重新发明轮子。只需使用GuavaHashBasedTable&lt;R,C,V&gt; 实现Table&lt;R,C,V&gt; 接口,以满足您的需要。这是一个例子

        Table<String, String, Integer> table = HashBasedTable.create();
        
        table.put("key-1", "lock-1", 50);
        table.put("lock-1", "key-1", 100);
        
        System.out.println(table.get("key-1", "lock-1")); //prints 50
        System.out.println(table.get("lock-1", "key-1")); //prints 100
        
        table.put("key-1", "lock-1", 150); //replaces 50 with 150
        

        编码愉快!

        【讨论】:

        • 这个实现比公认的答案要好。
        【解决方案9】:

        我想提两个我认为其他答案未涵盖的选项。它们是否适合您的目的,您必须自己决定。

        地图>

        您可以使用映射的映射,在外部映射中使用字符串 1 作为键,在每个内部映射中使用字符串 2 作为键。

        我不认为这是一个非常好的语法解决方案,但它很简单,而且我已经看到它在某些地方使用过。它还应该在时间和内存上很有效,而这在 99% 的情况下不应该是主要原因。我不喜欢它的是我们丢失了关于密钥类型的显式信息:它只是从代码中推断出有效的密钥是两个字符串,阅读不清楚。

        地图

        这是针对特殊情况的。我不止一次遇到过这种情况,所以没有比这更特别的了。如果您的对象包含用作键的两个字符串,并且基于这两者定义对象相等性是有意义的,则根据此定义 equalshashCode 并将对象用作键和值。

        在这种情况下,人们可能希望使用Set 而不是Map,但是Java HashSet 没有提供任何方法来从基于相等对象的集合中检索对象。所以我们确实需要地图。

        一个责任是您需要创建一个新对象才能进行查找。这也适用于许多其他答案中的解决方案。

        链接

        Jerónimo López: Composite key in HashMaps关于地图效率的地图。

        【讨论】:

          猜你喜欢
          • 2014-02-01
          • 2018-10-31
          • 2018-04-19
          • 1970-01-01
          • 1970-01-01
          • 2014-06-18
          • 2018-04-07
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多