HashMap应该是java中用的很频繁的数据结构,最近几天研究了下HashMap源码,了解的不算很多,讲讲我的体会
我看的是JDK1.8的HashMap,1.7跟1.8还是有些区别的
先看看HashMap源码中的关键变量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认容量16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负载因子
transient Node<K,V>[] table; //存储元素的Node数组,hashMap的元素就是放在这
final float loadFactor; //负载因子,参与 threshold的计算
int threshold; // 当size> threshold时,会进行扩容, threshold= capacity * loadFactor
transient int size; //元素个数
HashMap的容量是2的N次方,每次扩容新的table长度是 老的 一倍,
在size达到threshold的时候会进行扩容
HashMap是根据key通过 hash算法得到一个整数,然后再放入数组中(不是简单的直接放入哦)
看看保存元素的Node的结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //根据key计算出来的 hash值
final K key;
V value;
//next是用来在hash冲突的时候,利用链表的形式去保存冲突的值,第一个冲突的保存在next,如果还有
//冲突,就保存在next元素的next里面,,以此类推,当然,jdk1.8调整了保存方式,冲突过多的时候,会保存在 红黑树里面
Node<K,V> next;
}
下面是HashMap一个构造函数:
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);
}
这里有个tableSizeFor方法
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;
}
这个方法用于求出大于等于 给定参数cap的 最小2次方,有点绕口,
比如你传入capacity 2, 大于等于capacity的最小2次方就是2
比如你传入capacity 3, 大于等于capacity的最小2次方就是4
可是问题来了,我传入的capacity如果是7,那这时候的threshold就是8了啊,岂不是说表的容量 还小于 扩容的阈值threshold ?
当然不是,这个时候其实HashMap并没有具体 实例化table, table是 在第一次put的时候创建的,
put发现table为null,就会调用resize方法扩容,resize里面重新计算了threshold和capacity
HashMap的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;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
假设new HashMap<>(7),
然后构造方法将threshold经过计算后得到8,可是在resize方法做了啥事呢,
先将newCap=oldThr,也就是newCap为8了,
然后计算出新的threshold= newCap * loadfactory,计算结果为6了,所以,threshold不是为8,还是变成了 capacity * loadFactor
接下来看关键的 hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个方法跟jdkk1.7的不同了,不过效率可能更高了
先计算出key的hashCode然后 将计算出的 hashCode值无符号 右位移16位,然后 异或运算, 这个方法我也不大理解
这里有个问题了,hash方法返回的Int值可能很大,比如是3456吧,那我不可能建个 长度是3456的吧,比如hashMap默认容量16的时候,
要将这个数据放入 数组中怎么办?取余是其中一种方式
具体的存放的位置是根据这个表达式:
i = (n - 1) & hash //n就是数组的length
这里没有直接使用 % 是为了提高效率
不过这个有可能会造成hash碰撞,所以对于碰撞的比较严重的,HashMap用红黑树来解决的,
看看put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果根据hash定位到 table中的 index位置上没有元素
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); //插入元素到 table数组中
else { //处理hash冲突的情况
Node<K,V> e; K k;
//如果之前 i位置的元素跟 要插入的元素,hash值,key都相等,将P赋值给e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果i位置的元素是 数节点,利用红黑树保存
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//冲突的元素都是放在 此位置的元素的next中,如果冲突多了,依次是next的next,,,,
//当hash冲突 达到TREEIFY_THRESHOLD - 1个,
//就用红黑树保存元素,若没有达到这个上限,就用链表保存
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
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//用于处理当put进来的key-value在表中已经存在,是否需要用新的value替换老的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
//如果配置了onlyIfAbsent为false或者 之前位置的元素value为null,就用新的value替换
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//若达到上限,调用resize扩展
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap的get方法:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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 &&
(first = tab[(n - 1) & hash]) != null) { //hash值定位到table中的index位置,若此位置有值
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//若不满足上面的条件(hash值相等,key相等),从链表中或者红黑树中查找
if ((e = first.next) != null) {
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);
}
}
return null;
}
这里没有讲关于具体怎么用红黑树解决冲突,其实HashMap中有些变量和 方法都是跟红黑树相关的,
也就是用来解决冲突的,这里只是简单介绍了下大致的,也不知道写的有没有错,只是把我看的源码的理解介绍了下