HashMap底层原理
1.底层实现
- HashMap底层是哈希表,又称散列表
- 当向hashmap使用put方法添加对象时,会进行去重(hash)
- 数据结构之哈希表又称散列表.
- 是一种查询,插入等都较快的一种结构
- 可以认为哈希表就是一个数组
- 但是在Java的设计中,HashMap的哈希表是数组+链表+红黑树实现的
2.去重
通过实例解释
- 创建集合,设置泛型Integer,加入数字,数字重复去重---->结果(去重)
- 创建集合,泛型为学生对象(age,name),属性一致去重学生—>结果(没有去重)
实际操作中学生并没有去重,因为在集合中存取学生对象时,通过equals来判断存取的元素是否相等
需要重写equals,不然会调用父类的equals(判断地址值,每个对象地址值都不一样,因此无法去重)
但重写equals 还是没有去重 ,为什么?
因为调用equals之前,先调用了hashCode()方法,获得地址值,在通过地址值向哈希表存储元素.具体是,将hash值当哈希表的下标存储,即每个对象的hash值都不相同,也就是存储到哈希表的位置不一样,所以认为元素不一样,直接存储,没有去重。
因此,在重写equals判断元素内容相同的时候,也要重写hashCode ()方法让其地址值相同(但是性能不太好,下面有优化),这样的话,就会将hash值当哈希表的下标存储,他们就会存储到哈希表同一位置。
具体如下:
- 先算出存储元素的地址值
- 如果地址值不一样,直接根据hash值当下标存储到表中
- 如果地址值一样,根据hash值当下标存储,
- 如果当前位置为空,直接存储
- 如果当前位置有数据,调用equals比较内容是否相同
- 如果内容相同,不存
- 如果内容不相同,在同一位置形成链表,存储如下
-
优化:(优化哈希冲突)
1.重写hashCode让地址值完全一致,会造成一个问题,就是每存储一个元素都要比较equals.且hash值当下标的,所有元素hash值一样,就会一直往一个地方放,形成超长链表,性能不好

2.底层代码解决哈希冲突

总结:无脑重写hashCode和equals
3.为什么是无序的
因为存储的数据,不是按顺序存的,是按hash值当下标存储的,每个对象的hash值不同,下标也就不固定,所以是无序的
4.扩容机制
-
创建HashSet时,其实创建HashMap时底层hash表容量是16
-
加载因子是0.75
-
当存储的元素个数 >加载因子与当前容量的乘积时(0.75*16)=12时扩容(rehash),重构(创建大数组,原来数据打乱重排),变成原来的2倍
5.顺序为什么还会改变
因为,当存储元素超出阈值,即超出加载因子与当前容量的乘积时(0.75*16)=12时扩容,变成2倍,原来打乱重排,所以位置会变!!!
6 .底层代码



- Java中HashMap的实现的基础数据结构是
哈希表,其实就是数组
,每一对key->value的键值对组成Entry类以双向链表的形式存放到这个数组中- 元素在数组中的位置由key.hashCode()的值决定,如果两个key的
哈希值相等,即发生了哈希碰撞
,则这两个key对应的Entry将以链表的形式存放在数组中- 调用HashMap.get()的时候会首先计算key的值,继而在数组中找到key对应的位置,然后遍历该位置上的链表找相应的值。
当然这张图中没有体现出来的有两点:
- 为了提升整个HashMap的读取效率,当HashMap中存储的元素大小等于桶数组大小乘以负载因子的时候整个HashMap就要扩容,以减小哈希碰撞
- 在Java 8中如果桶数组的同一个位置上的链表数量超过一个定值(8个),则整个链表有一定概率会转为一棵红黑树,在哈希表扩容时,如果发现链表长度 <= 6,则会由树重新退化为链表
- 链表转变成树之前,还会有一次判断,只有数组长度大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
- 当数组某一个桶内的链表节点数大于8,且数组容量小于64添加节点时,当链表节点数大于8时,触发treeifyBin方法,方法内判断数组小于64则进行扩容而不转为红黑树
整体来看,整个HashMap中最重要的点有四个:初始化,数据寻址-hash方法,数据存储-put方法,扩容-resize方法,只要理解了这四个点的原理和调用时机,也就理解了整个HashMap的设计。
put的过程源码
put的流程:
a、将传递的key通过扰乱算法计算一个新的值(目的:让hashcodel的高16位参与亦或运算使得hash值算的更加散列,减少hash碰撞)
b、如果是第一次添加元素,那么会初始一个长度为16的Node数组
c、如果不是第一次添加,那么会将hash值和数组的长度-1(算出来的值一定在0~数组长度-1之间)做与运算计算的出一个数组的下标
通过计算的下标获取数组中对应位置上的值(有三种情况)
1、如果当前位置上的元素与要添加的元素相同,那么就覆盖原来的值
2、如果当前位置上的元素是一个红黑树节点,那么就将元素添加到红黑树上
3、如果当前位置上的元素是一个链表,那么遍历当前链表
1)如果要添加的元素与链表上的元素相同,则覆盖这个元素
2)如果与链表上的元素都不相同,则添加到数组的尾部(尾插法)
3)当链表的元素达到条件的时候,将链表转成红黑树
条件1:链表上元素个数大于8
条件2:数的长度大于64
d、添加完元素后,如果元素个数大于阈值(数组的长度*0.75)则需要对数组进行扩容
put方法的返回值
a、如果put进去的key不存在,那么返回null
b、如果put进去的key存在,那么返回被覆盖的原来的valuet值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab表示存放Node节点的数据 p表示当前节点 n表示长度 i表示节点在数组中的下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断数组如果为空或者数组长度为0,那么就对数组进行扩容,数组默认初始大小为16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//将数组的长度-1与hash值进行与运算(计算的结果一定是0~数组长度-1)得到元素应该存放的下标
//如果当前下标位置为空,那么直接将Node节点存放在当前位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//如果当前位置不为空(分为三种情况)
else {
Node<K,V> e; K k;
//情况1:要添加的元素与当前位置上的元素相同(hash(hashCode)、key(equals)一致),则直接替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//情况2:如果要添加的元素是红黑树节点,那么将其添加到红黑树上
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//情况3:如果要添加的元素是链表,则需要遍历
else {
for (int binCount = 0; ; ++binCount) {
//将当前元素的下一个节点赋给e
//如果e为空,则创建新的元素节点放在当前位置的下一个元素上,并退出循环
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表的元素个数大于8个(且当数组中的元素个数大于64),则将其转换成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//要添加的元素与当前位置上的元素相同(hash(hashCode)、key(equals)一致),则直接退出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果返回的e不为null
if (e != null) { // existing mapping for key
//将e的值赋给oldValue
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
//返回以前的值(当添加的元素已经存在返回的是以前的值)
return oldValue;
}
}
++modCount;
//如果数组的元素个数大于阈值则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize过程原码
HashMap的扩容时机
如果是第一次扩容
1)将定义一个长度为16的数组
2)阈值为数组的长度16*0.75
当元素的个数大于阈值的时候,数组需要进行扩容
1)将数组的长度扩大一倍
2)将阈值也扩大一倍
为什么加载因子为0.75?
加载因子影响到阈值
如果加载因子太大,那么空间利用率高,但是查询效率降低
如果加载因子太小,那么空间利用率低,但是查询效率提高
为什么数组的长度是2的次方数?
减少计算出来的下标的重复的几率(减少hash碰撞)
final Node<K,V>[] resize() {
//oldTab 表示原来数组(如果是第二次扩容:长度为16的那个)
Node<K,V>[] oldTab = table;
//oldCap 表示原数组的容量(长度)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr 表示数组原来的阈值 12
int oldThr = threshold;
//newCap 新数组的容量 newThr 新数组的阈值
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新数组的容量扩大一半 newCap 32
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新阈值扩大老阈值的一半 newThr 24
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//threshold 24
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建一个长度为32的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//table指向新数组
table = newTab;
if (oldTab != null) {
//将原数组中的元素拷贝到新数组中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果当前位置元素不为空
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//情况1:当前位置上的下一个元素为空,则直接将这个元素拷贝到新数组中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//情况2:当前位置上的元素红黑树类型,则需要进行切割
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//情况3:当前位置上的元素链表类型,则需要进行分散拷贝
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get的过程原码
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//当前first与要找到的hash和key都相等直接返回当前这个first元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果当前first不为空(有两种情况)
if ((e = first.next) != null) {
//当前位置是一个红黑树
if (first instanceof TreeNode)
//根据hash、key从红黑树上找到对应的元素
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//当前位置是一个链表
do {
//循环进行比较直到找到向的hash和key的元素,并返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//如果数组的为空、数组的长度为0、当前下标位置上的值为null,这三种情况都返回null
return null;
}
`
环进行比较直到找到向的hash和key的元素,并返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//如果数组的为空、数组的长度为0、当前下标位置上的值为null,这三种情况都返回null
return null;
}
`