关闭

HashMap 内部原理

标签: hashmap
3678人阅读 评论(2) 收藏 举报
分类:

HashMap 内部实现

通过名字便可知道的是,HashMap 的原理就是散列。HashMap内部维护一个 Buckets 数组,每个 Bucket 封装为一个 Entry<K, V> 键值对形式的链表结构,这个 Buckets 数组也称为表。表的索引是 密钥K 的散列值(散列码)。如下图所示:

这里写图片描述

链表的每个节点是一个名为 Entry<K,V> 的类的实例。 Entry 类实现了 Map.Entry 接口,下面是Entry类的代码:

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

注: 每个 Entry 对象仅与一个特定 key 相关联,但其 value 是可以改变的(如果相同的 key 之后被重新插入不同的 value) - 因此键是最终的,而值不是。 每个Entry对象都有一个名为 next 的字段,它指向下一个Entry,所以实际上为单链表结构。hash 字段存储了 Entry 对象在 Buckets 数组索引,也就是 key 的散列值。

如果发生Hash碰撞,也就是两个key的hash值相同,或者如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。

影响 HashMap 性能的两个因素是初始容量和负载因子。容量是表数组的长度,初始容量只是创建哈希表时的容量。负载因子是衡量哈希表在容量自动增加之前是否允许获取的量度(比例)。

当散列表中的 Entry 数量超过负载因子和当前容量的乘积时,将会重新散列该表(也就是重建内部数据结构),使得散列表具有大约两倍的容量(这个其实和ArrayList类似)。

理解 put() 方法

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key); // 计算hash值
        int i = indexFor(hash, table.length); // 计算在数组中的索引
        // 遍历链表
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // hash值相同并且key相等,就直接替换
            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;
    }
注:这个计算出来的hash值被传递给内部哈希函数,哈希函数将返回密钥的散列值。这个值就是 bucket/数组 的索i引。
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

这里就有个疑问了,我们如何计算对应存储数组索引,首先想到的就是把hashcode对数组长度取模运算,也就是h%length,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那中?

首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。

如下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!

这里写图片描述

上图参考自:http://blog.csdn.net/oqqYeYi/article/details/39831029

理解 get() 方法

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key); // 计算hash值
        // 根据索引遍历链表,找出相等的key
        for (HashMapEntry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

get 与 put 总结

下面总结了 put()get() 发生的三个重要步骤:

  1. 通过调用 计算 Hash Code 方法计算密钥的哈希码。
  2. 将计算的散列码传递到内部散列函数indexFor()以获取表的索引。
  3. 迭代通过在索引处出现的链表,并调用 equals() 方法来查找匹配键。

所以在这之前要先理解 equals()hashCode() 这两个方法。

在 Java8 中的改善

在Java 8中,对HashMap有一个性能上的改进。当密钥中存在许多哈希冲突(不同的密钥最终具有相同的哈希值或索引)时,平衡树将用于存储 Entry 对象,而不是链表。做法是,一旦 bucket 中的 Entry 数量增长超过某一阈值,则 bucket 将从 Entry 链表切换到平衡树。

5
5
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

HashMap实现原理分析

HashMap其实也是一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。这可能让我们很不解,一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。   首先...
  • vking_wang
  • vking_wang
  • 2013-11-05 15:23
  • 291042

HashMap的存储结构及原理

HashMap是由数组+链表组成;寻址容易,插入和删除困难。(存储单元数组Entry[],数组里面包含链表) 3、HashMap、HashTable和ConcurrentHashMap的线程安全问题 ...
  • hu948162999
  • hu948162999
  • 2014-12-24 05:05
  • 5469

HashMap的原理和内部实现机制

HashMap为什么快? 就是和它优雅的设计密切相关的。 HashMap map = new HashMap(2);       map.put("语文" , 80.0);     &#...
  • crethfly
  • crethfly
  • 2014-04-20 21:44
  • 650

LRUCache原理及HashMap LinkedHashMap内部实现原理

LRUCache HashMap LinkedHashMap内部实现原理
  • hlglinglong
  • hlglinglong
  • 2015-11-27 17:11
  • 2106

了解HashMap的get和put内部的工作原理,需要理解透Java HashMap的原理

了解HashMap的get和put内部的工作原理,需要理解透Java HashMap的原理,今天我们单说get和put 的工作原理。 一、Put : 让我们看下put方法的...
  • chajinglong
  • chajinglong
  • 2016-03-12 22:01
  • 681

hashmap实现原理

  • 2014-10-15 22:38
  • 194KB
  • 下载

HashMap的实现原理

  • 2016-12-12 10:29
  • 573KB
  • 下载

什么时候ReHash,HashMap的内部实现机制,Hash是怎样实现的 - schbook

原文:http://www.cnblogs.com/schbook/p/3585159.html?utm_source=tuicool&utm_medium=referral 1.H...
  • SoundSlow
  • SoundSlow
  • 2016-04-28 14:20
  • 365

HashMap的内部实现机制

1.HashMap的内部实现机制 HashMap是对数据结构中哈希表(Hash Table)的实现, Hash表又叫散列表。Hash表是根据关键码Key来访问其对应的值Value的数据结构,它通...
  • VIP_WangSai
  • VIP_WangSai
  • 2017-05-17 22:54
  • 192

图解HashMap和HashSet的内部工作机制

1)使用键(key)和值(value)将一个对象放入 map 中时,会隐式调用 hashCode() 方法,返回哈希值(hash code value),比如 123。两个不同的键能够返回一样的哈希值...
  • sniperken
  • sniperken
  • 2016-11-01 22:07
  • 515
    个人资料
    • 访问:742639次
    • 积分:5904
    • 等级:
    • 排名:第5030名
    • 原创:71篇
    • 转载:3篇
    • 译文:3篇
    • 评论:402条
    我的微博