HashMap的数据结构

HashMap 底层是数组,JDK7 和 JDK8 版中HashMap实现是大有不同的。

存储结构:JDK7 是数组+链表,JDK8 是数组+链表+红黑树

1、为什么要使用数组+链表的形式?

HashMap 底层数组的默认大小是 16,大索引是 15, 数组是可以扩容的。

HashMap 以 key-value 的形式进行存储,key+value 会以一个 Entry 对象的形式来存储,把 Entry 存入数组 中,具体存入的下标不是按顺序存入,而是通过 key 获 取对应的 hash 值,再根据 hash 值与数组大索引进行按位与运算(将操作数转换为二进制,分别对比各位上的值,都为 1 则该位返回 1,否则返回 0,得到的结果就是按位与运算结果),与数组大索引进行按位与运算是为了保证数组下标不越界,A & B 结果一定是小于等于 B

很有可能出现两个 Entry 下标一样,hash 冲突,我们应该尽量避免,如果一旦一样怎么处理?意味着需要将两个 Entry 存入数组中的同一个位置,所以就形成了链表。数组中存储的是第一个 Entry 的地址,第二个 Entry 就 往后排。

2、为什么要使用红黑树呢?

链表的缺点是查询效率低,因为查询链表中的任意一个元素,都需要从头开始遍历。

这种方式会导致 HashMap 取数据很慢,而取数据又是 HashMap 常用的一个操作,所以 JDK 8 引入了红黑树来解决这个问题。

红黑树可以解决链表查询效率低的问题

红黑树是一种数据结构,其实是一个平衡搜索二叉树,二叉树的查询效率很高。

如何让二叉树保持平衡?这就是红黑树的作用。

1、节点是红色或者黑色。

2、跟节点是黑色。

3、每个叶子节点都是黑色的空节点。

4、每个红色节点的两个子节点都是黑色(从每个叶子节 点到根的路径上不能有两连续的红色节点)

5、从任一节点到它每个叶子节点的路径所包含的黑色节点的数目一致。

2aba775798994fac84456e6352481775.jpg

HashMap的构造函数:

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

HashMap 有两个重要的参数:容量(capacity)和 负载因子(loadFactor)

容量:是指 HashMap 中桶的数量,默认值是 16
负载因子:判断 HashMap 是否需要扩容,默认值是 0.75
HashMap 存放的元素总数量 / 容量,当该值等于 0.75 的时候,HashMap 就需要进行扩容

16*0.75 = 12

当 HashMap 中元素个数超过 12 的时候,数组就需要进行扩容,成倍扩容 32

//初始默认值   1<<4 表示 1左移四位就扩大为2的4次方 32
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
// aka 16 //数组最大容量 
static final int MAXIMUM_CAPACITY = 1 << 30; 
//默认加载因子 
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
//判断链表是否要转为红黑树 
static final int TREEIFY_THRESHOLD = 8; 
//判断红黑树是否转为链表 
static final int UNTREEIFY_THRESHOLD = 6; 
//用来判断到底是链表转红黑树还是数组扩容 容量超过64就转换为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;

HashMap中的hash方法:

//Java 8中的散列值优化函数
static final int hash(Object key) {
    int h;
    //无符号右移
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);    //key.hashCode()为哈希算法,返回初始哈希值
}

当 key = null 直接返回 0 (即key为空的话,该键值对放置在哈希数组 0索引的位置),否则返回 key 的 hashCode 值与自己的高 16 位进行异或运算

h >>> 16 是用来取出 h 的高 16 位,>>> 无符号右移

0000 0100 1011 0011 1101 1111 1110 0001

>>> 16

0000 0000 0000 0000 0000 0100 1011 0011

为什么不直接采取hashcode的返回值作为hash函数的返回值?

因为hashcode的映射返回值是int类型的,伦理上是很大的,int 的取值范围是 -21 亿到 21 亿。 因此需要想办法解决下述问题。

1、长度为40亿的数组 内存也是装不下的 ,数组也不可能那么长。

2、同时如果只取哈希的地位用来进行异或运算很容易产生哈希冲突。

因此通过使用高16位和低位进行异或运算,这样可以混合hash码的高位和地位,以此来增加地位的随机性,而且混合之后地位同样包含高位的信息,散列分布更加均匀,冲突的概率更小。

拿到 key 的 hash 值之后,还要跟数组大索引进行按 位与运算(都为 1 返回 1,否则返回 0),终的值才 是正在的数组下标。

h=hashCode:1111 1111 1111 1111 1111 0000 1110 1010

h>>>16: 0000 0000 0000 0000 1111 1111 1111 1111

^: 1111 1111 1111 1111 0000 1111 0001 0101

15: 0000 0000 0000 0000 0000 0000 0000 1111

& 0000 0000 0000 0000 0000 0000 0000 0101

index = 5

HashMap的put函数:

//put方法,会先调用一个hash()方法,得到当前key的一个hash值,
//用于确定当前key应该存放在数组的哪个下标位置
//这里的 hash方法,我们姑且先认为是key.hashCode(),其实不是的,一会儿细讲
 public V put(K key, V value) {
 
        //返回调用putVal方法
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // tab指代是hashmap的散列表再,在下方初始化,hashmap不是在创建的时候初始化,而是在put的时候初始化,属于懒初始化
        // p表示当前散列表元素
        // n表示散列表数组长度
        // i表示路由寻址的结果
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       //判断是否为空,为空的话初始化,不为空对tab和n进行赋值
        if ((tab = table) == null || (n = tab.length) == 0)
            //resize扩容
            n = (tab = resize()).length;
         //这个i就是(n-1)和hash做与运算得到的位置,p就是这个位置的Node元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            //直接在当前下表newNode
            tab[i] = newNode(hash, key, value, null);
         //如果要插入的元素在这个位置有元素了,执行以下操作
        else {
            //e 临时的node元素
            //k 表示临时的一个key
            Node<K,V> e; K k;
            //如果这个桶的位置的元素的key和将要插入的key是一个,会进行替换
            // 比较 哈希值 : 引用地址  : key
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))                 
                //如果相等则老元素地址指向新元素
                e = p;
            //不相等则判断节点类型:
            //结点类型为树 TreeNode是Node的一个子类
            else if (p instanceof TreeNode)                
                //红黑树的插入操作
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //节点类型为链表
            else {
                ///for循环:1.遍历到链表尾部,进行尾插
                //2.判断链表长度,超过8将链表改为红黑树
                for (int binCount = 0; ; ++binCount) {
                    //判断节点的下一个节点为空,遍历到链表尾部,进行尾插
                    if ((e = p.next) == null) {                       
                        //生成一个Node对象,将Node对象作为新节点插入到链表(p.next)
                        p.next = newNode(hash, key, value, null);
                          //如果链表长度 >= TREEIFY_THRESHOLD-1 = 7 因为是从0开始遍历,所以此时链表长度为8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //树化函数
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    //key相同时同样插入返回
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //这种属于覆盖操作,当e中有值进入操作
            if (e != null) { // existing mapping for key
                //oldValue保存老的值,方便return
                V oldValue = e.value;
                //onlyIfAbsent传入的是false,指定能进入判断
                if (!onlyIfAbsent || oldValue == null)
                    //新元素的值将老元素的值覆盖掉
                    e.value = value;
                //HashMap提供给子类的方法
                afterNodeAccess(e);
                //put操作有返回值,返回的是插入之前已经存在的元素的value值
                return oldValue;
            }
        }
 
        //增加修改次数
        ++modCount;
    	//统计当前map中有多少元素,和阈值对比,判断是否需要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

转红黑树的方法:

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
//转为红黑树之前 先要判断是否到达数组的最大容量 若没有则先进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

如果数组的长度小于64,则不进行红黑树的转换,而是继续进行数组扩容,如果数组的长度大于64,再将链表转为红黑树。

HashMap的存值过程:

1、根据key计算hash值。

2、在put的时候判断数组是否存在,如果不存在则用resize 方法创建默认长度为16的数组。3、确定要存入的 Node 在数组中的位置,根据 hash 值与数组最大索引进行按位与运算得到索引位置。

4、判断该位置是否有元素,如果没有直接创建一个Node 存入。

5、如果有元素,判断 key 是否相同,如果相同则覆盖,并且将原来的值直接返回。

6、如果 key 不相同,在原 Node 基础上添加新的Node,判断该位置是链表还是红黑树。

7、如果是红黑树,将 Node 存入红黑树。

8、如果是链表,遍历链表,找到最后一位,将 Node 存入。

9、将 Node 存入链表之后,判断链表的结构是否要调整,判断链表长度是否超过 8,如果超过 8 需要将链表转为红黑树,这里还有一个条件,如果数组的容量小于64,不转换红黑树,而是进行数组扩容,当数组的容量大于 64 的时候,再将链表转为红黑树。

10、存完之后,再次判断数组是否进行扩容,根据负载因子来判断。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

说好陪我数星星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值