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

How does a Java HashMap handle different objects with the same hash code?

根据我的理解,我认为:

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

    如果正确,我有以下问题:HashMap在内部使用对象的哈希代码。因此,如果两个对象可以有相同的哈希代码,那么HashMap如何跟踪它使用的键?

    有人能解释一下HashMap如何在内部使用对象的散列码吗?


    本厂在HashMap类(this is a点simplified,but the basic illustrates EN标准):P></

    "has a number of buckets EN EN uses to which store"在关键值对。每个桶有独特的identifies号茶斗是什么。当你把对into the Key值图,the hash队列将HashMap look at the of the key pair和商店,在the of which the茶斗队列标识符is the key of the hash。for example,the key is of the hash队列235—> pair is stored in number为235。(附注一斗,更多的可以,那么商店一关键值对)。P></

    当你在查找值在HashMap,by giving a key EN,EN将哈希队列第一look at the key of the给你。然后一个HashMap将茶的外观对比的桶,然后它会出现,你给the key ofθwith the keys的茶斗?比较模式中,equals()with them。P></

    现在你能看到how this is looking for非常高效的关键值对后续:by the hash map中的key code of the which the HashMap立即知道在桶的面貌,只知道有什么在我的测试对桶。P></

    看着the above you can see also机制,什么是必要的要求和equals()on the keys方法:hashCode()ofP></

    • 如果两个键(equals()are the same true归来,当你出现,他们必须返回他们的hashCode()method)the same number。如果我违反了这钥匙,钥匙可能是平等的,buckets be stored in different HashMap,and the key to find不会对能值(因为它会在相同的外观桶)。P></

    • 如果两个不同的钥匙是,如果他们不那么恩物hash码are the same or not。they will be stored in the same代码哈希桶if their are the same,and in this the HashMap使用房屋,将equals()告诉他们分开。P></


    你的三assertion is incorrect。P></

    这两perfectly for Legal to have the same不等的对象的哈希代码。这是HashMapused as a"模式的第一通滤波器"quickly find the map so that can with the key指定可能的entries。the keys with the same哈希代码是平等,那么测试key for with the specified。P></

    你不会想在T,T不要求双have the same不等对象的哈希代码,as that otherwise 232可能会限制你的对象。(that different types页也会意味着不安甚至使用T S字段生成hash码的object' as to,the same其他类可以生成哈希)。P></


    HashMap structure diagram

    HashMapEntry对象的数组。

    HashMap看作一个对象数组。

    看看这个Object是什么:

    1
    2
    3
    4
    5
    6
    7
    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            final int hash;

    }

    每个Entry对象代表一个键值对。如果一个桶有多个Entry对象,则字段next表示另一个Entry对象。

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

    使用默认构造函数创建HashMap

    1
    HashMap hashMap = new HashMap();

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

    添加新的键值对

  • 计算密钥的哈希代码
  • 计算元素应放置的位置(桶号)
  • 如果您试图用已经保存在HashMap中的键添加一个值,那么该值将被覆盖。
  • 否则,元素将添加到bucket中。
  • 如果bucket已经至少有一个元素,则会添加一个新元素并将其放置在bucket的第一个位置。它的next字段是指旧元素。

    删除

  • 计算给定键的哈希代码
  • 计算桶数hash % (arrayLength-1)
  • 获取对bucket中第一个entry对象的引用,并通过equals方法迭代给定bucket中的所有条目。最终我们会找到正确的Entry。如果找不到所需的元素,返回null

  • 您可以在http://javarevisited.blogspot.com/2011/02/how-hashmap-works-in-java.html上找到优秀的信息。

    总结:

    hashmap的工作原理是散列

    put(key,value):hashmap将key和value对象存储为map.entry。hashmap应用hashcode(key)获取bucket。如果发生冲突,hashmap使用linkedlist存储对象。

    获取(key):hash映射使用关键对象的哈希代码查找桶位置,然后调用Key .Error()方法来识别LinkedList中的正确节点,并在Java Hash映射中返回该关键字的关联值对象。


    这里描述的是一个粗糙的HashMap的机制,Java 8版本(它可能是不同的,需要从Java 6)。

    数据结构

    • 哈希表哈希值是通过计算hash()on键,它决定着铲斗的哈希表使用一个给定的密钥。
    • 联列表(singly)当count of a铲斗的元素是小,是一singly联列表。
    • 红-黑树当count of a bucket中元素A是大型,红黑树是用来。

    类(内部)

    • Map.Entry单地图代表实体,实体键/值。
    • HashMap.Node联列表版本的节点。

      它可以代表:

      • a的哈希桶。因为它有一个哈希的物业。
      • 在singly联节点列表(所以LinkedList的(头)。
    • HashMap.TreeNode树的节点。

    机场(内部)

    • Node[] table《斗(头表链接的列表)。
      如果一个桶不包含元素,然后它是零,因此只有一个空间参考。
    • Set entrySet设置实体。
    • int size数量的实体。
    • float loadFactor用哈希表的知识全是允许的,在调整大小。
    • int threshold在这一个大小调整。threshold = capacity * loadFactor方程式:

    方法(内部)

    • int hash(key)通过计算散列密钥。
    • 如何映射到哈希桶?使用下面的逻辑:

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

    关于容量

    在哈希表,铲斗容量意味计数,它可以把从table.length。因此,可以计算通过thresholdloadFactor,因此不需要类定义为一场。

    可以通过得到的有效容量:capacity()

    操作

    • 是由密钥的实体。第一个哈希值,然后桶,环联列表或搜索树的成员。
    • 一个附加的实体的关键。第一个哈希值根据桶的关键。然后尝试找到的价值:
      • 如果发现,替换值。
      • 此外,添加一个新节点开始在大学联列表或树,插入的成员。
    • 调整当threshold想达到,双哈希表的容量(table.length),然后再将所有的哈希表的元件上的重建。这可能是在昂贵的操作。

    性能

    • get &;看跌时间复杂度是O(1),因为:
      • 铲斗的访问是通过数组的下标,因此O(1)
      • 联列表在每个桶长度是小,因此可以作为O(1)视图。
      • 树的大小是有限的,因为想扩展容量&;再散列元素计数增加时,可以查看它O(1)O(log N),困境。


    the hashCode determines which to check for the HashMap的桶。if there is超过一个对象在搜索然后在线性茶斗is done to find the item中which茶斗equals(教育法equals()using the item)。P></

    在其他的话,如果你有一个完美的hashCode是常数,那么你将HashMap的接入,never have to迭代一斗(你也会在technically have to have the _ int max buckets,Java哈希代码可能执行一些股在Down to the same斗在线切空间的要求)。如果你有一个坏的hashCode(the the same归来总是HashMap那么你的number)接入becomes since You have to search一线性每个item in the map(他们在斗to get the same)是你想要的。P></

    most of the hash码时间好好不完美但已经给你更多的是独特的或更少的接入等。P></


    王先生你错了三点。双can have the same entries but not be平等的哈希代码。take a look at the implementation of hashmap.get from the OpenJDK。You can see that are equal that the EN检查hashes and the keys are等。三是真正的点,然后它会check that the Keys to be是不必要的民族平等。the key is before the hash队列前compared because the is a more高效进行。P></

    如果你在学习兴趣的小黑莓about this,take a look at the Wikipedia article on开addressing碰撞分辨率,which is the我uses that the OpenJDK的执行机制。不同的机制比subtly that is one of the"斗"的方法mentions其他答案。P></


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    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(11623211601)。现在因为有不同的哈希代码,所以甚至不需要调用equals方法。因为不同的哈希代码保证对象中的内容不同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
        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.


    每个entry对象表示键值对。如果一个bucket有多个条目,则字段next引用其他条目对象。

    有时,两个不同对象的哈希代码可能是相同的。在这种情况下,2个对象将保存在一个bucket中,并显示为LinkedList。入口点是最近添加的对象。这个对象指的是另一个具有下一个字段的对象,所以是一个。最后一个条目引用了空值。使用默认构造函数创建哈希映射时

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

    enter image description here

    (源)


    two objects are equal, implies that they have same hashcode, but not vice versa

    HAMAP中的Java 8更新

    在代码中执行此操作-

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

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

    myHashMap是一个散列图,假设最初您没有指定它的容量。因此,按照Java的默认容量为16。所以现在,一旦使用新关键字初始化了hashmap,它就创建了16个bucket。现在,当您执行第一个语句时-

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

    然后计算EDOCX1×0的哈希代码,因为Hash码也可能是非常大的整数,所以Java内部做了这个(哈希是HASCODE和> >是右移)。

    1
    hash XOR hash >>> 16

    因此,作为一个更大的图片,它将返回一些索引,介于0到15之间。现在,您的键值对"old""key-value-pair"将转换为entry对象的key和value实例变量。然后这个entry对象将存储在bucket中,或者您可以说,在一个特定的索引中,这个entry对象将被存储。

    fyi-entry是map interface-map.entry中的一个类,具有这些签名/定义

    1
    2
    3
    4
    5
    6
    class Entry{
              final Key k;
              value v;
              final int hash;
              Entry next;
    }

    现在,当您执行下一个语句时-

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

    "very-old"给出的hashcode与"old"相同,所以这个新的键值对再次发送到相同的索引或桶中。但是由于这个bucket不是空的,所以entry对象的next变量被用来存储这个新的键值对。

    对于每个具有相同哈希代码但使用值6指定trify_阈值的对象,此值将存储为链接列表。因此,当达到此值后,链接列表将转换为平衡树(红黑树),第一个元素作为根。


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

    我们有key、value、hashcode和bucket。

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

    • 一个社会
    • hashcode->社会地址(始终唯一)
    • 社会价值观
    • 钥匙->房屋地址。

    使用map.get(key):

    Stevie想去他朋友的家,他住在VIP协会的别墅里,让它成为Javalovers协会。Josse的地址是他的SSN(每个人都不一样)。这里有一个索引,我们根据SSN找出社会的名字。这个索引可以被认为是找出散列码的算法。

    • SSN协会名称
    • 92313(Josse's)--贾瓦洛弗斯
    • 13214——角度爱好者
    • 9808——哈维尔
    • 53808——生物有机体
  • 这个ssn(key)首先给我们一个hashcode(从索引表中),它只是社会的名字。
  • 现在,多个房子可以在同一个社会,所以哈希代码可以是常见的。
  • 假设,社会对于两个家庭来说是很普遍的,我们如何通过使用(ssn)键来确定我们要去哪一个家庭,是的,这个键只不过是家庭地址。
  • 使用map.put(key,value)

    这将通过查找哈希代码为该值找到一个合适的社会,然后存储该值。

    我希望这有帮助,这是开放的修改。


    哈希散列在原理图工程学院

    HashMap的get(密钥K)方法在密钥电话hashcode法适用对象和返回到自己的hashvalue静态散列函数找到A铲斗位置(支持数组的键和值)存储在一个嵌套的形式称为(map.entry入门级)。你已经结束了,那么这两个线从以前的密钥和值存储在作为一个进入桶形状的对象。所以觉得值是只读存储在斗是不正确和不给面试官的印象是一个很好的早餐。

    • 可以直接调用get(密钥K在HashMap对象)的方法。第一,它是检查是否关键是零或没有。注意,只能在一个零的HashMap的密钥。

    如果密钥是零,零键映射到哈希然后总是0,因此指数为0。

    如果密钥是不为零,然后,它会调用对象的密钥的Hash函数,海线4 IU以上方法key.hashcode(),()hashvalue归来后,key.hashcode线4的样子

    1
                int hash = hash(hashValue)

    现在,它适用于hashvalue返回到自己的散列函数。

    我们知道为什么我们会再次使用哈希计算的hashvalue(hashvalue)。答案是它对质量defends)哈希函数。

    现在是一个最终hashvalue铲斗位置在其中存储的对象是输入。输入对象存储在斗样本(哈希密钥值,bucketindex)


    正如人们所说,一张图片价值1000字。我说:有些代码比1000个单词好。这是hashmap的源代码。获取方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
         * 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;
        }

    所以很明显,散列用于查找"bucket",并且第一个元素总是签入该bucket。如果没有,则使用键的equals在链接列表中查找实际元素。

    让我们看看put()方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
      /**
         * 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是放置新元素的索引(或者是"bucket")。ntab数组(bucket数组)的大小。

    首先,它被试着作为"桶"中的第一个元素。如果已经有一个元素,那么在列表中附加一个新节点。


    这将是一个很长的答案,喝一杯然后继续读…

    散列就是将一个键值对存储在内存中,这样可以更快地读写。它在数组中存储键,在LinkedList中存储值。

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

    1
    2
    3
    4
    5
    6
    {
    "girl" =>"ahhan" ,
    "misused" =>"Manmohan Singh" ,
    "horsemints" =>"guess what",
    "no" =>"way"
    }

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

    因此Java找到单个密钥的哈希代码并将它们映射到特定的数组索引。哈希代码公式是-

    1
    2
    3
    4
    5
    6
    7
    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相乘?

    It’s because, 31 is an odd prime in the form 2^5 – 1 . And odd prime reduces the chance of Hash Collision

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

    答案是,Hash Code % (Array length -1)。所以在我们的例子中,"girl"被映射到(3173020 % 3) = 1上。它是数组的第二个元素。

    值"ahhan"存储在与数组索引1关联的LinkedList中。

    hashcollision-如果你试图用上面描述的公式找到"misused""horsemints"键的hasHCode,你会发现两者都给出了相同的1069518484。哇!!吸取教训-

    2 equal objects must have same hashCode but there is no guarantee if
    the hashCode matches then the objects are equal . So it should store
    both values corresponding to"misused" and"horsemints" to bucket 1
    (1069518484 % 3) .

    现在散列图看起来是-

    1
    2
    3
    4
    Array Index 0
    Array Index 1 - LinkedIst ("ahhan" ,"Manmohan Singh" ,"guess what")
    Array Index 2LinkedList ("way")
    Array Index 3

    现在,如果某个机构试图找到密钥EDOCX1 5的值,Java将很快找到它的Hash码,模块它,并开始在相应的EdCOX1(8)中的链接表中搜索它的值。因此,通过这种方式,我们不需要搜索所有4个数组索引,从而使数据访问更快。

    但是,等一下。在LinkedList对应的数组索引1中有3个值,它如何确定哪个值是键"horsmints"的值?

    实际上我撒谎了,当我说hashmap只在linkedlist中存储值时。

    它将两个键值对存储为映射项。所以实际上地图看起来是这样的。

    1
    2
    3
    4
    Array Index 0
    Array Index 1 - LinkedIst (<"girl" =>"ahhan"> , <" misused" =>"Manmohan Singh"> , <"horsemints" =>"guess what">)
    Array Index 2LinkedList (<"no" =>"way">)
    Array Index 3

    现在,当遍历与arrayIndex1对应的LinkedList时,您可以看到它实际上将该LinkedList的每个条目的键与"horsmints"进行了比较,当它找到一个条目时,它只返回其值。

    希望你读的时候玩得开心:)