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


223

根据我的理解,我认为:

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

我对么?

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

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


29
记录:#1和#2是正确的,#3是错误的:两个不相等的对象可能具有相同的哈希码。
约阿希姆·绍尔

6
#1和#3甚至是矛盾的
Delfic

确实,如果未遵循#2,则equals()实现(或可以说hashCode())是不正确的。
约阿希姆·索尔

Answers:


346

哈希图的工作方式如下(这有点简化,但它说明了基本机制):

它具有多个“存储桶”,用于存储键值对。每个存储桶都有一个唯一的数字-标识存储桶。将键值对放入地图时,哈希图将查看键的哈希码,并将该对存储在标识符为键的哈希码的存储桶中。例如:密钥的哈希码为235->该对存储在存储区编号235中。(请注意,一个存储区可以存储多于一个键值对)。

当您在哈希图中查找值时,通过为其提供键,它将首先查看您提供的键的哈希码。哈希图随后将查看相应的存储桶,然后将您提供的密钥与存储桶中所有对的密钥进行比较,方法是将它们与equals()

现在,您将看到如何在映射中查找键值对非常有效:通过键的哈希码,哈希图可以立即知道要在哪个存储桶中查找,因此只需要针对该存储桶中的内容进行测试。

通过上述机制,您还可以看到对键hashCode()equals()方法有什么要求:

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

  • 如果两个键不同,那么它们的哈希码是否相同也没关系。如果它们的哈希码相同,它们将存储在同一存储桶中,在这种情况下,哈希图将用于equals()区分它们。


4
您写道:“散列图将无法找到键值对(因为它将在相同的存储桶中查找)。” 您能解释一下它会在同一个存储桶中查看这两个相等对象是t1和t2且相等并且t1和t2分别具有哈希码h1和h2的原因,所以t1.equals(t2)= true和h1!= h2因此,当哈希图查找t1时,它将在存储桶h1中查找,在存储桶t2中查找t2吗?
极客

19
如果两个键相等,但它们的hashCode()方法返回不同的哈希码,则键类的equals()hashCode()方法违反协定,并且在中使用这些键时,您会得到奇怪的结果HashMap
杰斯珀(Jesper)2012年

每个存储桶可以具有多个“键值”对,它们在内部使用链表。但是我的困惑是-这里的水桶是什么?内部使用什么数据结构?桶之间是否有连接?
Ankit Sharma 2014年

1
@AnkitSharma如果您想真正了解所有详细信息,请查找的源代码HashMap,您可以src.zip在JDK安装目录中的文件中找到该源代码。
Jesper 2014年

1
@ 1290同一存储桶中的键之间的唯一关系是它们具有相同的哈希码。
杰斯珀(Jesper)

88

您的第三个断言是不正确的。

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

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


6
您是如何到达2 ^ 32个可能的物体的?
极客

5
我来晚了,但是对于那些仍然在想的人:Java中的哈希码是一个int,一个int有2 ^ 32个可能的值
Xerus

69

HashMap结构图

HashMap是一组Entry对象。

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

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

HashMap使用默认构造函数创建时

HashMap hashMap = new HashMap();

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

添加新的键值对

  1. 计算密钥的哈希码
  2. 计算hash % (arrayLength-1)元素放置位置(桶号)
  3. 如果您尝试使用已保存在中的键添加值HashMap,则该值将被覆盖。
  4. 否则,元素将添加到存储桶中。

如果存储桶中已经有至少一个元素,则会添加一个新元素并将其放置在存储桶的第一个位置。它的next字段是指旧元素。

删除中

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

2
这是错误的hash % (arrayLength-1)它会hash % arrayLength。但是实际上是 hash & (arrayLength-1)。也就是说,因为它对2^n数组长度使用2的幂,所以占用n最低有效位。
weston

我认为这不是实体对象数组,而是LinkedList / Tree数组。每棵树内部都有实体对象。
Mudit bhaintwal

@shevchyk为什么我们要存储密钥和哈希?它们有什么用?我们不是在浪费记忆吗?
roottraveller

哈希集在内部使用哈希图。哈希表的添加和删除规则对哈希集有效吗?
overexchange

2
@weston不仅如此,hashCode int当然可以是负数,对负数取模将得到负数
Eugene

35

您可以在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的哈希码找出存储桶的位置,然后调用keys.equals()方法在LinkedList中标识正确的节点,并在Java HashMap中返回该键的关联值对象。


3
我发现Jasper提供的答案更好,我觉得博客更着重于面试,而不是理解概念
Narendra N 2014年

@NarendraN我同意你的观点。
Abhijit Gaikwad 2014年

22

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


数据结构

  • 哈希表
    哈希值是通过hash()on键计算的,它决定给定键使用哈希表的哪个存储区。
  • 链接列表 (单个)
    当存储桶中的元素数较少时,将使用单个链接列表。
  • 红黑树
    当存储桶中的元素数量很大时,将使用红黑树。

(内部)

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

    它可以代表:

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

字段(内部)

  • Node[] table
    存储桶表(链接列表的头)。
    如果存储桶不包含元素,则它为null,因此仅占用引用空间。
  • 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
也可以通过threshold和进行计算loadFactor,因此无需将其定义为类字段。

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


运作方式

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

性能

  • 获取和放置
    时间复杂度为O(1),因为:
    • 桶是通过数组索引访问的O(1)
    • 每个存储桶中的链表长度很小,因此可以查看为O(1)
    • 树的大小也受到限制,因为当元素数量增加时会扩展容量并重新哈希,因此可以将其视为O(1),而不是O(log N)

您能举个例子吗时间复杂度为O(1)
Jitendra

@jsroyal这可能会更清楚地解释复杂性:en.wikipedia.org/wiki/Hash_table。但简而言之:找到目标存储桶是O(1),因为您是通过数组中的索引找到它的;然后在一个存储桶中,元素的数量很小,并且尽管整个哈希表中的元素总数平均为一个常数,所以在存储桶中搜索目标元素也是O(1); 因此,O(1)+ O(1)= O(1)。
Eric Wang

14

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

换句话说,如果您拥有完美的哈希码,那么哈希图访问是恒定的,那么您就不必遍历存储桶(从技术上讲,您还必须拥有MAX_INT存储桶​​,Java实现可能会在同一存储桶中共享一些哈希码来实现)减少空间需求)。如果您拥有最差的哈希码(总是返回相同的数字),则您的哈希图访问将变为线性,因为您必须搜索地图中的每个项目(它们都在同一个存储桶中)才能获得所需的内容。

在大多数情况下,编写良好的哈希码并不完美,但其唯一性足以使您或多或少地获得恒定的访问权限。


11

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

如果您有兴趣了解更多有关此方面的知识,请查看Wikipedia上有关“ 开放地址冲突解决”的文章,我相信这是OpenJdk实现所使用的机制。该机制与其他答案之一提到的“存储桶”方法略有不同。


6
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. 

3

两个对象相等,表示它们具有相同的哈希码,反之亦然。

2个相等的对象------>它们具有相同的哈希码

2个对象具有相同的哈希码---- xxxxx->它们不相等

HashMap中的Java 8更新-

您可以在代码中执行此操作-

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

所以,假设返回两个按键的哈希码"old""very-old"是一样的。然后会发生什么。

myHashMap是HashMap,并假设最初您没有指定其容量。因此,根据Java的默认容量为16。因此,当您使用new关键字初始化hashmap时,它立即创建了16个存储桶。现在,当您执行第一条语句时-

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

然后"old"计算for 的哈希码,并且因为该哈希码也可能是非常大的整数,所以java在内部进行了此操作-(哈希在这里是哈希码,而>>>是右移)

hash XOR hash >>> 16

这样一来就给一个更大的图片,它会返回一些指标,这将是你现在的键值对0到15之间,以"old""old-value"将被转换为输入对象的key和value实例变量。然后该条目对象将存储在存储桶中,或者可以说在特定索引处将存储该条目对象。

FYI- Entry是Map界面Map.Entry中的类,具有这些签名/定义

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

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

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

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

并将其存储为具有相同哈希码的每个对象的链表,但是用值6指定了TRIEFY_THRESHOLD。因此,到达此值后,链表将转换为以第一个元素为元素的平衡树(红黑树)。根。


很棒的答案(y)
Sudhanshu Gaur

2

每个Entry对象代表键值对。如果存储桶中有多个条目,则下一个字段引用其他条目对象。

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

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

在此处输入图片说明

(资源)


1

哈希图基于哈希原理

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

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

如果key为null,则Null总是映射到哈希0,因此索引为0。

如果key不为null,它将在键对象上调用hashfunction,请参见上述方法的第4行,即key.hashCode(),因此在key.hashCode()返回hashValue之后,第4行如下

            int hash = hash(hashValue)

现在,它将返回的hashValue应用于其自己的哈希函数。

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

现在,使用最终哈希值来查找存储Entry对象的存储桶位置。入口对象像这样存储在存储桶中(哈希,键,值,存储桶索引)


1

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

我们有键,值,哈希码和存储桶。

在一段时间内,我们将它们与以下各项相关:

  • 桶->一个社会
  • HashCode->社会地址(始终唯一)
  • 价值->社会中的房屋
  • 键->住所地址。

使用Map.get(key):

Stevie想去他朋友的(Josse)住的房子,该房子住在VIP社区的别墅里,那就叫JavaLovers Society。Josse的地址是他的SSN(每个人的地址都不同)。维护了一个索引,在该索引中我们根据SSN找出协会的名称。可以将该索引视为找出HashCode的算法。

  • SSN协会的名字
  • 92313(Josse's)-JavaLovers
  • 13214-AngularJS爱好者
  • 98080-JavaLovers
  • 53808-生物爱好者

  1. 这个SSN(key)首先给我们一个HashCode(来自索引表),它只是协会的名字。
  2. 现在,多栋房屋可以在同一社会中,因此HashCode可以是通用的。
  3. 假设该协会对于两所房屋来说是共同的,那么我们如何通过使用(SSN)密钥来确定我们要去哪所房屋,这只不过是房屋地址

使用Map.put(key,Value)

通过找到HashCode,然后为该值找到合适的社会,然后存储该值。

希望对您有所帮助,并且可以进行修改。


0

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

散列是关于将键值对存储在内存中的,可以更快地进行读写。它将键存储在数组中,将值存储在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”使用上述公式,您会发现两者都给我们相同1069518484。哇!学习到教训了 -

2个相等的对象必须具有相同的hashCode,但不能保证hashCode匹配则对象相等。因此,它应将与“滥用”和“ horsemints”相对应的两个值都存储到存储桶1(1069518484%3)。

现在哈希图看起来像–

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

现在,如果某个主体试图找到该键的值,则“horsemints”Java会迅速找到它的hashCode并将其进行模块化,然后开始在对应的LinkedList中搜索它的值index 1。因此,这种方式我们不必搜索所有4个数组索引,从而使数据访问更快。

但是,等等,一秒钟。在该linkedList对应的数组索引1中有3个值,它如何找出哪个是键“ horsemints”的值?

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

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

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

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

希望你在阅读的过程中玩得开心:)


我认为这是错误的:“它将键存储在数组中,将值存储在LinkedList中。”
ACV

每个存储区列表中的每个元素都包含键和值以及对下一个节点的引用。
ACV

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) & hashi是将放置新元素的索引(或它是“存储桶”)。ntab数组的大小(“存储桶”的数组)。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.