JDK1.7版本HashMap源码分析
文章目录
前言
注:观看本文章前要对hash表有一定的了解
HashMap一个Java中一个非线程安全、用于存储键值对的容器,JDK1.7版本和JDK1.8版本在实现方式上有着一定的不同,我们先简单介绍下JDK1.7版本的HashMap的原理:通过key计算出一个hash值,在通过hash值计算出一个下标index。将键、值、hash值,下一个节点的引用next封装到一个Entry对象中,将该Entry对象放入到数组下标为index的位置。不同的key计算出的下标可能会相同,这就是所谓的hash冲突,回了解决这种冲突,HashMap用了单项链表,如果发生Hash冲突,会以头插法的方式将新节点加入到对应的的链表中。上述原理只是简单说明。详情内容请大家接着往下看
数据结构
存储结构
JDK.1.7版本的HashMap可以理解成哈希表,数组,单项链表的集合,如下图所示,每一个方格就代表一个节点或元素。
Entry对象
Entry类是HashMap中的的一个静态内部类,Entry对象就代表存储的一个节点,包含了一对键值,还有key的hash值以及下一个节点的引用
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;//键
V value;//值
Entry<K,V> next;//下一个节点的引用
int hash;//键得hash值
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//键和值都相等返回true
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
//键和值得hashcode异或
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
//该方法在节点的值被更改时调用
void recordAccess(HashMap<K,V> m) {
}
/**
* 在节点被删除时调用
*/
void recordRemoval(HashMap<K,V> m) {
}
}
主要属性
//hashMap默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//HashMap的最大容量为1<<30,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
*负载因子,是用来计算是否要进行扩容操作的阀值,
*比如HashMap的容量为16,阀值 = 16 * 0.75 = 12;
*也就是说当HashMap被使用12个元素空间时,就要进行扩容,以保证HashMap的效率
*有的人估计会纠结为什么是0.75,我在这里作一个简单的解释
*假设负载因子是1,HashMap的容量是16,那么随着HashMap中元素的增加,哈希冲突会越来越严重
*Jdk1.8中的解决冲突的数据结构是红黑树,大量的哈希冲突会导致红黑树高度会越来越高。大大减小了查询速率
*假设负载因子是0.5,也就是当HashMap的容量用到一半的时候,就进行的扩容,虽然这样可以减少红黑树的高度,提高查询效率
*但是以前需要1M的存储空间,现在就需要2M。换句话说就是,我们虽然获得了时间,但是我们牺牲了空间
*所以0.75是编辑HashMap的那些高手们中和时间与空间得到的一个比较合适的值
*
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空表
static final Entry<?,?>[] EMPTY_TABLE = {};
*/
//HashMap用于储存键值对的表
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//表中元素的数量
transient int size;
//进行扩容判断的阈值
int threshold;
//负载因子
final float loadFactor;
//每次对HashMap的操作的会使modCount++;可以用来检查线程安全
transient int modCount;
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
构造方法
//双参构造,传入初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
//容量小于零,报异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//initialCapacity 不能超过最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
/*
负载因子不可能小于零,也不能是NaN,读者可以自己输出一下System.out.println(0.0f/0.0f);
就可以知道NaN是什么了,实际上就是如果loadFactor是通过除法计算得来的,当分母为零时,计算的结果就是NaN
NaN的全称为Not a Number,显然是不能作为负载因子的*/
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//给成员赋值
this.loadFactor = loadFactor;
//扩容阈值初始为初始容量
threshold = initialCapacity;
init();
}
//单参构造,指定容量initialCapacity,使用默认负载因子0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用默认容量 16, 使用默认负载因子0.75f
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//将一个Map容器中的节点赋值到当前table数组中
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
核心方法
roundUpToPowerOf2方法
该方法中使用到了 highestOneBit方法,关于这个方法的解释请看到我的另一博文
//table数组必须是2的幂次方,详情请看indexFor()方法,h & (length-1);只有当length的长度为2的幂次方时
//我们才能得到一个0~length-1的下标,比如length = 8,length - 1 = 7,7(D) = 111(B),通过与运算就能得出一个0~7的数字
//刚好满足数组的下标,这个方法就是将一个参数变为接近2的幂次方的数作为数组的容量
//其中 highestOneBit()方法请看我的另一篇博文
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
//highestOneBit方法是得到比参数小并且最接近参数的2的幂次方
//(number - 1) << 1 相当于将参数放大,使我们能得到一个大于number,并且最接近number的2的幂次方
//比如number = 15 Integer.highestOneBit((15 - 1) << 1) = 16
// number = 16 Integer.highestOneBit((16 - 1) << 1) = 16
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
inflateTable方法
//并不是想你想toSize设定多大就多大
//当一个HashMap对象被创建时,table数组是一个容量为0的空数组
private void inflateTable(int toSize) {
//得到一个大于toSize的2的幂次方,因为table数组的容量必须是2的幂次方
int capacity = roundUpToPowerOf2(toSize);
//得到扩容阈值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//给table数组申请空间
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
hash方法及indexFor方法
*/
//将指定参数对象,返回一个用来计算table数组下标的值,具体细节请查看,另一篇博文
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);
}
//其实就是取余运算,算出一个table数据的下标
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
get方法及相关方法
/*
这算是一个常用的核心方法:得到HashMap中对应键值的值
*/
public V get(Object key) {
//如果参数为null,返回键为null对应的值,并返回
if (key == null)
return getForNullKey();
//得到键值对应的entry对象
Entry<K,V> entry = getEntry(key);
//如果键不存在得到的entry对像为null
return null == entry ? null : entry.getValue();
}
//得到table数组中键为null的值,没有对应的值返回null,如果数组的length为0,返回0,默认从下标为0的链表中进行查找
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
//判断是否存在某一个键
public boolean containsKey(Object key) {
//能通过对应键找到对应的entry对象就表示存在
return getEntry(key) != null;
}
/*
*通过指定的键值key找到对应的Entry对象,如果参数为null,就是从数组中下标为0的链表开始找起
*/
final Entry<K,V> getEntry(Object key) {
//table表中没有元素,返回null
if (size == 0) {
return null;
}
//通过hash()方法得到键得哈希值
int hash = (key == null) ? 0 : hash(key);
//哈希表肯定会存在哈希冲突,JDK1.7版本的HashMap解决哈希冲突的方式是单项链表,所以查找时要遍历链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//找到键和参数相等的Entry对象,并返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
put方法及相关方法
public V put(K key, V value) {
//构造方法执行完之后threshold要么等于默认容量16,要么是一个自定义的大小
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
//得到键得hash值
int hash = hash(key);
//通过hash值得到table数组的下标
int i = indexFor(hash, table.length);
//遍历当前下标所对应的链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果链表中已经存在哈希值并别key相等的节点,替换节点中的值,并返回以前的值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//这个方法当一个节点中的值被替换时会被调用
e.recordAccess(this);
return oldValue;
}
}
//对table数组的操作都要使modCount++
modCount++;
//给table[i]对应的链表增加一个Entry成员为hash,key,value的节点
addEntry(hash, key, value, i);
//替换已有节点的值时,返回的是被替换的值,添加新的节点时,返回的是null
return null;
}
/**
* 给键为null的节点赋值,默认是table[0]的链表
*/
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
//这个方法论功能来说和普通方法有些相似,但是它不考虑扩容和初始化table数组的问题
private void putForCreate(K key, V value) {
//key == null =》 hash = 0
int hash = null == key ? 0 : hash(key);
int i = indexFor(hash, table.length);
/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
//创建一个节点加入table中
createEntry(hash, key, value, i);
}
//将一个Map容器中的键值对生成entry对象加入到table数组中
private void putAllForCreate(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putForCreate(e.getKey(), e.getValue());
}
resize方法及相关方法
//扩容,在addEntry方法中使用
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顾名思义,就是将老容器的所有元素搬运到引得数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//数组的容量变了,要重定义阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* 搬运老数组的所有entry对象,将他们复制到newTable中,注意这里给链表增加一个新元素时,是从头插入
*/
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;
//是否从新键得hash值,可通过参数来决定
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;
}
}
}
因为将数组中的节点放入到新数组中使用的是头插法,所以新数组中的链表和老数组中的链表顺序是反的。
remove方法和相关方法
//删除table数组中指定键的Entry对象
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
//通过一个键值删除一个Entry对象
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
//得到键得hash值,并求出下标
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
//从数组中得到entry对象,prev当街节点的前一个节点
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;
}
//此时说明没有找到要删除的节点,e = null;
return e;
}
//清空整个table数组
public void clear() {
modCount++;
//将table数组中的元素全部赋值为null
Arrays.fill(table, null);
size = 0;
}
containsValue方法和相关方法
//判断是否包含一个指定值的节点
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
Entry[] tab = table;
//遍历整个table数组,找到值为vaule的节点返回true
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
//
private boolean containsNullValue() {
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (e.value == null)
return true;
return false;
}
clone方法
public Object clone() {
HashMap<K,V> result = null;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// assert false;
}
if (result.table != EMPTY_TABLE) {
result.inflateTable(Math.min(
(int) Math.min(
//得数组的总容量,因为负载可以设定,但是这里将负载因子规定最小为0.25f
size * Math.min(1 / loadFactor, 4.0f),
// we have limits...
HashMap.MAXIMUM_CAPACITY),
table.length));
}
result.entrySet = null;
result.modCount = 0;
result.size = 0;
result.init();
//将本对象中的table数组中的所有元素放入到result中去
result.putAllForCreate(this);
return result;
}
addEntry方法
//给table[bucketIndex]对应的链表添加一个新节点
void addEntry(int hash, K key, V value, int bucketIndex) {
//添加一个Entry对象时,如果table数组中的元素数大于扩容阈值时,需要扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//因为table数组的容量必须是2的幂次方所以扩容两倍
resize(2 * table.length);
//扩容后数组容量改变,所以要重新计算hash值下标
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//构成entry对象,加入到数组中
createEntry(hash, key, value, bucketIndex);
}
createEntry方法
//创建一个entry对象,将它加到table[bucketIndex]中
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++;
}
迭代器
private abstract class HashIterator<E> implements Iterator<E> {
//下一个节点的引用
Entry<K,V> next; // next entry to return
//与modCount进行比较来判断
int expectedModCount; //fast-fail
//下一个节点的下标
int index;
//当前节点 // current slot
Entry<K,V> current; // current entry
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
//找到table数组中第一个不是null的元素
while (index < t.length && (next = t[index++]) == null)
;
}
}
//是否存在下一个节点
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();
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;
//删除对应的Entry对象
HashMap.this.removeEntryForKey(k);
//更新modCont的值
expectedModCount = modCount;
}
}
KeySet方法
//这也是我们经常用的方法了,返回值是得到HashMap的键集
public Set<K> keySet() {
//keySet是HashMap父类的AbstractSet<K>的一个成员,初始化后keySet=null
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
private final class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}
Iterator<K> newKeyIterator() {
return new KeyIterator();
}
//这个继承继承的非常巧妙,此类继承了HashIterator<K>,所有拥有hasNest()方法
private final class KeyIterator extends HashIterator<K> {
public K next() {
//nextEntry()返回当前Entry对象
return nextEntry().getKey();
}
}
关于在多线程条件下头插法引起的链表成环的问题
这个方法的功能是将将老数组的节点放入到扩容后的新数组中
1 void transfer(Entry[] newTable, boolean rehash) {
2 int newCapacity = newTable.length;
3 for (Entry<K,V> e : table) {
4 while(null != e) {
5 Entry<K,V> next = e.next;
6 //是否从新键得hash值,可通过参数来决定
7 if (rehash) {
8 e.hash = null == e.key ? 0 : hash(e.key);
9 }
10 //求出每一个元素在新数组中的下标
11 int i = indexFor(e.hash, newCapacity);
12 //这里链表的递增是头插法插入,因为这样不用寻找链表的尾节点
13 e.next = newTable[i];
14 newTable[i] = e;
15 e = next;
}
}
}
假设两个线程同时执行transfer()方法,为了方便说明我们规定两个线程分别为线程A和线程B
老数组为下图:
当线程A第一次进入for循环和while循环,执行完第5行的代码时,该线程的时间片段到了,紧接着执行B线程竞争到了CPU,开始执行。此时对于A线程来说,e = 节点1的地址(或者叫引用)。
B线程在它所在的时间片段内完成了扩容操作,为了方便讲解,我们假设假设扩容完毕后,变成了下模样。
这时B线程的时间片段也到了,A线成竞争到CPU开始执行,此时 e 是节点1的地址,紧接着执行完这几行代码时
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
我们的新数组变成了如下模样
我们发现我们的单项链表成环了,一旦成环,就代表我们遍历某一个链表就成了死循环。
这从根本上证明了HashMap是多线程不安全的