此处主要探究jdk7,jdk8对HashMap结构有较大改动
HashMap底层主要用数组+链表实现,元素以链表形式存放到数组中,每次加一个元素,先利用hash算法算出在数组中的位置,若是此处已有数据代表发生了哈希碰撞,那么就将元素放到此处的后面
主要变量
/**
* 数组默认初始化容量,每次扩容后一定是2的倍数
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 数组扩容的最大值
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认装载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 存放数据的数组
*/
transient Entry<K,V>[] table;
/**
*存放元素的数量
*/
transient int size;
/**
* 当HashMap的size大于threshold时会执行resize操作。 threshold=capacity*loadFactor
*/
int threshold;
/**
* The load factor for the hash table.
*
* loadFactor译为装载因子。装载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity, 而不是占用桶的数量去除以capacity。
*/
final float loadFactor;
/**
* 记录HashMap被修改的次数 以尽行快速失败
*/
transient int modCount;
/**
* 临界值
*/
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
HashMap 一共4个构造方法
//指定容量与装载因子
public HashMap(int initialCapacity, float loadFactor) {
//容量不能小于0 也不能大于最大值
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
//若容量小于默认容量则直接扩充2倍
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}
HashMap主要操作
put
public V put(K key, V value) {
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;
}
- 第一步 先计算key值的hash值,这里没有直接用Object的hash函数,而是进行了多次哈希
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
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);
}
- 第二步 根据第一步中算出的哈希值与数组长度计算柱数组下标
static int indexFor(int h, int length) {
return h & (length-1);
}
- 第三步 由第二步计算出的下标得到数组中的链表,若链表不为空则表明此刻发生了哈希碰撞,接着先遍历这个链表,
当Entry的hash值与计算的hash值相当并且 链表的某个元素的key值与当前key值相等即为同一个对象或者内容相同时表明put了一个相同的value值,此时将旧值覆盖后,直接返回旧值,结束;若是不符合条件则进入addEntry中开始添加Entry
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倍,并且还要重新计算每个元素的位置,进行迁移
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];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
//迁移数据
transfer(newTable, rehash);
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);
//该节点的next指向新数组的头结点
e.next = newTable[i];
//新数组位置链表的头结点变成新节点
newTable[i] = e;
//节点向后转移一个位置
e = next;
}
}
}
扩容迁移数据后 就要开始放置新元素了,新元素被直接放置到链表的头部,即采用了头插法,jdk8改用了尾插法
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
线程不安全原因
HashMap多线程环境下会发生死锁,原因是扩容时调用了transfer方法进行数据迁移,迁移时要把链表拷贝到新数组里面,此时进行的是逆序迁移,
若此时有两个线程,链表 A -> B 即A.next =B, B.next=null ,
线程1运行到逆序迁移前一刻挂起,线程2开始迁移 ,逆序迁移 B ->A, 即正常结果应该是B.next=A, A.next=null,
此时线程A“复活”,开始遍历链表,此刻它未发现链表的变化,遵循规矩将链表逆序,A-B,即正常结果应该是A.next=B, B.next=null,
上面看起来并没有发生死锁,但线程的运行时间的不可控性可能是灾难性的,我们设想一下,线程1运行到B.next=A停止了,而此时线程2开始作死了,A.next=B,此刻两个节点的next都是对方,然后运行完代码e = next; 结果while循环变成了一个死循环,系统gg了
HashMap遍历方法:
1、HashMap提供的迭代器
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
}
该方法将Entry对象以set集合的方式返回, 看到这里有点疑惑,这里好像没有获取Entry数组的操作,HashMap的put方法也没有将数据跟entrySet联系起来,那我们跟踪到EntrySet构造函数中,
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<K,V> e = (Map.Entry<K,V>) o;
Entry<K,V> candidate = getEntry(e.getKey());
return candidate != null && candidate.equals(e);
}
public boolean remove(Object o) {
return removeMapping(o) != null;
}
public int size() {
return size;
}
public void clear() {
HashMap.this.clear();
}
}
我们注意到EntrySet 类是一个内部类,由于没有static修饰符修饰,此时由它new出来的对象是可以直接通过HashMap.class.this访问存放数据的Entry数组的,相当于打开了通向金矿的大门
遍历时需要调用iterator()方法返回一个迭代器,有了迭代器自然后续遍历不用过多解释。我们来研究下这个迭代器是怎么实现的,毕竟map结构不想ArrayList那种线性结构一样,一个个往后推就是了
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
}
这里new了一个EntryIterator 对象,看下他的源码
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
EntryIterator 类里面只定义了一个方法,next()方法也是调用了父类中的nextEntry(),但看起来集成了一个好爸爸HashIterator,来看下HashIterator源码
HashIterator实现了Iterator接口,嗯,看起来迭代器的核心逻辑都由它完成了,分析一下
先看成员变量,
Entry<K,V> next; // next entry to return
int expectedModCount; // For fast-fail
int index; // current slot
Entry<K,V> current;
看起来跟ArrayList的迭代器差不多,存放了数据源Entry,游标index,expectedModCount来进行快速失败
再看构造函数,只有一个
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
第一步初始化了expectedModCount ;
第二步,,初始化index与next,遍历数组找到第一个不为空的元素下标x, 此时index= x+1,next=x;
判断后面是否还有元素的条件是next不为空
public final boolean hasNext() {
return next != null;
}
接着来看迭代器如何后移的
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
//返回元素前先把next在链表上后移一位,若后移后是空的表明这个链表遍历完毕,将元素换到数组中下个位置
if ((next = e.next) == null) {
Entry[] t = table;
//跟构造函数中的循环方式一样找到数组中下一个不为空的位置
while (index < t.length && (next = t[index++]) == null)
;
}
//记录返回的元素
current = e;
return e;
}
既然是迭代器那么要支持同步删除,
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
//由于哈希算法不是线性的,无需像ArrayList那样回拨游标,也不会漏掉元素
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
这个地方比较有意思的是迭代器已经掌控了目标元素但却没有直接在数组与链表中删掉目标,而是根据key值来删除的,来看下removeEntryForKey源码,HashMap对外公开的remove(key)方法内部也是调用的这个方法,也许是为了重用
final Entry<K,V> removeEntryForKey(Object key) {
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
从代码中可以看出删除数据时会将链表的匹配元素从链表中移除,由于都是指针操作,迭代器保存的next仍然是下一个元素的引用,不用像ArrayList那样回拨,倒是跟LInkedList相像
由此,HashMap的迭代器完满实现遍历目标(包括同步删除)。