在分析完HashMapJDK1.8之后,今天依然从以下几个方面分析JDK1.7版本的HashMap的底层源码实现,虽然JDK1.7版本下又分几个不同的版本,但都大同小异。
继承结构、基本属性、默认值、构造函数、扩容机制、CRUD操作
1.继承结构分析
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
HashMap继承自AbstractMap父类,实现类Map接口、实现序列化、克隆
2.基本属性、默认值
/*HashMap中的主要参数 = 容量、加载因子、扩容阈值讲解前先对参数解释一下
1. 容量(capacity): HashMap中数组的长度
a. 容量范围:必须是2的幂 & <最大容量(2的30次方)
b. 初始容量 = 哈希表创建时的容量
*/
//默认的容量大小:16->哈希结构中数组的默认值大小
static final int DEFAULT_INITIAL_CAPACITY = 16;
//哈希结构中数组的最大容量上限为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认值:默认的加载因子 -> 怎么用?在哪里使用?为什么这样用?
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap的存储结构,(transient:表示不进行序列化)
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
}
//存储entry节点的个数
transient int size;
//控制扩容的,扩容的阈值 ->在哪使用?怎么使用?
//当哈希表的大小>=扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
//扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
//扩容阈值 = 容量 x 加载因子
int threshold;
// 加载因子
final float loadFactor;
//hash种子
transient int hashSeed = 0;
//修改版本号
transient int modCount;
Entry结构存储信息包含:
key:键值对的键
value:键值对的值
next:在同一个哈希位置(索引)下的值通过next来连接
hash:和key相关的哈希值 key.hashcode
3.构造函数
//第一个构造方法:通过初始容量和状态因子构造HashMap,加载因子&容量=自己指定,其他三个构造方法都//会调用这个方法
public HashMap(int initialCapacity, float loadFactor) {
//参数校验:初始容量不能小于0,否则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果自定义的初始容量大于默认的最大容量(1<<30=2^30)则默认最大容量赋值给传入的初始容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果加载因子小于等于0或者加载因子不是数字,抛异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//当前加载因子=传入加载因子
this.loadFactor = loadFactor;
//扩容变量=传入初始容量
threshold = initialCapacity;
init(); //init方法在HashMap中没有实际实现,不过在其子类LinkedHashMap会有对应实现
}
//第二个构造方法:指定“容量大小”的构造函数,加载因子=默认=0.75,容量=指定大小
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//第三个无参构造方法:装载因子为0.75,容量为16,构造HashMap
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//第四个构造方法:通过其他map来初始化HashMap,容量通过传入的map的size来计算,加载因子为0.75,//容量为16
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); //初始化HashMap底层的数据结构
putAllForCreate(m); //添加m中的元素
}
4.CRUD操作
(1)HashMap
//hash方法
final int hash(Object k) {
//hash种子,在第一次put的时候会生成一次
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
(2) get()方法分析
public V get(Object key) {
//如果key为空,则取table[0]上的值
if (key == null)
return getForNullKey();
//key不为空时,根据key获取value
Entry<K,V> entry = getEntry(key);
//如果entry为null则返回null,若不为null则返回当前entry的value
return null == entry ? null : entry.getValue();
}
//get()方法中的getForNullKey()方法
//作用:当key=null时,则到以哈希表数组的第一个元素table[0]为头结点的链表中去
private V getForNullKey() {
//判断元素的个数为0返回null
if (size == 0) {
return null;
}
//遍历table[0]为头结点的链表,寻找key==null对应的值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
//否则返回null
return null;
}
//get()方法中的getEntry方法
//作用:key不为null时,去获得对应值
final Entry<K,V> getEntry(Object key) {
//判断元素个数为0返回null
if (size == 0) {
return null;
}
//根据key获取hash值
int hash = (key == null) ? 0 : hash(key);
//根据hash值计算对应的数组下标
//遍历以该数组下标的数组元素为头结点的链表所有节点,寻找该key对应的值
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//如果hash值和key相等,则返回对应的entry,通过equals()判断key是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
//否则返回空
return null;
}
总结get方法过程:
判断key是否为null,特殊处理,在0号索引位置进行遍历,若存在key为null的节点,则直接返回节点Entry的value值
若key不等于null,则对key进行哈希,并找到在哈希表中的索引位置(indexFor)遍历该索引位置的节点,
判断key是否存在(hash,key),存在则直接返回value,不存在返回null。
思考一下重写equal方法的同时必须重写hashCode()方法?如果不重写会有什么问题呢?
重写前的equals方法和hashCode方法都可以用来比较两个对象的地址值是否相同,
不同的是,两个地址值不同的对象的hashCode可能相同,但是equals一定不同。
(3)Entry
//从成员变量HashMap.Entry<K,V>[] table,可以看出Entry是HashMap的一个静态内部类,实现了map.Entry接口,
//即 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法
//table变量实际就是Entry[],和int[]类似,就是一个数组,不过加了括号<K,V>,
//后面的HashMap的put、get操作都是基于table实现的,都是存储在entry中
static class Entry<K,V> implements Map.Entry<K,V> {//该内部类实现了map接口的内部类Entry
final K key; //被定义为final类型,不可改变key的值
V value; //映射的value值
Entry<K,V> next;//在Entry内部类中又定义了一个Entry的成员变量,指向下一个元素的引用
int hash; //哈希值
/**
* Creates new entry.构造方法为Entry赋值
*/
Entry(int h, K k, V v, Entry<K,V> n) {
//将新的value赋值给当前类的value
//划重点:这里是老的entry赋值给新的entry的next,所以这里得出key的hash发生冲突后 不能说覆盖以前的值
//以前的值想要获取就在新的值得next里面,他们只是hash相同key不相同,只有hash相同时才会进行碰撞处理
//因为下标是hash和表容量计算出来的,没有发生碰撞时next为null
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;
}
//equals()
// 作用:判断2个Entry是否相等,必须key和value都相等,才返回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;
}
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
/**
* 当向HashMap中添加元素时,即调用put(k,v)时,
* 对已经在HashMap中k位置进行v的覆盖时,会调用此方法
* 此处没做任何处理
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* 当从HashMap中删除了一个Entry时,会调用该函数
* 此处没做任何处理
*/
void recordRemoval(HashMap<K,V> m) {
}
}
(4)put方法分析
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
//当哈希结构未初始化时(即table为空,第一次插入元素),
//使用构造函数时设置的阈值(即初始容量)进行初始化数组table
inflateTable(threshold);
}
//key为null时特殊处理,将该键值对添加到table[0]中,该位置永远只有一个value,因为最多允许一个key为空
if (key == null)
return putForNullKey(value);
//key不为null,计算该key的hash值,然后通过hash值计算相应的下标
int hash = hash(key); //根据key求hash值
int i = indexFor(hash, table.length); //根据hash值求下标
//以上两步获取到当前元素要存储的哈希表的索引位置
//判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表逐个判断)
//当发生 Hash冲突时,为了保证 键key的唯一性,哈希表并不会马上在链表中插入新数据,
//而是先查找该 key是否已存在,若已存在,则替换即可
for (Entry<K,V> e = table[i]; e != null; e = e.next) { //遍历index处链表
Object k;
//如果有值,则判断是否存在相同key,若存在,当前value则覆盖已有值的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this); //一个空的预留方法,可以重写
return oldValue;
}
}
//若key对应的键值对不存在,将key-value添加到table中,修改次数+1
modCount++;
addEntry(hash, key, value, i); //否则添加新元素,添加到table[i]处,元素数量+1
return null;
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//将初始值给定为大于等于当前初始值的最近的一个2的次方值
,
//即如果传入的容量大小是19,那么转化后的初始化容量大小为32
int capacity = roundUpToPowerOf2(toSize);
//重新计算阈值 threshold=容量*加载因子
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//使用计算后的初始容量初始化数组table
//即哈希表的容量大小=数组大小
table = new Entry[capacity]; //用该容量初始化table
initHashSeedAsNeeded(capacity); //选择合适的Hash因子
}
分析roundUpToPowerOf2
作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
private static int roundUpToPowerOf2(int number) {
// 若容量超过了最大值,初始化容量设置为最大值,否则设置为:>传入容量大小的最小的2的幂
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//当key为null时,做特殊处理,存放在0号索引位置上
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
//先查找table[0]中是否有值,若已经存在键为null的值,覆盖旧值
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//若不存在,即table[0]还是空,则插入此节点,修改次数+1
modCount++;
addEntry(0, null, value, 0);//新建一个Entry将它放入table[0]中
return null;
}
//根据hash值和table的容量计算出一个下标
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
// 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
return h & (length-1);
}
//分析addEntry()
//作用:添加键值(Entry)到HashMap中
void addEntry(int hash, K key, V value, int bucketIndex) {
//先判断大小,若元素个数>=容量,且出现碰撞
if ((size >= threshold) && (null != table[bucketIndex])) {、
//,则进行扩容,即2倍扩容
resize(2 * table.length); //每次2倍扩容
//如果当前key不为null则取hash(key),否则hash为0
hash = (null != key) ? hash(key) : 0; //重新计算hash值
//获取一个下标覆盖原有下标
bucketIndex = indexFor(hash, table.length); //重新计算key对应的table中的下标
}
//HashMap实际大小小于阈值,创建一个Entry,将key-value插入指定位置,bucketIndex是位置索引
createEntry(hash, key, value, bucketIndex);
}
//分析createEntey()
//若容量足够,创建一个新的数组元素Entry,并放到数组中
void createEntry(int hash, K key, V value, int bucketIndex) {
//将table中该位置原来的Entry保存到e中
Entry<K,V> e = table[bucketIndex];
//在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对放到(链表)后1个结点中
//将需插入的键值对放入到头结点中->从而形成链表
//即在插入元素时,是在链表头插入的,
table[bucketIndex] = new Entry<>(hash, key, value, e);
//已使用数组位置+1
size++;
}
//进行头插,创建一个新的entry(jdk1.7版本采用的是头插法)
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//分析resize
//作用:当容量不足时(容量>阈值),则扩容(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];
//将旧数组的数据(键值对)转换到新的数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//新数组table引用到HashMap的table属性上
table = newTable;
//计算新数组的扩容阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//哈希桶内的元素被逆序排列到新表中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历旧数组得到每一个key,再根据新数组的长度重新计算下标存进去,如果是一个链表,则链表中的每个
//键值对也要重新hash计算索引
for (Entry<K,V> e : table) {
//如果此位置上存放元素,则进行遍历,直到e==null,退出循环
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);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
总结put的操作过程:
1.第一次put值的时候初始化表空间
2.key为空的时候会加入
3.会根据key生成哈希值
4.如果发生碰撞(hash值相等)的时候,会将新的值存放在新的entry里,老的值会存放在新值的entry.next里
5.正常情况下 修改次数+1,元素数量+1,新建一个Entry放入table[bucketIndex]中
6.bucketIndex是根据hash值和当前table长度计算出来的
7.如果遇到扩容,新容器是旧容器的2倍,新的容器将重新生成hash种子,老元素会赋值到新容器中,注意:高并发的时候取值可能为null,严重时会出现数组越界,死循环的问题,所以HashMap是线程不安全的
(5)remove()方法分析
//作用:删除键值对
public V remove(Object key) {
//移除key对应table[]中的entry
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
//移除key对应table[]中的entry具体操作
final Entry<K,V> removeEntryForKey(Object key) {
//元素个数为0返回null
if (size == 0) {
return null;
}
//计算hash值,key==null和不等于null统一处理
int hash = (key == null) ? 0 : hash(key);
//计算存储的数组下标位置
int i = indexFor(hash, table.length);
//将当前下标的entry放在局部变量中
Entry<K,V> prev = table[i];
//局部变量pre赋值给局部变量e
Entry<K,V> e = prev;
//如果当前元素不为空
while (e != null) {
//取出当前元素的next
Entry<K,V> next = e.next;
Object k;
//判断hash值和key值是否相等,注意碰撞只是hash值相同key不同
//该判断中key是否为null都可以判断
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++; //相等修改次数+1
size--; //元素-1
//若删除的是table数组中的元素(即链表的头结点)
//则删除操作=将头结点的next引用存入table[i]中
if (prev == e)
//将next的值覆盖当前下标的值,没有碰撞时这时候table[i]当前值为null
table[i] = next;
//否则将以table[i]为头结点的链表中,当前Entry的前一个Entry中的next设置为 当前Entry的next
else
//碰撞情况下将next的值覆盖pre.next,也就是说这时候prev是有值的,移除的只是相同hashcode但key不同的碰撞体
prev.next = next;
e.recordRemoval(this);
//返回移除的entry
return e;
}
//上面我们知道了碰撞后的旧值在新值得next中,就是如果当前下标中有碰撞那么将e赋值给prev
prev = e;
//e=e.next,如果不为空则继续移除
e = next;
}
//e为null直接返回
return e;
}
final Entry<K,V> removeMapping(Object o) {
if (size == 0 || !(o instanceof Map.Entry))
return null;
Map.Entry<K,V> entry = (Map.Entry<K,V>) o;
Object key = entry.getKey();
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;
if (e.hash == hash && e.equals(entry)) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
//清除hashmap
public void clear() {
//修改+1
modCount++;
//循环table将每个下标对应的值都替换成null
Arrays.fill(table, null);
//元素个数归0
size = 0;
}
//循环table将每个下标对应的值都替换成null
public static void fill(Object[] a, Object val) {
for (int i = 0, len = a.length; i < len; i++)
a[i] = val;
}