繁体   English   中英

处理地图,equals()和hashCodes()。 这有多有效?

[英]Dealing with maps, equals() and hashCodes(). How efficient is this?

我正在写一些东西,每秒将接收大量事务。 对于每个进来的事务,都会引用一个映射,该映射的键值是id和一个有助于处理特定事务的bean。 基本上,每个事务都带有一个ID,将对地图进行查找以检索相应的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()之前先调用并比较对象的哈希码方法。 如果您的哈希值不匹配,它甚至都不会比较。 尽管这样做有效,但必须以这种方式实现哈希码方法才能欺骗地图,这让人有些困惑。

想知道是否有更有效的方法来做到这一点。 我们正在处理的相当数量的交易/秒,因此相当多的查找在地图上。

PS:我编码了这个盲人,所以我确定有语法错误。 请忽略那些。 只是试图传达一般想法。

如果您的hashCode()方法返回一个恒定值,则所有键都将散列到HashMap的同一存储桶中,从而有效地将HashMap减少为链接列表,访问时间为O(n)(而不是近似O(1))。

一种可能的解决方案(不节省空间):对于每个字符串,都存储多个与可能的String首选项相对应的键,但所有键都引用相同的值 例如,对于单词“ Hello”,您将存储键“ H”,“ He”,“ Hel”,“ Hell”,“ Hello”。 显然这会占用更多空间,但是查找时间会非常快,并且您无需破坏类的equals()方法即可执行“模糊”比较。 您可以通过编写自定义类来提高空间效率。 例如

/**
 * 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;
  }
}

如果您的比较器使用startsWith() ,则哈希映射是错误的数据结构。 您需要一些可以按字母首字母快速找到键的东西:您需要树形图。

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

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

我的建议是使用一个数组并将所有处理器放入该数组中。 按其ID对它们进行排序。

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

我认为哈希表不是一个好的解决方案。 @Adamskis加载带有前缀的哈希表的想法很有趣,但是我认为如果键共享前缀或如果您需要即时插入/删除条目,它将变得混乱。

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

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

为什么以这种低效的方式使用HashMap。 使用TreeMap可以更快地获得相同的结果-完全可以完成所需的操作。 哈希代码中的const也将显示O(n)性能,而TreeMap则为ln(n)。

该对象甚至不遵循hashCode的一般约定

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

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

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

您可能要测试实现(总是返回常量的存根)和“正常” Object (如String 测试测试测试思考测试测试测试 ,...

您的equals()方法不遵守Object.equals()的约定-它不是可传递的。 它将使“ a” .equals(“ ab”)返回true,而“ a” .equals(“ ac”)返回true,但是“ ab” .equals(“ ac”)返回false。

如果您尝试基于字符串前缀存储与字符串相关的对象,则可能需要使用trie进行研究

好的,谢谢您的投入。 认为问题陈述中最大的因素之一是存储的密钥几乎总是比比较短。 为此,提出了两种不同的方法来解决问题陈述,以防万一将来有人遇到类似情况时需要参考:

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

  2. 这是一位小鸽友。 相当喜欢我读到的关于Don Knuth的Trie(感谢Avi的参考),并提出了一个非常简单的实现。 (仅供参考,Ids的格式应类似于1.1.1.2。请记住这一点,以使示例代码看起来不会太怪异)。

公共类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个级别,因此,如果您的树结构变得非常复杂,则可能需要分析性能问题,并查看额外的查找是否会导致过多的开销。 哦,由于我们只处理String对象,因此这两种方法都不需要对hashCode进行任何狡猾的更改或等于合同。

注意事项:

尚未决定使用哪个来进行待定行为分析。 问题是大多数时候,比较值将与存储在地图中的比较值完全相同,因此简单查找就足够了。 它只是需要满足的其他“特殊”情况。 总而言之,如果特殊事件的发生频率往往非常低,我很想去最初的进阶(#1)。 绝大多数搜索将很快进行,当出现特殊情况时,我将承受字符串处理开销的痛苦。 如果情况相反,#2可能会更具吸引力。

PS:欢迎评论

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

为了提供更好的解决方案,我可能需要更多信息,例如:地图中的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.get(myId.getMyIdMap())

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM