Java中HashMap的实现原理

来源:互联网 发布:linux grep指令 编辑:程序博客网 时间:2024/04/25 20:07
HashMap是现在面试中最受欢迎的话题。

假设如果你对HashMap的内部工作感兴趣。在向前推进之前,我强烈建议您阅读我以前的文章:在java中使用hashCode和equals方法。

单一声明答案

如果有人要求我描述“ HashMap如何工作?“我只是回答:” 哈哈的原则 “。一样简单。我们必须非常肯定地了解至少哈希的基础知识。

什么是哈希?

以最简单的形式进行散列,是在对其属性应用任何公式/算法之后为任何变量/对象分配唯一代码的方式。真正的哈希函数必须遵循这个规则:

当函数应用于相同或相等的对象时,哈希函数应该每次都返回相同的哈希码。换句话说,两个相等的对象必须一致地产生相同的哈希码。

Java中的所有对象都继承了Object类中定义的hashCode()函数的默认实现。该函数通过将对象的内部地址转换为整数来产生散列码,从而为所有不同的对象生成不同的哈希码。

一点关于入门课

按照定义的映射是:“将键映射到值的对象”很容易..对吧?

所以,HashMap中必须有一些机制来存储这个键值对。答案是YES。HashMap有一个内部类Entry,如下所示:

static class Entry<K ,V> implements Map.Entry<K ,V>{    final K key;    V value;    Entry<K ,V> next;    final int hash;    ...//More code goes here}

确定Entry类具有存储为属性的密钥和值映射。密钥已被标记为最终,另外还有两个字段:next和hash。我们将努力了解这些领域的需求。

put()方法实际上是什么

在进入put()方法的实现之前,了解Entry类的实例存储在数组中是非常重要的。HashMap类将此变量定义为:

/** * The table, resized as necessary. Length MUST Always be a power of two. */transient Entry[] table;

现在看看put()方法的代码实现:

/*** Associates the specified value with the specified key in this map. If the* map previously contained a mapping for the key, the old value is* replaced.** @param key*            key with which the specified value is to be associated* @param value*            value to be associated with the specified key* @return the previous value associated with <tt>key</tt>, or <tt>null</tt>*         if there was no mapping for <tt>key</tt>. (A <tt>null</tt> return*         can also indicate that the map previously associated*         <tt>null</tt> with <tt>key</tt>.)*/public V put(K key, V value) {    if (key == null)    return putForNullKey(value);    int hash = hash(key.hashCode());    int i = indexFor(hash, table.length);    for (Entry<K , V> e = table[i]; e != null; e = e.next) {        Object k;        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }     modCount++;    addEntry(hash, key, value, i);    return null;}

让我们逐个记下一步:

1)首先,key对象被检查为null。如果key为null,则将值存储在表[0]位置。因为null的哈希码始终为0。

2)然后在下一步,通过调用其hashCode()方法,使用密钥的哈希码来计算哈希值。该哈希值用于计算数组中存储Entry对象的索引。JDK设计师很好地假设可能会有一些编写不良的hashCode()函数,它们可以返回非常高或低的哈希码值。为了解决这个问题,他们引入了另一个hash()函数,并将对象的哈希码传递给这个hash()函数,使hash值在数组索引大小的范围内。

3)现在调用indexFor(hash,table.length)函数来计算用于存储Entry对象的精确索引位置。

4)这里是主要部分。现在,我们知道两个不相等的对象可以具有相同的哈希码值,两个不同的对象将如何存储在同一个数组位置[ 称为bucket ]中。
答案是LinkedList。如果你还记得,Entry类有一个属性“next”此属性始终指向链中的下一个对象。这正是LinkedList的行为。

因此,在碰撞的情况下,Entry对象以LinkedList形式存储。当一个Entry对象需要存储在特定的索引中时,HashMap会检查是否有一个条目?如果没有条目已存在,则Entry对象将存储在此位置。

如果已经存在坐在计算索引上的对象,则检查其下一个属性。如果为null,并且当前Entry对象成为LinkedList中的下一个节点。如果下一个变量不为空,则遵循以下步骤,直到下一个变量为null。

如果我们使用与之前输入的相同的键添加另一个值对象,该怎么办?在逻辑上,它应该替换旧的值。怎么做 那么,在确定Entry对象的索引位置之后,在计算索引上迭代LinkedList的同时,HashMap会为每个Entry对象的关键对象调用equals方法。LinkedList中的所有这些Entry对象将具有类似的哈希码,但equals()方法将测试真正的相等性。如果key.equals(k)将为真,则两个键都被视为相同的关键对象。这将导致仅在Entry对象内部替换值对象。

这样,HashMap就可以确保键的唯一性。

get()方法在内部如何工作

现在我们已经有了这个想法,键值对如何存储在HashMap中。下一个大问题是:当HashMap的get方法中传递对象时会发生什么?如何确定价值对象?

答案我们应该知道在put()方法中确定关键唯一性的方式,同样的逻辑也适用于get()方法。当HashMap识别作为参数传递的关键对象的完全匹配时,它只返回存储在当前Entry对象中的值对象。

如果没有匹配,get()方法返回null。

让我们来看看代码:

/*** Returns the value to which the specified key is mapped, or {@code null}* if this map contains no mapping for the key.** <p>* More formally, if this map contains a mapping from a key {@code k} to a* value {@code v} such that {@code (key==null ? k==null :* key.equals(k))}, then this method returns {@code v}; otherwise it returns* {@code null}. (There can be at most one such mapping.)** </p><p>* A return value of {@code null} does not <i>necessarily</i> indicate that* the map contains no mapping for the key; it's also possible that the map* explicitly maps the key to {@code null}. The {@link #containsKey* containsKey} operation may be used to distinguish these two cases.** @see #put(Object, Object)*/public V get(Object key) {    if (key == null)    return getForNullKey();    int hash = hash(key.hashCode());    for (Entry<K , V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {        Object k;        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))            return e.value;    }    return null;}

以上代码与put()方法相同,直到if(e.hash == hash &&((k = e.key)== key || key.equals(k))),在这个简单的值对象被返回之后。

主要说明

  1. 存储Entry数据的数据结构是一个名为Table的数组
  2. 数组中的特定索引位置称为bucket,因为它可以容纳Entry对象的LinkedList的第一个元素。
  3. Key对象的hashCode()是计算Entry对象的索引位置所必需的。
  4. Key对象的equals()方法用于在Map中维护Keys的唯一性。
  5. 在HashMap的get()和put()方法中不使用值对象的hashCode()和equals()方法。
  6. 空键的哈希代码始终为零,并且这样的Entry对象始终存储在Entry []中的零索引中。

 Java 8的改进

HashMap对象的性能改进是通过使用平衡树而不是链表来存储映射条目的键中存在大量冲突的。主要的想法是,一旦哈希桶中的项目数量增加超过一定的阈值,那么该桶将从使用链表的条目切换到平衡树。在高哈希冲突的情况下,这将改善从O(n)到O(log n)的最坏情况的性能

基本上,当一个桶变得太大(目前为:TREEIFY_THRESHOLD = 8)时,HashMap会动态地用树形图的ad-hoc实现替换它。这样,而不是悲观O(n),我们得到更好的O(log n)。

TreeNodes的Bins(元素或节点)可以像任何其他的一样遍历和使用,但是当过度填充时,还可以支持更快的查找。然而,由于绝大多数正常使用的垃圾箱没有人口过剩,所以在桌面方法的过程中检查树箱的存在可能会延迟。

树仓(即,其元素都是TreeNodes的仓)主要由hashCode排序,但是在绑定的情况下,如果两个元素相同class C implements Comparable<C>,则键入它们的compareTo()方法用于排序。

因为TreeNodes是常规节点大小的两倍,所以只有当bin包含足够的节点时才使用它们。当它们变得太小(由于移除或调整大小)时,它们被转换回平原箱(目前为:UNTREEIFY_THRESHOLD = 6)。在使用分布良好的用户hashCodes的用法中,很少使用树仓。



原创粉丝点击