Java HashMap如何使用相同的哈希代码处理不同的对象?

时间:2011-06-27 13:31:41

标签: java hashmap hashcode hash-function

根据我的理解,我认为:

  1. 两个对象具有相同的哈希码是完全合法的。
  2. 如果两个对象相等(使用equals()方法),则它们具有相同的哈希码。
  3. 如果两个对象不相等,则它们不能具有相同的哈希码
  4. 我说错了吗?

    现在,如果我是对的,我有以下问题: HashMap在内部使用对象的哈希码。因此,如果两个对象可以具有相同的哈希码,那么HashMap如何跟踪它使用哪个密钥?

    有人可以解释HashMap内部如何使用对象的哈希码吗?

14 个答案:

答案 0 :(得分:318)

散列图的工作原理如下(这有点简化,但它说明了基本机制):

它有许多用于存储键值对的“桶”。每个桶都有一个唯一的编号 - 这就是识别存储桶的内容。将键值对放入映射时,哈希映射将查看键的哈希码,并将该对存储在桶中,其中标识符是键的哈希码。例如:密钥的哈希码是235 - >该对存储在桶号235中。(注意,一个桶可以存储多个键值对)。

当你在hashmap中查找一个值时,通过给它一个键,它将首先查看你给出的键的哈希码。然后,hashmap将查看相应的存储桶,然后通过将它们与equals()进行比较,将它与您提供的密钥与存储桶中所有对的密钥进行比较。

现在你可以看到这对于在地图中查找键值对非常有效:通过键盘的哈希码,哈希表可以立即知道要查看哪个桶,这样它只需要测试中的内容。那桶。

查看上述机制,您还可以了解密钥的hashCode()equals()方法需要哪些要求:

  • 如果两个键相同(equals()在比较它们时返回true),则其hashCode()方法必须返回相同的数字。如果密钥违反了这个,那么相同的密钥可能会存储在不同的存储桶中,并且hashmap将无法找到键值对(因为它将在同一个存储桶中查找)。

  • 如果两个密钥不同,那么它们的哈希码是否相同并不重要。如果它们的哈希码相同,它们将存储在同一个桶中,在这种情况下,哈希映射将使用equals()来区分它们。

答案 1 :(得分:84)

你的第三个断言不正确。

两个不等对象具有相同的哈希码是完全合法的。它由HashMap用作“首过滤镜”,以便地图可以使用指定的键快速找到可能的条目。然后测试具有相同哈希码的密钥与指定密钥的相等性。

您不希望要求两个不相等的对象不能具有相同的哈希码,否则将限制为2 32 可能的对象。 (这也意味着不同的类型甚至不能使用对象的字段来生成哈希码,因为其他类可以生成相同的哈希值。)

答案 2 :(得分:63)

HashMap structure diagram

HashMapEntry个对象的数组。

HashMap视为一个对象数组。

看看这个Object是什么:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;
… 
}

每个Entry对象代表一个键值对。如果存储区有多个next,则字段Entry会引用另一个Entry对象。

有时可能会发生2个不同对象的哈希码相同。在这种情况下,两个对象将保存在一个存储桶中,并将显示为链接列表。 入口点是最近添加的对象。此对象引用具有next字段的另一个对象,依此类推。最后一个条目是null

使用默认构造函数

创建HashMap
HashMap hashMap = new HashMap();

创建的数组大小为16,默认为0.75负载平衡。

添加新的键值对

  1. 计算密钥的哈希码
  2. 计算应放置元素的位置hash % (arrayLength-1)(桶号)
  3. 如果您尝试使用已在HashMap中保存的密钥添加值,则会覆盖值。
  4. 否则将元素添加到存储桶中。
  5. 如果存储桶已经至少有一个元素,则会添加一个新元素并将其放置在存储桶的第一个位置。它的next字段指的是旧元素。

    删除

    1. 计算给定密钥的哈希码
    2. 计算货号hash % (arrayLength-1)
    3. 获取对存储桶中第一个Entry对象的引用,并通过equals方法迭代给定存储桶中的所有条目。最终我们会找到正确的Entry。 如果找不到所需的元素,请返回null

答案 3 :(得分:34)

您可以在http://javarevisited.blogspot.com/2011/02/how-hashmap-works-in-java.html

找到优质信息

总结:

HashMap基于哈希原理

put(key,value): HashMap将key和value对象存储为Map.Entry。 Hashmap应用hashcode(key)来获取存储桶。如果存在冲突,HashMap使用LinkedList来存储对象。

get(key): HashMap使用Key Object的hashcode查找存储桶位置,然后调用keys.equals()方法识别LinkedList中的正确节点,并在Java中返回该键的关联值对象HashMap中。

答案 4 :(得分:21)

以下是HashMap机制的粗略描述,Java 8版本,(可能与Java 6略有不同)


数据结构

  • 哈希表
    哈希值是通过密钥上的hash()计算的,它决定用于给定密钥的哈希表的哪个桶。
  • 关联列表 (单独)
    当存储桶中的元素数量很小时,使用单链表。
  • 红黑树
    当存储桶中的元素数量很大时,使用红黑树。

(内部)

  • Map.Entry
    在地图中表示单个实体,即键/值实体。
  • HashMap.Node
    节点的链接列表版本。

    它可能代表:

    • 哈希桶 因为它有一个哈希属性。
    • 单链表中的节点(因此也是链表的主管)
  • HashMap.TreeNode
    节点的树版本。

字段(内部)

  • Node[] table
    桶表,(链表的头部)。
    如果存储桶不包含元素,则它为空,因此只占用引用的空间。
  • Set<Map.Entry> entrySet 实体集。
  • int size
    实体数量。
  • float loadFactor
    在调整大小之前,指示允许哈希表的填充程度。
  • int threshold
    下一个调整大小的尺寸 公式:threshold = capacity * loadFactor

方法(内部)

  • int hash(key)
    按键计算哈希。
  • 如何将哈希映射到存储桶?
    使用以下逻辑:

    static int hashToBucket(int tableSize, int hash) {
        return (tableSize - 1) & hash;
    }
    

关于容量

在哈希表中,容量表示存储桶计数,可以从table.length获取 也可以通过thresholdloadFactor进行计算,因此无需将其定义为类字段。

可以通过以下方式获得有效容量:capacity()


操作

  • 按键查找实体。
    首先通过哈希值找到存储桶,然后循环链表或搜索排序树。
  • 使用密钥添加实体 首先根据密钥的哈希值找到桶 然后尝试找到值:
    • 如果找到,请替换值。
    • 否则,在链接列表的开头添加一个新节点,或插入已排序的树。
  • 调整大小
    到达threshold时,会将哈希表的容量(table.length)加倍,然后对所有元素执行重新哈希以重建表。
    这可能是一项昂贵的操作。

效果

  • get&amp;把
    时间复杂度为O(1),因为:
    • 通过数组索引访问存储桶,因此O(1)
    • 每个存储桶中的链接列表长度很小,因此可以查看为O(1)
    • 树木的大小也有限,因为它会扩大容量和范围。当元素数量增加时重新哈希,因此可以将其视为O(1),而不是O(log N)

答案 5 :(得分:14)

哈希码确定要检查的hashmap的哪个存储桶。如果存储桶中有多个对象,则执行线性搜索以查找存储桶中的哪个项目等于所需项目(使用equals())方法。

换句话说,如果你有一个完美的哈希码,那么hashmap访问是不变的,你将永远不必遍历一个桶(技术上你也必须有一个MAX_INT桶,Java实现可能共享一些哈希码)减少空间要求的同一桶。如果您有最差的哈希码(总是返回相同的数字),那么您的哈希映射访问将变为线性,因为您必须搜索地图中的每个项目(它们都在同一个桶中)才能获得您想要的内容。

大多数情况下,编写良好的哈希码并不完美,但它足够独特,可以为您提供或多或少的持续访问。

答案 6 :(得分:11)

你在第三点上错了。两个条目可以具有相同的哈希码但不相等。看一下HashMap.get from the OpenJdk的实现。您可以看到它检查哈希值是否相等且键是否相等。如果点三是真的,那么就没有必要检查密钥是否相等。在密钥之前比较哈希码,因为前者是更有效的比较。

如果您有兴趣进一步了解这一点,请查看关于Open Addressing collision resolution的维基百科文章,我认为这是OpenJdk实现使用的机制。这种机制与“桶”方法略有不同,其中一个答案提到了。

答案 7 :(得分:5)

import java.util.HashMap;

public class Students  {
    String name;
    int age;

    Students(String name, int age ){
        this.name = name;
        this.age=age;
    }

    @Override
    public int hashCode() {
        System.out.println("__hash__");
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        System.out.println("__eq__");
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Students other = (Students) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    public static void main(String[] args) {

        Students S1 = new Students("taj",22);
        Students S2 = new Students("taj",21);

        System.out.println(S1.hashCode());
        System.out.println(S2.hashCode());

        HashMap<Students,String > HM = new HashMap<Students,String > (); 
        HM.put(S1, "tajinder");
        HM.put(S2, "tajinder");
        System.out.println(HM.size());
    }
}

Output:

__ hash __

116232

__ hash __

116201

__ hash __

__ hash __

2

所以在这里我们看到,如果对象S1和S2都有不同的内容,那么我们非常确定我们重写的Hashcode方法将为两个对象生成不同的Hashcode(116232,11601)。现在因为有不同的哈希码,所以它甚至不会打扰调用EQUALS方法。因为不同的Hashcode保证对象中的内容不同。

    public static void main(String[] args) {

        Students S1 = new Students("taj",21);
        Students S2 = new Students("taj",21);

        System.out.println(S1.hashCode());
        System.out.println(S2.hashCode());

        HashMap<Students,String > HM = new HashMap<Students,String > (); 
        HM.put(S1, "tajinder");
        HM.put(S2, "tajinder");
        System.out.println(HM.size());
    }
}

Now lets change out main method a little bit. Output after this change is 

__ hash __

116201

__ hash __

116201

__ hash __

__ hash __

__ eq __

1
We can clearly see that equal method is called. Here is print statement __eq__, since we have same hashcode, then content of objects MAY or MAY not be similar. So program internally  calls Equal method to verify this. 


Conclusion 
If hashcode is different , equal method will not get called. 
if hashcode is same, equal method will get called.

Thanks , hope it helps. 

答案 8 :(得分:2)

每个Entry对象代表键值对。如果存储桶具有多于1个条目,则字段next将引用其他Entry对象。

有时可能会发生2个不同对象的hashCodes相同。在这种情况下,2个对象将保存在一个存储桶中,并将显示为LinkedList。入口点是最近添加的对象。此对象引用具有下一个字段的其他对象,因此一个。最后一个条目是指null。 使用默认构造函数

创建HashMap时

使用大小16和默认0.75负载平衡创建数组。

enter image description here

(Source)

答案 9 :(得分:1)

哈希映射的工作原理是哈希

HashMap get(Key k)方法在密钥对象上调用hashCode方法,并将返回的hashValue应用于其自己的静态哈希函数,以查找存储桶位置(支持数组),其中键和值以名为Entry的嵌套类的形式存储(Map.Entry)。因此,您已得出结论,从上一行开始,Key和value都作为Entry对象的形式存储在存储桶中。所以认为只有价值存储在桶中是不正确的,不会给面试官留下好印象。

  • 每当我们在HashMap对象上调用get(Key k)方法时。首先,它检查密钥是否为空。请注意,HashMap中只能有一个空键。

如果key为null,则Null键始终映射到hash 0,因此索引为0。

如果key不为null,则它将调用key对象的hashfunction,参见上面方法中的第4行,即key.hashCode(),所以在key.hashCode()返回hashValue之后,第4行看起来像

            int hash = hash(hashValue)

现在,它将返回的hashValue应用到自己的散列函数中。

我们可能想知道为什么我们使用hash(hashValue)再次计算hashvalue。答案是它可以抵御质量差的哈希函数。

现在,最终的哈希值用于查找存储Entry对象的存储桶位置。入口对象像这样存储在桶中(散列,键,值,桶指数)

答案 10 :(得分:1)

我不会详细介绍HashMap的工作原理,但会给出一个例子,这样我们就可以通过将HashMap与现实联系起来来记住HashMap的工作原理。

我们有Key,Value,HashCode和bucket。

有时,我们会将每一项与以下内容联系起来:

  • 铲斗 - &gt;社会
  • HashCode - &gt;社会地址(总是独一无二的)
  • 价值 - &gt;社团中的一所房子
  • 键 - &gt;房子地址。

使用Map.get(key):

斯蒂维想要找到住在贵宾社会别墅的朋友家(Josse),让它成为JavaLovers Society。 Josse的地址是他的SSN(每个人都不一样)。 我们保留了一个索引,我们在其中找到了基于SSN的社团名称。 该索引可以被认为是找出HashCode的算法。

  • SSN社会名称
  • 92313(Josse&#39; s) - JavaLovers
  • 13214 - AngularJSLovers
  • 98080 - JavaLovers
  • 53808 - BiologyLovers
  1. 这个SSN(密钥)首先给我们一个HashCode(来自索引表),它只是社会名称。
  2. 现在,多个房子可以在同一个社会,所以HashCode可以是常见的。
  3. 假设,这个社会对于两个房子来说很常见,我们如何确定我们要去哪个房子,是的,使用(SSN)钥匙,这只是房子地址
  4. 使用Map.put(键,值)

    通过查找HashCode然后存储该值,可以找到适合此值的社会。

    我希望这会有所帮助,并且可以进行修改。

答案 11 :(得分:1)

  

两个对象相等,意味着它们具有相同的哈希码,但反之亦然

HashMap中的Java 8更新 -

您在代码中执行此操作 -

myHashmap.put("old","key-value-pair");
myHashMap.put("very-old","old-key-value-pair");

因此,假设为"old""very-old"键返回的哈希码是相同的。然后会发生什么。

myHashMap是一个HashMap,假设您最初没有指定其容量。所以java的默认容量是16.所以现在只要使用new关键字初始化hashmap,就会创建16个桶。现在当你执行第一个声明 -

myHashmap.put("old","key-value-pair");

然后计算"old"的哈希码,并且因为哈希码也可能是非常大的整数,所以,java内部做了这个 - (哈希是哈希码在这里,&gt;&gt;&gt;是右移)

hash XOR hash >>> 16

所以给出一个更大的pictureit将返回一些索引,它将在0到15之间。现在你的键值对"old""key-value-pair"将被转换为Entry对象的键和值实例变量。然后此条目对象将存储在存储桶中,或者您可以说在特定索引处将存储此条目对象。

FYI- Entry是Map interface-Map.Entry中的一个类,具有这些签名/定义

class Entry{
          final Key k;
          value v;
          final int hash;
          Entry next;
}

现在执行下一个语句 -

myHashmap.put("very-old","old-key-value-pair");

"very-old"提供与"old"相同的哈希码,因此这个新的键值对再次发送到同一个索引或同一个桶。但由于此存储桶不为空,因此Entry对象的next变量用于存储此新的键值对。

并且这将被存储为具有相同哈希码的每个对象的链表,但是TRIEFY_THRESHOLD被指定为值6.因此在此到达之后,链表被转换为平衡树(红黑树)元素作为根。

答案 12 :(得分:0)

据说,一张图片价值1000字。我说:有些代码优于1000字。这是HashMap的源代码。获取方法:

/**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

因此很明显,哈希用于查找“桶”,并且始终在该桶中检查第一个元素。如果没有,则使用密钥的equals来查找链表中的实际元素。

让我们看一下put()方法:

  /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

稍微复杂一点,但很明显新元素放在基于哈希计算的位置的标签中:

i = (n - 1) & hash此处i是放置新元素的索引(或者是“存储桶”)。 ntab数组(“桶”数组)的大小。

首先,尝试将其作为“桶”中的第一个元素。如果已有元素,则将新节点附加到列表中。

答案 13 :(得分:-1)

这将是一个很长的答案,喝一杯,继续阅读...

Hashing就是将键值对存储在内存中,可以更快地读写。 它将键存储在数组中,并将值存储在LinkedList中。

假设我想存储4个键值对 -

{
“girl” => “ahhan” , 
“misused” => “Manmohan Singh” , 
“horsemints” => “guess what”, 
“no” => “way”
}

所以要存储键我们需要一个4元素的数组。现在我如何将这4个键中的一个映射到4个数组索引(0,1,2,3)?

因此,java会找到各个键的hashCode,并将它们映射到特定的数组索引。 哈希码公式是 -

1) reverse the string.

2) keep on multiplying ascii of each character with increasing power of 31 . then add the components .

3) So hashCode() of girl would be –(ascii values of  l,r,i,g are 108, 114, 105 and 103) . 

e.g. girl =  108 * 31^0  + 114 * 31^1  + 105 * 31^2 + 103 * 31^3  = 3173020
哈哈和女孩!!我知道你在想什么。你对这种野性二重唱的迷恋可能让你错过了一件重要的事情。

为什么java将它乘以31?

  

这是因为,31是2 ^ 5 - 1形式的奇数素数。奇数素数降低了哈希碰撞的几率

现在这个哈希码如何映射到数组索引?

答案是,Hash Code % (Array length -1)。因此,“girl”在我们的案例中映射到(3173020 % 3) = 1。这是数组的第二个元素。

并且值“ahhan”存储在与数组索引1相关联的LinkedList中。

HashCollision - 如果您尝试使用上述公式找到hasHCode个键“misused”“horsemints”,您会看到两个都给我们相同{ {1}}。 Whooaa !!吸取教训 -

  

2个相等的对象必须具有相同的hashCode,但不能保证是否   hashCode匹配然后对象是相等的。所以它应该存储   两个值对应于桶1的“误用”和“马心”   (1069518484%3)。

现在哈希映射看起来像 -

1069518484

现在,如果某个主体试图找到键Array Index 0 – Array Index 1 - LinkedIst (“ahhan” , “Manmohan Singh” , “guess what”) Array Index 2 – LinkedList (“way”) Array Index 3 – 的值,java会快速找到它的hashCode,对其进行模块化并开始在LinkedList对应的“horsemints”中搜索它的值。因此,我们不需要搜索所有4个数组索引,从而使数据访问速度更快。

但是,等一下。在LinkedList对应的数组索引1中有3个值,它如何找出哪个是关键字“horsemints”的值?

实际上我撒谎,当我说HashMap只是将值存储在LinkedList中时。

它将两个键值对都存储为映射条目。所以实际上Map看起来像这样。

index 1

现在你可以看到,当遍历对应于ArrayIndex1的linkedList时,它实际上将该LinkedList的每个条目的键与“horsemints”进行比较,当它找到一个时,它只返回它的值。

希望你在阅读时玩得开心:)