HashMap
学习资料
https://www.cnblogs.com/skywang12345/p/3310835.html
https://blog.youkuaiyun.com/jeffleo/article/details/54946424
http://www.importnew.com/16301.html
HashMap数据结构
- JDK7之前hashmap又叫散列链表:基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储。
- JDK8中,当同一个hash值(Table上元素)的链表节点数不小于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。这就是JDK7与JDK8中HashMap实现的最大区别。
==本文是基于JDK1.7的HashMap的学习==
HashMap常量
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1073741824;
static final float DEFAULT_LOAD_FACTOR = 0.75F;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
transient Set<Entry<K, V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
- DEFAULT_INITIAL_CAPACITY: 初始容量,16
- DEFAULT_LOAD_FACTOR: 默认加载因子:0.75。加载因子是控制HashMap自动扩容的一个因子。HashMap条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构)。比如,当前容量时16,1*0.75=12,当HashMap元素个数超过12时,就会自动扩容到32.
- MAXIMUM_CAPACITY:1073741824 = Integer.MAX_VALUE = 2的31次方
- size:HashMap的大小
- threshold:HashMap的极限容量,扩容临界点(容量和加载因子的乘积)
- loadFactor:负载因子loadFactor衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。
- Entry< K,V>[] table:Entry类型的数组,HashMap用这个来维护内部的数据结构,它的长度由容量决定
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
//用于指向下一个元素,组成链表结构
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
//创建新元素时,把next指向旧元素,维持链表结构
next = n;
key = k;
hash = h;
}
·····
}
HashMap构造方法
// 默认构造函数。
HashMap()
// 指定“容量大小”的构造函数
HashMap(int capacity)
// 指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity, float loadFactor)
// 包含“子Map”的构造函数
HashMap(Map<? extends K, ? extends V> map)
这里主要学习下 HashMap(int capacity, float loadFactor) 构造方法,因为我看到HashMap对容量的规定是必须是2的幂次方,而这个控制算法很值得学习下
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;
//控制容量必须是2的幂次方
this.threshold = tableSizeFor(initialCapacity);
}
- 容量控制算法
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
在java中,”>>>”表示无符号右移位运算符,”m >>> n”表示m的二进制右移n位,高位补0(无符号)。如 13 >>> 2 相当于01101右移两位高位补0,得到00011
tableSizeFor(int cap)方法主要目的就是算出离cap最近的2的幂次方数(大于或等于cap)。
主要是利用了cap的最高位1右移运算(>>>),然后或运算(|=),实现了,从最高非0位开始,后面全部是1,从而算出离cap最近的2的幂次方数。
比如:
int n = cap - 1 = 01xxxxxxxx
让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。
n >>> 1 得到 001xxxxxxx, ” |= “与运算就是 01xxxxxxxx | 001xxxxxxx = 011xxxxxxx ,最高非0位开始,2位都是1
n >>> 2 得到 00011xxxxx, ” |= “与运算就是 011xxxxxxx | 00011xxxxx = 01111xxxxx ,最高非0位开始,4位都是1
以此类推,int是32位,最终只需要把高16位右移到低16位,然后进行与运算,就能算出离cap最近的2的幂次方数:0111111111
那么问题来了,为什么要把容量控制在2的幂次方?,在后面的put方法会讲到。
存储put
特点
- HashMap允许null做key值。HashMap将“key为null”的元素都放在table[0]位置
- 调用put方法时,如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value;
如果是新的一个key,则返回的是null;
代码实现
public V put(K key, V value) {
//如果key为空的情况,放在table[0]
if (key == null)
return putForNullKey(value);
int hash = hash(key);
//计算该hash值在table中的下标
int i = indexFor(hash, table.length);
//对table[i]存放的链表进行遍历
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//把当前key,value添加到table[i]的链表中
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建Entry节点,存储新数据
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//先获取到bucketIndex位置的旧元素
Entry<K,V> e = table[bucketIndex];
//创建新节点,同时把新节点的next指向旧元素e,维持了链表结构
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
- 根据hash计算位置
static int indexFor(int h, int length) {
return h & (length-1);
}
这里我们假设length为16(2^n)和15,h为5、6、7
h | lenght | h & (length-1) | result |
---|---|---|---|
5 | 16 | 0101 & 1111 = 0101 | 5 |
6 | 16 | 0110 & 1111 = 0110 | 6 |
7 | 16 | 0111 & 1111 = 0111 | 7 |
该方法仅有一条语句:h&(length -1),这句话除了上面的取模运算外还有一个非常重要的责任:均匀分布table数据和充分利用空间。
==当length = 2^n时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。== 所以HashMap会限制size是2的幂次方。
获取元素get
理解了HashMap的数据结构,获取元素也就很好理解了
public V get(Object key) {
//如果key为null,求null键
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
HashMap 非线性安全
- put新增元素时
上面讲HashMap插入元素(addEntry)时,会创建新Entry节点,同时把新节点的next指向旧元素e,维持了链表接口
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
//创建新节点,同时把新节点的next指向旧元素e,维持了链表接口
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
在高并发时候,就会存在一个线程获取到的旧元素,而另外一个线程又同时插入新元素,更新了链表head头元素
- put更新节点值时
上面同样说到过,如果已经存在一个相同的key, 则返回的是前一个key对应的value,同时该key的新value覆盖旧value
public V put(K key, V value) {
·······省略
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
······省略
高并发时,有两个线程同时进入了上面判断语句后,数据就会被混乱
remove时
同put原理。rehash时
当多个线程同时检测到总数量超过门限值的时候就会同时调用resize操作,各自生成新的数组并rehash后赋给该map底层的数组table,结果最终只有最后一个线程生成的新数组被赋给table变量,其他线程的均会丢失。而且当某些线程已经完成赋值而其他线程刚开始的时候,就会用已经被赋值的table作为原始数组,这样也会有问题。