HashMap学习
众所周知,HashMap的底层是数组加链表的方式实现的,在jdk1.7和jdk1.8的实现方式有所改变,接下来让我们去学习
jdk7
首先从JDK7的put方法进入
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//初始化
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
//对key进行hash运算得到hash值
int hash = hash(key);
//计算下标索引
int i = indexFor(hash, table.length);
//循环遍链表,如果有相同的key则替换,返回原来的值
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++;
//如果没有key相同,则新增(头插法)
addEntry(hash, key, value, i);
return null;
}
初始化
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
//去找大于这个toSize的最小的2的次方幂的数 如 tosize = 7 那么capacity = 8
// tosize = 14 那么capacity = 16
//这个是程序设计,扩容也是原来的2倍,计算下标会用到
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
hash算法
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
//此函数确保在每个位位置上仅相差常量倍数的hashCodes具有有限数量的碰撞(默认负载因子大约为8)。
//这里进行了很多亦或(^)以及位运算,目的保证hash值得散列性,使之后计算下标位置时保证所有下标
//都能取到
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
计算下标索引
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
/**
* length:数组长度(2的次方数) h:key的hash值
* & 运算 会将值变为二进制进行运算 如: 8 0000 1000
* &
* h 0000 0010
* (1 & 1 = 1; 1 & 0 = 0;0 & 0 = 0)
* 0000 0000
* 这里数组长度减一的原因:
* 假如数组的长度为 8,减一后为 7 0000 1111
* &
* h 0000 0010
* 因为&运算,那么决定结果的就是 减一后的后四位(1111),那么最大值也就是 7,所以就能取到 0-7 所
* 有的下标值
*/
return h & (length-1);
}
新增节点(头插法)
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断是否需要扩容,从这里也可以看出扩容后为原来的2倍
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//新增节点
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//得到原来数组这个下标下的链表,也是链表的头部
Entry<K,V> e = table[bucketIndex];
//增加节点 添加到数组bucketIndex的这个位置,然后它的下一个元素为原来链表的头部,即实现了
//头插法
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
resize()方法扩容
void resize(int newCapacity) {
Entry[] oldTable = table;//旧数组
int oldCapacity = oldTable.length;//旧数组的长度
//MAXIMUM_CAPACITY作为一个2的幂方中最大值
if (oldCapacity == MAXIMUM_CAPACITY) {//判断是否等于最大长度 1 << 30
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//移动到新的数组
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
jdk8
jdk8的put方法
public V put(K key, V value) {
//计算key的hash值
return putVal(hash(key), key, value, false, true);
}
jdk8的hash算法
static final int hash(Object key) {
/*
*没有jdk7那么复杂,要进行树化,所以对散列性没有那么高的要求
*/
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//初始化,扩容都在resize()方法里面
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算下标(与jdk7相同),看这个位置的值是否有值(没有值)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//计算下标,看这个位置的值是否有值(有值)
else {
Node<K,V> e; K k;
//判断是否有相同的key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断节点类型是否是树节点类型(看链表是否树化),如果是直接在再红黑树中添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//即没有相同的key,也没有树化
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//如果链表长度大于等于TREEIFY_THRESHOLD = 8 - 1,就会进行红黑树化
treeifyBin(tab, hash);
break;
}
//有相同的kay
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//返回被替换的值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
树化条件
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//链表长度大于 8-1 后 在这里判断数组长度是否小于 MIN_TREEIFY_CAPACITY 也就是64
//如果数组长度小于64则会扩容,不会树化
//所以树化条件的链表长度大于 7 并且 数组长度大于 64
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
jdk8的resize()方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//扩容长度为原来的两倍 位运算
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 = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//扩容后移动元素
if (oldTab != null) {
//遍历老数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
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;
//这里会进行&运算,当等于0是会将这个节点分到低位
//不为0分到高位
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;
}
jdk7与jdk8的区别
jdk8会将链表转换为红黑树(性能折中)
- 当链表的长度大于8,并且数组长度大于64就会进行树化,当删除节点的时候,如果树的节点小于6就会变为链表
jdk8hash算法简化
- 因为决定使用红黑树,散列性要求就没有那么高
resize()方法实现
- jdk7的resize进行初始化,jdk8是进行扩容并且初始化,jdk8resiae方法进行扩容的时候,他会对节点进行分组,分为低位和高位,如果是低位就放原来的位置,高位则为原来的位置加上原来数组的长度
插入顺序
- jdk7是头插法,jdk8因为要遍历链表进行树化,就使用尾插法