处理map,equals()和hashCodes()。这有多高效?

时间:2009-08-12 07:13:02

标签: java hashmap

我正在写一些每秒会收到大量交易的东西。对于每个进入的事务,都会引用一个映射,其中键值是id,bean是一个有助于处理该特定事务的bean。基本上每个事务都带有一个id,一个查找将对map执行检索相应的bean进行处理。粘性部分带有这样的事实:每个事务的id并不意味着精确匹配地图中的id。更多的是从操作开始。为此,我创建了一个名为MyId的简单pojo,而不是使用字符串作为id。代码如下:

public class MyId
{

    private static final int HASHCODE_CONSTANT = 1;
    private String value;

    public MyId(String value)
    {
        this.value = value;
    }

    @Override
    public int hashCode()
    {
        //Returns the same hashcode value for all instances of this pojo
        return HASHCODE_CONSTANT;
    }

    @Override
    public boolean equals(Object obj)
    {
        //Checks for object type, forcibly casts and then compares the starts with
        if(obj instanceof MyId)
        {
            if(!(obj == null || "".equals(obj)))
            {
                return this.value.startsWith(((MyId)obj).getValue());
            }
        }
        return false;
    }

    public String getValue()
    {
        return value;
    }

    public void setValue(String value)
    {
        this.value = value;
    }

    //Test
    public static void main(String[] args)
    {
         Map map = new HashMap();
         map.put(new MyId("123456"), "");

         System.out.println("Result: " + map.containsKey(new MyId("12345677")));
         System.out.println("Result: " + map.containsKey(new MyId("11234567")));
    }
}

第一个测试返回true,第二个测试返回false,就像它应该的那样。似乎map.containsKey()方法在调用equals()之前首先调用并比较对象的hashcode方法。如果你的哈希不匹配,它甚至都不愿意比较。虽然这有效,但是以这种方式实现哈希码方法来欺骗地图感觉有点狡猾。

想知道是否有更有效的方法来做到这一点。我们 处理了大量的事务/秒,因此在地图上查找了很多。

PS:我对此进行了编码,因此我确信存在语法错误。请忽略这些。只是想传达一般的想法。

8 个答案:

答案 0 :(得分:5)

如果您的hashCode()方法返回一个常量值,则所有密钥都会哈希到HashMap中的同一个存储区,从而有效地将HashMap缩减为链接列表,访问时间为O (n)(而不是近似O(1))。

一种可能的解决方案(不节省空间):对于每个字符串,存储与可能的字符串前缀相对应的多个键,但所有引用相同的值。例如,对于单词“Hello”,您将存储键“H”,“He”,“Hel”,“Hell”,“Hello”。这显然会消耗更多空间,但查找时间会非常快,您不需要使用类equals()方法来执行“模糊”比较。您可以通过编写自定义类来提高空间效率; e.g。

/**
 * Class representing String prefix.
 * Storage overhead == original string + two ints.
 */
public class Prefix {
  private final String str;
  private final int len;
  private final int hc;

  public Prefix(String str, int len) {
    this.str = str;
    this.len = len;
    this.hc = toString().hashCode(); // Precompute and store hash code.
  }

  public String toString() {
    return str.substring(0, len);
  }

  public int hashCode() {
    return hc;
  }

  public boolean equals(Object o) {
    boolean ret;

    if (this == o) {
      ret = true;
    } else if (o instanceof Prefix) {
      ret = toString().equals(((Prefix)o).toString());
    } else {
      ret = false;
    }

    return ret;
  }
}

答案 1 :(得分:5)

如果您的比较器使用startsWith(),则哈希映射是错误的数据结构。你需要一些东西,你可以用他们的第一个字母快速找到钥匙:你需要一张树图。

与哈希映射不同,树映射是有序的。因此,您可以开始在根处搜索,而不是盲目地潜入奇数分布数字的数学空间,性能将是O(log(n))。 Java实现的主要问题:它已关闭并锁定。您无法将其扩展为使用startsWith()进行搜索。

在您的情况下,事务处理器的数量似乎是稳定的(意味着您不会一直创建新的事务处理器)。如果不是这种情况,那么处理器的数量应该相对较小(例如,< 1000)。

我的建议是使用一个数组并将所有处理器放在该数组中。按ID分类。

现在,您可以使用Arrays.binarySearch(T[] a, T key, Comparator<? super T> c)使用比较器中equals()的代码有效地查找元素。

答案 2 :(得分:4)

我不认为哈希表是一个很好的解决方案。 @Adamskis使用前缀加载哈希表的想法很有意思,但我认为如果密钥共享前缀或者你需要动态插入/删除条目,它会变得混乱。

如果您的地图/查找表条目没有chjange,那么使用预先排序的数组和Arrays.binarySearch(...)(由@Aaron建议)是一个很好的解决方案。它应该给你O(log(N))查找。

但是,如果您需要动态插入或删除映射条目,则对于基于阵列的解决方案,这些操作将为O(N)。相反,您应该使用TreeMap,并使用NavigableMap API中的方法,例如'lowerKey(), floorKey()and higherKey()`来查找表中的“最接近”匹配。这应该给你O(log(N))进行查找,插入和删除。

答案 3 :(得分:2)

为什么以这种低效的方式使用HashMap。使用TreeMap可以更快地获得相同的东西 - 它完全按照您的要求完成。 散列码中的const也会显示O(n)性能,而TreeMap会显示ln(n)。

答案 4 :(得分:2)

此对象甚至不跟随the general contract of hashCode

  • 如果两个对象根据equals(Object)方法相等,则对两个对象中的每一个调用hashCode方法必须产生相同的整数结果。

  • 如果两个对象根据equals(java.lang.Object)方法不相等,则不需要在两个对象中的每一个上调用hashCode方法必须生成不同的整数结果。

  

但是,程序员应该知道为不等对象生成不同的整数结果可能会提高哈希表的性能。

你可能想要测试你的实现(一个总是返回常量的存根)和一个“普通”Object,就像String一样。请测试测试测试 认为 测试测试测试,...

答案 5 :(得分:1)

您的equals()方法不遵守Object.equals()的合同 - 它不具有传递性。它会有“a”.equals(“ab”)返回true,“a”.equals(“ac”)返回true,但“ab”.equals(“ac”)返回false。

如果您尝试根据字符串前缀存储与字符串相关的对象,则可能需要考虑使用某种trie

答案 6 :(得分:1)

好的,谢谢输入的人。认为问题陈述中最重要的因素之一是存储的密钥几乎总是比比较短。为此,提出了两种不同的方法来解决问题陈述,以防万一有人需要参考,如果他们在将来遇到类似的东西:

  1. 按正常方式使用地图。当输入比较进入时,比较。如果没有命中,请修剪字符串并再次进行比较。

  2. 这个人有点发烧友。非常喜欢我读到的Don Knuth的Trie(感谢参考Avi),并提出了一个非常简单的实现。 (仅供参考,Ids的格式类似于1.1.1.2。需要牢记这一点,因此示例代码看起来并不太奇怪)。

  3. 公共课Trie {     private HashMap map = new HashMap();

    public Trie()
    {
    }
    
    public Object get(String key)
    {
        return recurse(key.split("\\."), map, 0);
    }
    
    protected Object recurse(String[] key, Map map, int location)
    {
        Object value = map.get(key[location]);
        if(value instanceof Map)
            return recurse(key, (Map)value, location+1);
        else
            return value;
    }
    
    public void addKey(String key, Object value)
    {
        String[] keys = key.split("\\.");
        addKey(keys, map, 0, value);
    }
    
    protected void addKey(String[] key, Map map, int location, Object value)
    {
        if((location+1) == key.length)
        {
            //end of the road. value insertion
            map.put(key[location], value);
        }
        else
        {
            Map hashMap = (Map) map.get(key[location]);
            if(!(map.containsKey(key[location])))
            {
                hashMap = new HashMap();
                map.put(key[location], hashMap);
            }
            addKey(key, hashMap, location+1, value);
        }
    }
    
    public static void main(String[] args)
    {
        Trie trie = new Trie();
        trie.addKey("1.1.2.1", "1.1.2.1");
        trie.addKey("1.1.2.2", "1.1.2.2");
        trie.addKey("1.1.2.3.1", "1.1.2.3.1");
        trie.addKey("1.1.2.3.2", "1.1.2.3.2");
        trie.addKey("1.1.2.4", "1.1.2.4");
    
        System.out.println(trie.get("1.1.2.1.0")); //returns 1.1.2.1
        System.out.println(trie.get("1.1.2.3.1.0")); //returns 1.1.2.3.1
        System.out.println(trie.get("1.1.2.4.1.0")); //returns 1.1.2.4
    }
    

    }

    在我的用例中,我预计Trie的深度不会超过2-3级,所以如果你的树结构变得非常复杂,你可能想要分析性能问题并查看额外的查找是否会导致太多开销。哦,这两种方法都不需要对hashCode或equals合同进行任何狡猾的更改,因为我们只处理String对象。

    考虑:

    尚未决定使用哪个待处理行为分析。事情大部分时间,比较值将完全与存储在地图中的值相比,因此简单的查找就足够了。它只是其他需要照顾的“特殊”案例。总而言之,如果特殊情况往往是非常低的频率,我会倾向于采取初步的方法(#1)。绝大多数搜索都是快速的,当一个特殊情况出现时,我会忍受字符串操作开销带来的痛苦。如果相反,#2可能更具吸引力。

    PS:欢迎评论

答案 7 :(得分:0)

我认为你强迫两个不同的对象使用相同的数据结构,这使得你的地图效率不高。

为了提供更好的解决方案,我可能需要更多信息,例如:地图中的ID是否总是6位?

那么你可以创建两个这样的类。

public class MyIdMap {

   private String value;

   public MyIdMap(String value) {
      this.value = value;
   }

   public String getValue() {
      return value;
   }

   public void setValue(String value) {
      this.value = value;
   }

   @Override
   public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((value == null) ? 0 : value.hashCode());
      return result;
   }

   @Override
   public boolean equals(Object obj) {
      if (this == obj)
         return true;
      if (obj == null)
         return false;
      if (getClass() != obj.getClass())
         return false;
      MyIdMap other = (MyIdMap) obj;
      if (value == null) {
         if (other.value != null)
            return false;
      } else if (!value.equals(other.value))
         return false;
      return true;
   }
}


public class MyId {

   private String value;

   public MyId(String value) {
      this.value = value;
   }

   public String getValue() {
      return value;
   }

   public void setValue(String value) {
      this.value = value;
   }

   public MyIdMap getMyIDMap() {
      return new MyIdMap(value.substring(0, 6));
   }
}

将MyIdMap放入Map中,然后当你找到它时,只需使用map.get(myId.getMyIdMap())