HashMap数据结构
HashMap是一种数组加链表的数据结构。基于Map接口方式实现,元素以键值对的方式存储,允许null为key,key不允许重复,无序,线程不安全。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f; //负载因子0.75
如图:
存取过程
put
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
put的时候先判断数组容量,如果是0就初始化数组。数组默认大小为16,当然hashMap提供一个有参数的构造方法来指定数组容量。注意 ,并不是容量并不由我们指定的算,eg:当你指定容量是6,那么会生成一个为8的数组,如果思20,就会生成一个32的数组,也就是你想初始一个大小为n的数组,HashMap会初始化一个大小 大于等于N的二次方的一个数组。
如果key是null,就把改元素放到一个指定的位置。
计算要存放的数组位置: 计算key的hash值,用key的hash和数组的size 做 & 与运算
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
按位与运算符(&)
参加运算的两个数据,按二进制位进行“与”运算. 两位同时为“1”,结果才为“1”,否则为0
eg: 3&5 即 0000 0011& 0000 0101 = 00000001 因此,3&5的值得1。
为什么计算数组下标要用 与 操作
因为把任意长度的字符串变成固定长度的字符串,所以存在一个hash对应多个字符串的情况,所以碰撞必然存在
为了减少hash值的碰撞,需要实现一个尽量均匀分布的hash函数,在HashMap中通过利用key的hashcode值,来进行位运算
公式:index = e.hash & (newCap - 1)
所以我们发现,如果我们想把HashCode转换为覆盖数组下标取值范围的下标,跟我们的length是非常相关的,length如果是16,那么减一之后就是15(0000 1111),正是这种高位都为0,低位都为1的二级制数才保证了可以对任意一个hashcode经过逻辑与操作后得到的结果是我们想要的数组下标。这就是为什么在真初始化HashMap的时候,对于数组的长度一定要是二次方数,二次方数和算数组下标是息息相关的,而这种位运算是要比取模更快的。
如果hash冲突 以链表的方式进行存储。将新节点插在链表的头部,此时新节点就是当前这个链表的头节点,接下来把头节点移动到数组位置即可。
如果key相同,value覆盖,返回新值
总结:初始化数组,,
- 如果数组为空,初始化数组
- 计算key的hash值,hash值和数组size-1做与运算 得到下标
- 遍历改下标下的链表,如果 key相同,value覆盖,返回旧值
- 将key,value组成Entry对象,将节点插入 index 链表的头部
- 将链条的头节点移动到数组上
get
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();}
- 判断数组为0,直接返回空
- 计算key的hash值,hash值和数组size-1做与运算 得到下标
- 遍历改下标下的链表,比较key的hash,key值 返回entry对象
扩容
数组容量扩大两倍,然后把元素 移到新 数组。eg index = 1 ,链表 1 2 3 。
移动 链表的时候有规律 在jdk8 会改动。
jdk1.8变动:
链表长度在8 的时候 会判断 数组的容量大于64吗,如果小于 会扩容 数组。
如果链表长度大于8 并且 容量大于64 会将链表改成 红黑树。
因为红黑树 存 取 的速度都比较均匀。