📜  Java中HashMap的内部工作

📅  最后修改于: 2021-10-27 08:04:52             🧑  作者: Mango

在本文中,我们将看到 hashmap 的 get 和 put 方法在内部是如何工作的。执行哪些操作。散列是如何完成的。如何通过键获取值。键值对的存储方式。
在上一篇文章中,HashMap 包含一个 Node 数组,Node 可以表示一个具有以下对象的类:

  1. 整数哈希
  2. K键
  3. V值
  4. 下一个节点

现在我们将看到它是如何工作的。首先我们将看到散列过程。

散列

散列是使用 hashCode() 方法将对象转换为整数形式的过程。有必要正确编写 hashCode() 方法以获得更好的 HashMap 性能。在这里,我使用我自己的类的键,以便我可以覆盖 hashCode() 方法来显示不同的场景。我的关键课是

//custom Key class to override hashCode()
// and equals() method
class Key
{
  String key;
  Key(String key)
  {
    this.key = key;
  }
  
  @Override
  public int hashCode() 
  {
     return (int)key.charAt(0);
  }

  @Override
  public boolean equals(Object obj)
  {
    return key.equals((String)obj);
  }
}

这里重写的 hashCode() 方法返回第一个字符的 ASCII 值作为哈希码。所以只要key的第一个字符相同,哈希码就会相同。你不应该在你的程序中接近这个标准。它仅用于演示目的。由于 HashMap 也允许空键,所以空的哈希码将始终为 0。

hashCode() 方法

hashCode() 方法用于获取对象的哈希码。对象类的 hashCode() 方法以整数形式返回对象的内存引用。 hashCode() 方法的定义是 public native hashCode()。它表明 hashCode() 的实现是原生的,因为在Java没有任何直接的方法来获取对象的引用。可以提供您自己的 hashCode() 实现。
在 HashMap 中,hashCode() 用于计算桶,从而计算索引。

等于() 方法

equals 方法用于检查 2 个对象是否相等。该方法由 Object 类提供。您可以在您的类中覆盖它以提供您自己的实现。
HashMap 使用 equals() 来比较键是否相等。如果 equals() 方法返回 true,则它们相等,否则不相等。

桶是 HashMap 数组的一个元素。它用于存储节点。两个或多个节点可以具有相同的存储桶。在这种情况下,使用链表结构来连接节点。桶的容量不同。桶和容量之间的关系如下:

capacity = number of buckets * load factor

一个桶可以有多个节点,这取决于 hashCode() 方法。你的 hashCode() 方法越好,你的桶就会被更好地利用。

Hashmap中的索引计算

键的哈希码可能大到足以创建一个数组。生成的hash码可能在整数范围内,如果我们为这样的范围创建数组,那么很容易导致outOfMemoryException。所以我们生成索引来最小化数组的大小。基本上执行以下操作来计算索引。

index = hashCode(key) & (n-1).

其中 n 是桶数或数组大小。在我们的示例中,我将 n 视为默认大小,即 16。

为什么用上面的方法来计算指数

使用按位 AND运算符类似于进行位掩码,其中仅考虑散列整数的低位,这反过来提供了一种基于散列图长度计算模数的非常有效的方法。

  • 初始为空的hashMap:这里hashmap的大小取为16。
HashMap map = new HashMap();

  • 哈希映射:

empty_hasharray

  • 插入键值对:将一个键值对放在上面的 HashMap 中
map.put(new Key("vishal"), 20);

  • 脚步:
    1. 计算 Key {“vishal”} 的哈希码。它将生成为 118。
    2. 使用指数法计算指数为6。
    3. 创建一个节点对象:
{
  int hash = 118

  // {"vishal"} is not a string but 
  // an object of class Key
  Key key = {"vishal"}

  Integer value = 20
  Node next = null
}
  1. 如果没有其他对象出现在那里,则将此对象放在索引 6 处。
  • 插入另一个键值对:现在,放置另一个键值对,即
map.put(new Key("sachin"), 30);

  • 脚步:
    1. 计算 Key {“sachin”} 的 hashCode。它将生成为 115。
    2. 使用索引法计算索引为3。
    3. 创建一个节点对象:
{
  int hash = 115
  Key key = {"sachin"}
  Integer value = 30
  Node next = null
}
  • 在发生碰撞的情况下:现在,放置另一对,即
map.put(new Key("vaibhav"), 40);

  • 脚步:
    1. 计算 Key {“vaibhav”} 的哈希码。它将生成为 118。
    2. 使用指数法计算指数为6。
    3. 创建一个节点对象:
{
  int hash = 118
  Key key = {"vaibhav"}
  Integer value = 40
  Node next = null
}
  1. 如果那里没有其他对象,则将此对象放在索引 6 处。
  2. 在这种情况下,在索引 6 处发现了一个节点对象——这是一个碰撞的情况。
  3. 在这种情况下,通过 hashCode() 和 equals() 方法检查两个键是否相同。
  4. 如果键相同,则用当前值替换该值。
  5. 否则,通过链表将此节点对象连接到前一个节点对象,并且两者都存储在索引 6 处。
    现在 HashMap 变成了:

3_hasharray

使用获取方法()

现在让我们尝试一些 get 方法来获取一个值。 get(K key) 方法用于通过其键获取值。如果您不知道密钥,则无法获取值。

  • 获取关键 sachin 的数据:
map.get(new Key("sachin"));

  • 脚步:
    1. 计算 Key {“sachin”} 的哈希码。它将生成为 115。
    2. 使用索引法计算索引为3。
    3. 转到数组的索引 3 并将第一个元素的键与给定的键进行比较。如果两者相等则返回值,否则检查下一个元素是否存在。
    4. 在我们的例子中,它被发现为第一个元素,返回值为 30。
  • 获取密钥 vaibahv 的数据:
map.get(new Key("vaibhav"));

  • 脚步:
    1. 计算 Key {“vaibhav”} 的哈希码。它将生成为 118。
    2. 使用指数法计算指数为6。
    3. 转到数组的索引 6 并将第一个元素的键与给定的键进行比较。如果两者相等则返回值,否则检查下一个元素是否存在。
    4. 在我们的例子中,它不是作为第一个元素找到的,节点对象的下一个不为空。
    5. 如果节点的下一个为空,则返回空。
    6. 如果节点的下一个不为空,则遍历第二个元素并重复过程3,直到未找到键或下一个不为空。
    7. put 和 get 方法的时间复杂度几乎是恒定的,直到重新散列未完成。
    8. 在发生冲突的情况下,即两个或多个节点的索引相同,节点通过链表连接,即第二个节点由第一个节点引用,第三个节点由第二个节点引用,依此类推。
    9. 如果给定的键已存在于 HashMap 中,则该值将替换为新值。
    10. 空键的哈希码为 0。
    11. 当获取带有键的对象时,将遍历链表,直到键匹配或在下一个字段中找到 null。