HashMap底层源码

这里是修真院后端小课堂,每篇分享文从

本篇分享的是:【HashMap 】

(1)背景介绍:

不讲HashMap的使用方法,看一看底层的源码是什么?

思考:HashMap使用key,·value进行存储,使用的数据结构是什么?

我们知道数组和链表两种数据结构

数组:

优点:查询速度快

缺点:增加和删除慢

链表:

优点:增加和删除快

缺点:查询速度慢

那我们可不可以将两者的优点结合一下,达到查询、增加、删除效率都非常快呢?

所以我们猜测一下,HashMap能否源码底层数据结构是采用的链表+数组的形式呢?

(2)知识剖析:

1.HashMap数据底层具体存储的是什么?

Java是一门面向对象开发的语言,可以把所有东西可以看做是对象。

通过查看源码可知:map里面的key和value都保存在了这个Node对象里面。

class Node<K,V>{

private K key;   //用来定位数组索引位置

private V value;

private Node<K,V> next;  //链表的下一个node

}

2.数组怎么表示?

transient Node< K,V>[] table;

每个列表被称为桶,即哈希桶数组,是一个Node的数组。

HashMap使用哈希表进行存储。怎样得到表中对象的索引位置?

key.hashCode( )----->hashCode----->Hash算法的高位运算和取模运算----->得到存储位置

有时两个key会定位到相同的位置,就发生了Hash碰撞。Hash算法计算结果越分散均匀,Hash碰撞的概率就越小,map的存取效率就会越高。

如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现较多碰撞。

3.数组要不要有一个大小呢?

学过数组都知道,要给数组一个初始化大小,那么HashMap有没有大小呢?

Defalutsize = … 初始大小???

Maxmumsize = … 上限大小???

肯定是要有的。

4.数组初始大小是多少呢?

从源码可以看到

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

初始大小是16

如果初始大小不够用,或者达到某个值,是不是要进行数组大小的扩容?

final Node<K,V>[] resize()

5.扩容是不是要有一个依据?

int threshold = 16; Node[] table的初始化长度

transient int size; HashMap中实际存在的键值对数量

size > threshold * 小于1的小数

实际使用的size肯定不可能达到16才进行扩容,那样会损失性能,一定要达到一个点就进行扩容,这个点是0.25 0.5 0.75哪一个呢?

源码中定义了一个扩容因子

static final float DEFAULT_LOAD_FACTOR = 0.75f

size = threshold * Load factor ----->16 * 0.75 =12

所以在12的时候就会进行扩容。

为什么要选择0.75?

默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

6.链表的是不是需要一个长度限制呢?

负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。链表达到一个限制值后,是否需要进行一个改变?

在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。

当链表长度太长时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能

源码中定义,当链表超过8时,就转化为红黑树

static final int TREEIFY_THRESHOLD = 8;

7.Node节点到底存在哪里?

存在哪里要有一个位置,这个位置就是通过计算得来的。

利用这个key进行一个计算,计算出这个Node应该存在哪里。

想知道key,value存放在HashMap结构中的位置,使用hash函数,源码的实现(方法一+方法二):

方法一:

static final int hash(Object key) { //jdk1.8 & jdk1.7

 int h;

 // h = key.hashCode() 为第一步 取hashCode值

 // h ^ (h >>> 16)  为第二步 高位参与运算

 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

方法二:

static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的

 return h & (leng

(3)常见问题:

HashMap线程安全吗?

(4)解决方案:

HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全在多线程使用场景中,应该尽量避免使用线程不安全的HashMap,而使用线程安全ConcurrentHashMap。

(5)编码实战:

比如我的一个key的哈希值

h=hashCode():

11111111111111111111000011101010

h>>>16:

00000000000000001111111111111111

hash=h^(h>>>16):

11111111111111110000111100010101

(n-1)&hash:

11111111111111110000111100010101

最后取后四位0101转换成十进制就是5

(6)拓展思考:

JDK1.8与JDK1.7的性能对比?

JDK1.8引入红黑树大程度优化了HashMap的性能。

(7)参考文献:

Java 8系列之重新认识HashMap——美团技术团队

(8)更多讨论:

Q1.Map集合还有哪些实现类?

A1:LinkedHashMap、Hashtable和treeMap

Q2.HashMap为什么要保证hashmap的容量为2的幂次方?

A2:HashMap容量为2的幂次方,是为了减小哈希冲突,因为如果存在哈希冲突的时候,存储entry的时候,会出现一个位置上存在多个entry的情况,会造成读写速度的降低;使用2的幂次方的容量,存储的时候,会根据key的哈希值跟hashmap的(容量-1)进行位运算,这样的运算,当(容量-1)的二进制数全为1的时候,存储位置的计算结果是哈希冲突最小的。

Q3.HashMap优缺点?

A3:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

(9)鸣谢:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值