HashMap的实现原理

HashMap介绍

HashMap是基于哈希表的Map接口的非同步实现,提供所有可选的映射操作,并允许使用null值和null键不保证映射的顺序,特别是它不保证该顺序恒久不变

HashMap的数据结构

在java编程语言中,最基本的结构就是两种,一个是数组,另一个是模拟指针(引用),所有的数据结构都可以用这个基本结果来构造,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,这是数组和链表的结合体,如下图所示
这里写图片描述

源代码


  //存储Node的数组
  transient Node<K,V>[] table;
  
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//对key的hashcode值进行hash运算后得到的值,存储在Node,避免重复计算
        final K key;
        V value;
        Node<K,V> next;//存储指向下一个元素的引用,单链表结构

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        }

HashMap的存取实现

(1)存储

  public V put(K key, V value) {
        //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(24=16)
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
       //如果key为null,存储位置为table[0]或table[0]的冲突链上
        if (key == null)
            return putForNullKey(value);
        //根据key的keyCode重新计算hash值
        int hash = hash(key.hasCode());
        //搜索指定hash值在对应table中索引
        int i = indexFor(hash, table.length);
        //如果i索引处的Entry不为null,通过循环不断遍历e元素的下一个元素。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
            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++;//modCount指修改次数
        //将key、value添加到i索引处
        addEntry(hash, key, value, i);
        return null;
    }    

就首先按照hash值(即下标)存储数组,如果发生哈希冲突(hash值相同),就在这个位置上的元素以链表的形式存储,新元素存储在头链头,HashMap中链表出现越少性能越好。

1、addEntry()方法

void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //如果Map的key-value对的数量超过了极限,就把table对象的长度扩充到原来的2倍
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

void addEntry(int hash, K key, V value, int bucketIndex) 方法根据计算出的hash值,将key-value对放在数组table的i索引处。addEntry是HashMap提供的一个包访问权限(就friendly,当前包才权限访问)的方法
从源码看出,当系统决定存储HashMap中key-value对时,是只考虑key的,而value是key的附属,key去哪value就去哪。

2、hash()方法

//这是一个神奇的函数,用了很多的异或,移位等运算,对key的hashcode进一步进行计算以及二进制位的调整等来保证最终获取的存储位置尽量分布均匀
final int hash(Object k) {
  
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

3、indexFor()方法

 static int indexFor(int h, int length) {
        return h & (length-1);
    }

这个方法通过 h & (length-1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化, h & (length-1)运算等价对length取模,也就h%length,但是&比%具有更高的效率。

(2)读取

 public V get(Object key) {
     //如果key为null,则直接去table[0]处去检索即可。
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
 }

HashMap的resize(rehash)

当HashMap中元素越来越多,hash冲突的概率就越来越大,为了提高查询效率,就对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的情况就是:原数组中的数据必须重新计算其在新数组的位置,并放进去,这也就是resize
当HashMap中的元素个数超过数组大小length*loadFactor就进行扩容,loadFactor默认值是0.75,也就是数组大小16,当HashMap中元素个数超过16
*0.75=12的时候就进行扩容。扩展到16 *2=32,然后重新计算其在新数组的位置

HashMap的性能参数

HashMap()
构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空HashMap。
HashMap(int initialCapacity)
构造一个带指定初始容量和默认加载因子 (0.75) 的空HashMap。
HashMap(int initialCapacity, float loadFactor)
构造一个带指定初始容量和加载因子的空HashMap。
##Fail-Fast机制
我们知道HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么抛出ConcurrentModificationException,这就是fail-fast策略

HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

在迭代过程,判断modCount和expectedModCount 是否相等,如果不相等就表示已经有其他线程修改Map

注意modCount声明为volatile,保证线程之间修改的可见性.

为什么链表的长度为8是变成红黑树?为什么为6时又变成链表?

为6时 :

  • 当链表平均长度为 n/2=3
  • 红黑树为 log(6)=2.6

为8时 :

  • 链表 8/2=4
  • 红黑树 log(8)=3

根据两者的函数图也可以知道随着bin中的数量越多那么红黑树花的时间远远比链表少,所以我觉得这也是原因之一。为7的时候两者应该是 链表花的时间小于红黑树的,但是为什么不是在7的时候转成链表呢,我觉得可能是因为把7当做一个链表和红黑树的过渡点。

当链表长度为6时 查询的平均长度为 n/2=3 等于 红黑树 log(8)=3,也就是从6到8那边红黑树查找大幅度比链表快,把7当做一个链表和红黑树的过渡点。

通俗点将就是put进去的key进行计算hashCode时 只要选择计算hash值的算法足够好(hash碰撞率极低),从而遵循泊松分布,使得桶中挂载的bin的数量等于8的概率非常小,从而转换为红黑树的概率也小,反之则概率大。

所以,之所以选择8,不是拍脑袋决定的,而是根据概率统计决定的

为什么不开始就变成红黑树?

TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值