将总结的内容记录下来,方便他人,也能防止自己遗忘!脑容量不够的时候,真是捉急啊!
一、概述
1. 什么时候会使用HashMap?有什么特点呢?
2. HashMap的工作原理
3. get和put的原理是怎么的?equals()和hashCode()都有什么作用?
4. hash的实现,为什么要这样实现呢?
5. 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
执行下面的操作时:
HashMap<String,Integer> map = new HashMap<String,Integer>();
map.put("语文",1);
map.put("数学",2);
map.put("英语", 3);
map.put("历史", 4);
map.put("政治", 5);
map.put("地理", 6);
map.put("生物", 7);
map.put("化学", 8);
for(Entry<String,Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
输出的结果为:
政治: 5
生物: 7
历史: 4
数学: 2
化学: 8
语文: 1
英语: 3
地理: 6
下面是大致的结构,对HashMap有一个初步的认识:
在官方文档中有几个关键的信息:基于Map接口实现、允许null键/值、非同步、不保证有序(比如插入的顺序)、也不保障不随时间变化。
二、两个重要的参数
在HashMap中有两个重要的参数,容量(Capacity)和负载因子(Load factor)
Capacity就是bucket的大小,Load factor就是bucket填满程度的最大比例。如果对迭代性能要求很高的话,不要把capacity设置的过大,也不要把load factor设置过小。当bucket中的entries的数目大于capacity*load factor时候就需要调整bucket的大小为当前的2倍。
三、put函数的实现
put函数大致的思路:
1.首先调用Key的hash方法,计算出哈希码,通过哈希码快速找到莫格存放位置(桶),这个位置可以被称为bucketIndex
2. 如果发生了碰撞(可能多个元素找到了相同的bucketIndex),这时会取到bucketIndex位置已经存储的元素,最终通过equals来比较,equals方法就是再碰撞的情况下才会执行的方法,并以链表的方式存在buckets里。
3. 如果没有发生碰撞直接放入bucket里
4. 如果碰撞导致链表过长(大于等于TREEIEY_THRESHOLD),将链表转换成红黑树;
5. 如果节点已经存在就替换old value(保证key的唯一性)
6. 如果bucket满了(超过load factor*current capacity),需要resize。
四、get函数的实现
get函数的大致思路为:
1. 如果是bucket中的第一个节点,直接读取
2. 如果有冲突,通过key.equals(k)去查找对应的entry
若为树,则在树中通过key.equals(k)查找,O(logn);
若为链表,则在链表中通过key.equals(k)查找,O(n).
详情可见代码:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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) {
// 直接命中
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 未命中
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 在链表中get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
五、hash函数的实现
在get和put的过程中,计算下标时,先对hashCode进行hash操作,然后再通过hsah值进一步计算下标,详细如下:
在对hashCode()计算hash时具体实现是这样的:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个函数大概的意思就是:高16bit不变,低16bit和高16bit做了一个异或。
在之前说过的,在获取HashMap的元素时,基本上分为两部:
1. 首先根据hashCode(),然后确定bucket的index;
2. 如果bucket的节点的key不是我们需要的话,通过keys.equals()在链中找。
在Java8之前的实现时用链表解决冲突的,在产生碰撞的情况下,进行get的时候,这两部的事件复杂度时O(1)+O(n)。因此,在碰撞比较严重的时候n会很大,O(n)的速度显然时影响速度的。
在Java8中,利用红黑树替换链表,这样复杂度就变成了O(1)+O(logn)了,这样在n很大的时候,对速度又较大的提升。
六、 RESIZE的实现
当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程中,将bucket扩充2倍,之后重新计算index,将节点放入新的bucket中。又因为我们用的是2次幂的扩展(指长度扩为原来2倍),元素的位置要么在原来的位置,要么在原来的位置移动到2次幂的位置。
七、 总结
根据开头咱们说的几点来进行下简单的总结:
1. 什么时候会使用HashMap?有社么特点呢?
基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的,HashMap存储着Entry(hash,key,value,next)对象。
2. 你知道HashMap的工作原理吗?
通过hash的方法,通过put和get存储和获取对象。存储对象时,将key/value传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Factor,则resize为原来的2倍)获取对象时,我们将K传给get,他会调用hashCode计算hash从而得到bucket位置,并进一步调用equals方法确定键值对。如果发生碰撞时,HashMap通过链表将产生碰撞冲突的元素组织起来,在Java8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。
3. get和put的原理是怎样的?equals和hashCode有社么作用?
通过对key的hashCode()进行hashing,并计算下标((n-1)&hash),从而获得bucket的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点
4. hash实现是怎么样的呢?为啥要这么实现?
在Java1.8的实现中,通过hashCode()的高16位异或低16位实现的:(h=k.hashCode())^(h>>>16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销。
5. 如果HashMap的大小超过负载因子(load factor)定义的容量,怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap,并能重新调用hash方法。