hashmap详细剖析

确定hash桶数组的索引位置

我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现(方法一+方法二):

方法一:
static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法二:
static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 

这里的Hash算法本质上就是三步:取key的hashCode值、高位运算、位运算。

第一步:显而易见,直接取就是了。

第二步:在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

第三步:不是 直接的位运算,由于length为2的幂次,所以可以使用“与”运算 h & (length-1)代替位运算。这样速度更快。

哈希算法 及 扰动函数

jdk1.7

final int hash(Object k) {
	int h = hashSeed;
	if (0 != h && k instanceof String) {
		return sun.misc.Hashing.stringHash32((String) k);
	}

	h ^= k.hashCode();

	// This function ensures that hashCodes that differ only by
	// constant multiples at each bit position have a bounded
	// number of collisions (approximately 8 at default load factor).
    // 这里是扰动函数
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}

jdk 1.8

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看出扰动函数就是将key的哈希值h与右移16位后的h进行异或运算。

用一句话来概括扰动函数的作用就是:将h的hashCode右移16位并与自身相异或 相当于 使自己的高16位和低16位 相异或,得到的值既包含了自己高位的特性又包含了自己低位的特性,从而增加了之后得到的下标的不确定性,降低了碰撞的概率。

put

数组的第0个位置 固定放置key为null的value

  1. 通过扰动函数计算key的哈希值
  2. 如果哈希表为空,初始化
  3. 如果哈希表不为空,计算下标
    1. 如果table[i]为空,创建新节点存入

    2. 如果table[i]不为空,根据HashCode和key值在链表/红黑树中寻找目标位置并更新其value值
      a. 如果没有发生碰撞

      1. jdk1.7 将新节点插在当前链表头部的前面,然后再下移(头插法)
      2. jdk1.8
        1. 如果是红黑树,调用红黑树的插入
        2. 如果是链表,遍历到链表尾部并插入新结点(尾插法),遍历的同时进行计数,当插入新节点后的计数达到阈值,就把链表转化为红黑树

      b. 如果发生碰撞,通过比较key的地址或者key的值(equals)遍历链表/红黑树获取旧值,覆盖后返回旧值
      c. 如果HashMap容量达到阈值initialCapacity*loadFactor,则进行扩容

jdk1.8
新增链表转红黑树的阈值,因此在插入的时候必须知道链表的长度,如果长度超出这个阈值就将其转化为红黑树,因此在插入式必须遍历链表得到链表长度,于是在jdk1.8里插入结点时选择直接插在链表尾部,反正都要遍历一次,这样还保证了在扩容的时候对元素进行transfer时链表的顺序不会像1.7一样倒转,也就不会出现死循环链表。

resize

扩容条件

jdk1.7 (size >= threshold) && (null != table[bucketIndex])
存放的键值对数超出阈值,并且新增结点要插入的地方不为空

jdk1.8 ++size > threshold
只要存放键值对数超出阈值就扩容

扩容方式

默认扩容16,原数组长度,构建一个原数组长度两倍的新数组,并调用transfer将原数组的数组通过重新计算哈希值得到下标再转移到新增数组。

jdk1.7调用indexFor方法重新计算下标,并采用跟插入结点时一致的方式(头插法)挨个移动结点

jdk1.8则是根据规律将原链表拆分为两组,分别记录两个头结点,移动时直接移动头结点

我们会发现 HashMap扩容后,原来的元素要么在原位置,要么在原位置+原数组长度 那个位置上
举个栗子来说:

原来的HashMap长度为4,table[2]上存放了A。现在要进行扩容,先创建了一个长度为8的新数组,现在要进行transfer,那么这个A要放到哪里呢?

我们先来根据他原本所在的位置2来倒推,我们知道index = HashCode(Key) & (Length - 1),那么就有
hashmap详细剖析_第1张图片
Hash(key) 可能为010,可能为110。

我们用新的长度(8 = (111)2)和这两个数分别再去通过Hash算法来计算新的下标会发现

010 & 111 = 010 在原位置

110 & 111 = 110 在原位置+4,当前下标+旧数组长度

在转移链表时,结点的转移和插入是一致的,jdk1.7将采用头插法(转移完后链表反转),jdk1.8在分解完链表后直接移动头结点

为什么HashMap是线程不安全的?

主要体现

jdk1.7中,当多线程操作同一map时,在扩容的时候会因链表反转发生循环链表或丢失数据的情况

jdk1.8中,当多线程操作同一map时,会发生数据覆盖的情况

在put的时候,由于put的动作不是原子性的,线程A在计算好链表位置后,挂起,线程B正常执行put操作,之后线程A恢复,会直接替换掉线程b put的值 所以依然不是线程安全的

为什么会遇到ConcurrentModificationException异常

首先我们要知道,HashMap中有个属性modCount,用于记录当前map的修改次数,在对map进行put、remove、clear等操作时都会增加modCount。

他的作用体现在对map进行遍历的时候,我们知道HashMap不是线程安全的,当对其进行遍历的时候,会先把modCount赋给迭代器内部的expectedModCount属性。当我们对map进行迭代时,他会时时刻刻比较expectedModCount和modCount是否相等,如果不相等,则说明有其他的线程对同一map进行了修改操作,于是迭代器抛出ConcurrentModificationException异常。

这就是Fail-Fast 机制。

在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统。快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。

源码解读

jdk1.7

构造函数

/** 参数为空的构造方法 */
public HashMap() {
	this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/** 自定义初始化容量的构造方法 */
public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
 * 构造一个空的HashMap
 *
 * @param  initialCapacity 初始容量
 * @param  loadFactor      负载因子
 * @throws IllegalArgumentException 初始容量/负载因子非法则抛出异常
 */
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); // 初始负载因子小于等于0或NaN,抛异常

	this.loadFactor = loadFactor; // 将负载因子赋值给全局变量
	threshold = initialCapacity; // 将初始化容量赋值给阈值
	init(); 
}

关于负载因子:负载因子表示一个散列空间的使用程度,负载因子越大则散列表的装填程度越高,填入表中的元素越多,越容易发生冲突;负载因子越小则散列表的装填程度越低,填入表中的元素越少,越不易发生冲突,但是容易造成空间浪费。

关于最后的init(),在源码里只是个空方法,似乎是为了子类的反序列化而准备的,在hashmap构造之后、数组创建之前调用,jdk1.8以后就没有了

put

public V put(K key, V value) {
	// 如果数组为空,初始化一个数组,长度为大于阈值的最小的2的幂次数
	if (table == EMPTY_TABLE) {
		inflateTable(threshold);
	}
	// 键为空,调用putForNullKey方法,因为空键固定放在0号位
	if (key == null)
		return putForNullKey(value);
	int hash = hash(key); // 计算哈希值
	int i = indexFor(hash, table.length); // 计算下标i
	// 键不为空
	// 遍历i号位的链表
	for (Entry<K,V> e = table[i]; e != null; e = e.next) {
		Object k;
		// 找到键为key的结点,判断条件:
		if (e.hash == hash // 1.key的哈希值相同
				&& ((k = e.key) == key || key.equals(k))) { // 2.key的值相同
			V oldValue = e.value;
			e.value = value; // 覆写
			e.recordAccess(this);
			return oldValue;
		}
	}
	// 不存在,插入
	modCount++;
	addEntry(hash, key, value, i);
	return null;
}

addEntry

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); // 重新计算下标
	}
	// 加入新结点
	createEntry(hash, key, value, bucketIndex);
}

get

public V get(Object key) {
	// key为空,调用getForNullKey方法,因为空键固定放在0号位
	if (key == null)
		return getForNullKey();
	// 得到key所在的结点
	Entry<K,V> entry = getEntry(key);
	// 结点为空直接返回null,反之获取对应val
	return null == entry ? null : entry.getValue();
}

getEntry / containsKey

public boolean containsKey(Object key) {
	return getEntry(key) != null;
}
final Entry<K,V> getEntry(Object key) {
	// 空map返回null
	if (size == 0) {
		return null;
	}

	int hash = (key == null) ? 0 : hash(key); // 得到key的哈希值
	// 计算key对应下标i,并遍历i号位的链表,找到key值对应的结点
	for (Entry<K,V> e = table[indexFor(hash, table.length)];
	     e != null;
	     e = e.next) {
		Object k;
        // 找到键为key的结点,判断条件:
		if (e.hash == hash && // 1.key的哈希值相同
				((k = e.key) == key || (key != null && key.equals(k)))) // 2.key的值相同
			return e;
	}
	// 找不到就返回null
	return null;
}

resize

/**
 * Rehashes the contents of this map into a new array with a
 * larger capacity.  This method is called automatically when the
 * number of keys in this map reaches its threshold.
 * 把当前map中的元素翻新到新的更大的新map中。当前map中的键值对达到阈值就会触发扩容方法。
 *
 * If current capacity is MAXIMUM_CAPACITY, this method does not
 * resize the map, but sets threshold to Integer.MAX_VALUE.
 * This has the effect of preventing future calls.
 * 如果当前容量已经达到最大容量,map将不会进行扩容,而是将阈值提到Integer.MAX_VALUE,从而达到以后不会再调用扩容方法的效果
 *
 * @param newCapacity the new capacity, MUST be a power of two;
 *        must be greater than current capacity unless current
 *        capacity is MAXIMUM_CAPACITY (in which case value
 *        is irrelevant).
 *        新的容量,必须是2的幂次,必须比当前容量大,除非当前容量已经达到最大容量
 */
void resize(int newCapacity) {
	Entry[] oldTable = table; // 当前数组
	int oldCapacity = oldTable.length; // 当前容量
	// 已经达到最大容量的情况下,将阈值升到Integer.MAX_VALUE,能够有效阻止日后调用扩容
	if (oldCapacity == MAXIMUM_CAPACITY) {
		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); // 重新计算阈值
}

transfer

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; // 指针记录e.next
			// 如果需要,重新计算哈希值
			if (rehash) {
				e.hash = null == e.key ? 0 : hash(e.key);
			}
			int i = indexFor(e.hash, newCapacity); // 重新计算下标
			// 头插法
			e.next = newTable[i]; // 将e插入到table[i]头部之前
			newTable[i] = e; // 将链表下移
			e = next; // 指向下一个要移动的结点
		}
	}
}

jdk1.8

多出的两个阈值

static final int TREEIFY_THRESHOLD = 8; // 链表树化阈值
static final int UNTREEIFY_THRESHOLD = 6; // 树链表化阈值

结点的引入

Node

构造函数

和1.7几乎相同,不做注释,两点不同的是

  • 移除了钩子函数init()
  • 1.7是直接将自定义容量赋给threshold,在inflateTable时才计算数组长度,而1.8是直接计算数组长度赋给threshold
/**
 * Constructs an empty HashMap with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
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;
	this.threshold = tableSizeFor(initialCapacity);
}

put

/**
 * Associates the specified value with the specified key in this map.
 * If the map previously contained a mapping for the key, the old
 * value is replaced.
 * 以给定的键值对寻找是否有相同的键值,如果有,覆写。
 *
 * @param key key with which the specified value is to be associated 键
 * @param value value to be associated with the specified key 值
 *
 * @return the previous value associated with key, or
 *         null if there was no mapping for key.
 *         (A null return can also indicate that the map
 *         previously associated null with key.)
 *         有相同的key,则返回旧值;没有就返回null
 *         返回值同时可以作map中是否包含该key值的判断
 */
public V put(K key, V value) {
	// 计算哈希值开始找
	// onlyIfAbsent: false <=> 允许覆写
	// evict: true <=> 插入结点后是否允许操作(看了下应该是给子类LinkedHashMap用的
	return putVal(hash(key), key, value, false, true);
}

putVal

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
	Node<K,V>[] tab; Node<K,V> p; int n, i;
	// 数组为空 或 数组长度为0,构造table数组
	if ((tab = table) == null || (n = tab.length) == 0)
		n = (tab = resize()).length;
	// 计算下标i,数组i号位的结点p为空,直接插入新结点
	if ((p = tab[i = (n - 1) & hash]) == null)
		tab[i] = newNode(hash, key, value, null);
	else {
		// i号位不为空,遍历
		Node<K,V> e; K k;
		// 头结点是要找的结点(key的哈希值或key的值相同),e指向p
		if (p.hash == hash && // key的哈希值相同
				((k = p.key) == key || (key != null && key.equals(k)))) // 或 key值相同
			e = p;
		// p是树节点,调用红黑树的插入方法
		else if (p instanceof TreeNode)
			e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		// p是链表结点
		else {
			// 遍历链表,同时计数binCount,用于判断是否该树化链表
			for (int binCount = 0; ; ++binCount) {
				// 遍历到尾结点
				if ((e = p.next) == null) {
					// 插入结点
					p.next = newNode(hash, key, value, null);
					// 判断是否超出阈值,是则树化
                    // -1是因为除去了数组内的头结点
					if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
						treeifyBin(tab, hash);
					break;
				}
				// 找到目标结点,赋值给e
				if (e.hash == hash &&
						((k = e.key) == key || (key != null && key.equals(k))))
					break;
				p = e;
			}
		}
		// 找到目标结点,覆写(如果允许的话 或 旧值为null)并返回旧值
		if (e != null) { // existing mapping for key
			V oldValue = e.value;
			if (!onlyIfAbsent || oldValue == null)
				e.value = value;
			afterNodeAccess(e);
			return oldValue;
		}
	}
	++modCount; // 操作次数++
	// 键值对数量增加并判断是否该扩容(与1.7相比判断条件少了
	if (++size > threshold)
		resize();
	afterNodeInsertion(evict);
	return null;
}

注意两点

  • 先插入结点,再判断是否需要树化
  • 关于树化的阈值判断,模拟一下会知道,其实是当链表长度超出8才会转红黑树
    hashmap详细剖析_第2张图片

get

public V get(Object key) {
	Node<K,V> e;
    // 计算哈希值寻找结点,有就返回val,没有就返回null
	return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode / containsKey

public boolean containsKey(Object key) {
   return getNode(hash(key), key) != null;
}

os:1.8真的很喜欢把赋值语句放在条件判断里…

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 // 数组长度大于0
			&& (first = tab[(n - 1) & hash]) != null) { // 计算下标i,i号位不为空
		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);
			// 如果不是,那就是链表结构,那就遍历寻找
			do {
				// 找到了就返回
				if (e.hash == hash &&
						((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	// 找不到就返回null
	return null;
}

resize

建议先把前面的扩容方式计算新下标的思路看一下

大概意思就是根据高位是0还是1来确定最后存的下标位置,0就保持原位,1就向后移一个旧长度的位置,这个结果和重新计算下标的结果是一致的
hashmap详细剖析_第3张图片

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 * 初始化或翻倍数组。如果为空,初始化数组。
 *
 * @return the table
 */
final Node<K,V>[] resize() {
	Node<K,V>[] oldTab = table; // 暂存旧table数组
	int oldCap = (oldTab == null) ? 0 : oldTab.length; // 暂存旧容量,如果是初始化调用扩容方法,旧容量为空,赋0
	int oldThr = threshold; // 暂存旧阈值
	int newCap, newThr = 0;
	// 旧容量不为0
	if (oldCap > 0) {
		// 旧容量达到最大容量,不再扩容,将阈值提高至Integer.MAX_VALUE,有效阻止以后再度出现达到阈值的情况
		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
	}
	// 旧容量为0,即table数组还未创建
	// 为带参构造方法使用HashMap(int initialCapacity)及HashMap(int initialCapacity, float loadFactor)
	// 因为初始化容量一开始是被存放在threshold中的
	else if (oldThr > 0) // initial capacity was placed in threshold
		newCap = oldThr;
	// 旧容量为0,即table数组还未创建
	// 为无参构造方法使用HashMap(),使用默认值
	else {               // zero initial threshold signifies using defaults
		newCap = DEFAULT_INITIAL_CAPACITY;
		newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 初始化阈值
	}
    // 如果新阈值为0
    // 一种情况是使用了带参构造方法(else if (oldThr > 0))
    // 另一种是旧容量未达到默认容量大小或翻倍后超出最大容量(else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    //						oldCap >= 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; // 赋给全局变量
	// 这里开始实施转移,transfer
	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) // 树节点,调用树的split方法
					((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
				// 链表结点,保留之前的顺序(尾插法
				else { // preserve order
					// 第二层,链表遍历
					// (我们以该链表原本位置在010(2),旧容量为100(4)举例子,分析已知原本的哈希值可能为010,可能为110

					// head记录头结点,tail记录尾结点
					Node<K,V> loHead = null, loTail = null; // lo表示low表示0,记录要转移的结点
					Node<K,V> hiHead = null, hiTail = null; // hi表示high表示1,记录保留在原位置的结点
					Node<K,V> next;
					do {
						next = e.next;
						if ((e.hash & oldCap) == 0) { // 高位为0(010
							if (loTail == null)
								loHead = e;
							else
								loTail.next = e;
							loTail = e;
						}
						else { // 高位为1(110
							if (hiTail == null)
								hiHead = e;
							else
								hiTail.next = e;
							hiTail = e;
						}
					} while ((e = next) != null);
					// 高位为0的链表,保持原位置
					if (loTail != null) {
						loTail.next = null;
						newTab[j] = loHead;
					}
					// 高位为1的链表,转移阵地
					if (hiTail != null) {
						hiTail.next = null;
						newTab[j + oldCap] = hiHead;
					}
				}
			}
		}
	}
	// 返回新数组
	return newTab;
}

jdk1.7到1.8的主要变化

数据结构改变
jdk1.7,数组+链表
jdk1.8,数组+链表+红黑树

链表节点插入
jdk1.7,头插法,再下移
jdk1.8,尾插法

扩容
扩容条件减少
jdk1.7,达到阈值并且新增结点发生碰撞才扩容,也就是说,如果达到阈值后,新增节点插入位置为空,则先不扩容
jdk1.8,只要达到阈值就扩容

扩容方式(针对链表而言)
jdk1.7,采取和插入时一致的方式,头插法,再下移
jdk1.8,拆分链表后直接移动头结点

扩容下标计算
jdk1.7,重新计算
jdk1.8,根据高位二进制决定,为0则下标不变,为1则往后移动一个旧数组长度的距离

简化了哈希算法
jdk1.7,使用了哈希种子,使用了四次扰动函数
jdk1.8,使用一次扰动函数

你可能感兴趣的:(hashmap,数据结构,java)