jdk7中HashMap的数据结构是数组+链表来实现的,底层维护着一个数组,每个数组项是一个Entry;
transient Entry[] table;
static class Entry implements Map.Entry {
final K key;
V value;
Entry next;
int hash;
数组中的Entry的位置是通过key的HashCode进行计算的:
final int hash(Object k) {
int h = 0;
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
之后通过IndexFor进行计算出来:
static int indexFor(int h, int length) {
return h & (length-1);
}
其实就是key的hash值对length取模;如果两个key的hash值一样,那么就产生冲突,或者说碰撞;HashMap解决碰撞的方法是通过链表。将新的值存放在entry[i]数组中,原来的值作为entry[i]的next;所以新插入的值放在链表的头节点中,旧值存放在链表的尾部;
当size大于capacity*装载因子的时候就发生扩容;
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);
}
每次扩容,容量变为原来的2倍;
在jdk7中,如果上百个元素存在一个链表上,那么如果要查找其中的一个元素的时候,查找时间为o(n),性能比较低的;在Jdk8中解决了这个问题,通过引入红黑树,在最差的情况下,时间复杂度为o(logN);
JDK8中HashMap的实现为数组+链表/红黑树;默认当链表的长度大于8之后,数据结构就变成红黑树;如图所示
jdk8中的定义如下
transient Node[] table;
节点名字不再是entry,而是node,就是因为和红黑树的实现TreeNode关联;put方法如下所示
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab;
Node p;
int n, i;
//如果当前map中无数据,执行resize方法。并且返回n
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果要插入的键值对要存放的这个位置刚好没有元素,那么把他封装成Node对象,放在这个位置上就完事了
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//否则的话,说明这上面有元素
else {
Node e; K k;
//如果这个元素的key与要插入的一样,那么就替换一下,也完事。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//1.如果当前节点是TreeNode类型的数据,执行putTreeVal方法
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//还是遍历这条链子上的数据,跟jdk7没什么区别
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//2.完成了操作后多做了一件事情,判断,并且可能执行treeifyBin方法
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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) //true || --
e.value = value;
//3.
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判断阈值,决定是否扩容
if (++size > threshold)
resize();
//4.
afterNodeInsertion(evict);
return null;
}
红黑树是二叉排序树,但在每个结点增加了一个存储位来标识颜色,red或者black;满足二叉排序树的所有特性:
1.若任意结点的左子树不为空,则左子树的所有结点的值小于根结点;
2.若任意结点的右子树不为空,则右子树的所有结点的值大于根结点;
3.左右子树也是二叉排序树;
4.没有键值相等的节点
一个有n个节点的二叉排序树的高度为lgn,所以查找时间复杂度为O(lgn);
HashMap是线程不安全的,是因为在resize的时候会产生死循环;默认的size是16,当超过这个size之后,会扩容,这样一来,整个Hash表中的元素都需要被重新计算一遍,实现代码如下所示:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
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);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while(null != e) {
Entry 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;
}
}
}
transfer简单解释下:每次取出旧数组的头结点的next,之后重新计算头结点在新的Hash中的位置,然后将头节点的next指向新的table[i],然后把table[i]设置成当前的头结点,那么就完成了头结点的转移;
所以转移之后的顺序会跟之前的顺序相反,比如原来是1->2->3,转移之后为
第一次 1
第二次 2 1
第三次 3 2 1,
顺序正好反过来了。HashMap的死锁问题就出在这个transfer函数上;
用个图进行简单的说明:
线程一执行到这里被挂起;线程二执行了
Entry next = e.next;
完成了3和7元素的转移之后,线程一就接着执行,这时候,线程一种3.next是7,线程二中7.next是3;就形成了环形的链表;
另外:在迭代的过程中,如果有线程修改了map,会抛出ConcurrentModificationException错误,就是所谓的fail-fast策略;
HashMap是无序的,先通过一个例子验证下
HashMap< String, String> map = new HashMap<>();
map.put("5", "@sohu.com");
map.put("2","@163.com");
map.put("3", "@sina.com");
for (String key : map.keySet()) {
System.out.println("key= "+key+" and value= "+map.get(key));
}
程序的返回结果:
key= 2 and value= @163.com
key= 3 and value= @sina.com
key= 5 and value= @sohu.com
可以发现,放进去的顺序和遍历的时候的顺序是不一致的;那么为什么会是这样的那,我们需要先了解下HashMap的遍历方式;
先说map.keySet的方式,keySet的代码如下所示(Jdk8的情况)
public Set keySet() {
Set ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
可以看到是new了一个keySet对象,这个KeySet是一个内部类,代码如下所示
final class KeySet extends AbstractSet {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer super K> action) {
Node[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
这个内部类中的iterator方法实现了迭代器接口,接口定义代码如下
这里有一点需要说明下,增强for循环,那么什么是增强for循环那,上面遍历keyset的for就是一个增强for循环:
for(String key:keys)类似这样的,底层在迭代的时候还是使用的迭代器,当然这个调用是jvm来完成的,我们只需要知道调用for的时候就会调用iterator方法,我们再来接着看iteraotr方法返回一个new keyIterator对象
然后KeyIteraotr类的定义如下所示;
这里的nextNode方法如下所示
这里看到的就是最终的核心了,真正的遍历过程;之前我们说HashMap的数据结构是数组+链表,这里遍历的时候从第一个元素开始,然后遍历链表,当链表遍历结束的时候,遍历下面一个数组和对应的链表数据;
现在再来看看HashMap为啥是无序的,因为存放的时候是根据key的Hash值来存放的,先放进去的计算hash之后可能存放在数组的后面了,所以遍历之后就在后面的再遍历出来;