1.HashMap的源码分析
2.HashSet的源码分析
一、HashMap的源码分析
(一) 概述
1、HashMap出现在JDK1.2版本,到JDK7版本为止,都没有产生过较大的变化
2、HashMap在JDK8版本产生了较大的变化
(1)HashMap在JDK7版本的时候,底层数据结构是【数组+链表】
(2)HashMap在JDK8版本的时候,底层数据结构是【数组+链表 + 红黑树】
3、JDK8HashMap设计的思想:因为链表过长会造成查询较慢,所以在JDK8中,链表长到一定程度,会
转化为红黑树,用于保持一定的效率
4、7和8版本的HashMap,链表都是单向链表,JDK7中HashMap的链表使用的是头插法,JDK8中
HashMap的链表使用尾插法
(二)HashMap的源码分析JDK7版本
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,
Cloneable, Serializable
实现了Map接口,因为HashMap是Map的实现类
实现了Cloneable接口,证明可以使用Object中的clone方法
实现了Serializable接口,证明支持序列化
/**
* 默认初始化容量为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;
/**
* 用于存储简直对对象的数组
*/
transient Entry<K,V>[] table;
/**
* 键值对对象个数
*/
transient int size;
/**
* 扩容阈值:当前数组容量*加载因子
* 作用:当我们数组快要装满的时候,达到了阈值,数组就会扩容
* 扩容阈值:默认的扩容阈值为12
*/
int threshold;
/**
* 加载因子
*/
final float loadFactor;
public HashMap() {
//将默认初始化容量16,和默认加载因子0.75,通过this语句访问有参构造并赋值
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
//判断指定容量如果小于0,抛出非法参数异常,不予创建对象
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//当指定的容量极大时,创建对象时只能将HashMap能够提供的最大容量作为当前Map集合底层容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断加载因子如果小于0或者不是个数就抛出非法参数异常,不予创建对象
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity
int capacity = 1;
// 确保容积最小为16
while (capacity < initialCapacity)
capacity <<= 1;
//将传入的加载因子,赋值给当前正在创建HashMap对象
this.loadFactor = loadFactor;
//计算扩容阈值
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建一个容量为16的Entry数组
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
/**
* 内部类,它的对象就是我们的键值对对象,也就是Entry对象
*/
static class Entry<K,V> implements Map.Entry<K,V> {
//键值对对象所包含的成员变量
final K key;//键
V value;//值
Entry<K,V> next;//用于将来形成链表挂载时,用于指向下一个链表节点的地址
int 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;//返回旧的值
}
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();
}
}
public V put(K key, V value) {
//判断当前的key值如果为null,就做出添加null键的操作,也就是HashMap允许添加null键
if (key == null)
return putForNullKey(value);
//计算当前键的哈希值
int hash = hash(key);
//索引:计算将来key要添加到数组的什么位置
int i = indexFor(hash, table.length);
//将当前计算出的索引上的元素拿过来
//判断当前索引i上是否已经存了元素
//如果已经存了元素就进入循环进一步判断
//for循环是根据已经有的键修改对应的值
// 将当前索引上的元素地址存储 地址不为空进入循环
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//比较当前老元素哈希和新的哈希一样吗?如果不一样,就不是一个键,如果一样,再通过equals确认,确保是一个键
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;//修改原来的value为新的value
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断如果现在数组容量已经达到了扩容的阈值,并且当前元素要插入的位置还有元素,就进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容为原来的2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, 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++;
}
总结:
(1)JDK7中,创建一个HashMap对象,底层使用【数组+单向链表】的哈希表来存储元素
(2)JDK7的数组初始容量默认为16,加载因子是0.75,扩容阈值是12
(3)数组:用于存放内部类Entry对象的,Entry对象实际上就是键值对对象
(4)加载因子:用于根据当前的数组大小,计算数组填满多少个元素之后就可以扩容了,设定为0.75是
为了保证较好的数组空间的利用,和及时的扩容
(5)当数组中的元素个数达到12个时(基于数组容量为16的情况),不直接扩容,我们添加一个元素
进数组,如果要添加的位置上已经有了元素,则数组扩容为【原来的容量乘2】,如果要添加的位置上没
有元素,则不扩容直接添加
(6)当添加一个元素的时候,不需要扩容,则创建Entry对象添加,并且,如果目标位置没有元素,直
接添加,如果目标位置有元素,则使用【头插法】行程单向链表
(7)当put方法添加key和value的时候,如果key的hash和具体的值都和某个已存在的元素的key相
等,就用新的value替换旧的value
(三)HashMap的源码分析JDK8版本
1、JDK8相较于JDK7底层从【数组+链表】变成了【数组+链表+红黑树】,主要目的在于提升效率,因
为如果链表挂载过长,会降低查询效率,如果链表很长的时候,将链表转为红黑树,这样一来可以保证
较高的查询效率
/**
* 默认初始化容量为16
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量为2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认加载因子:0.75
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 树形结构阈值:当链表长度为8的时候,就从链表转为红黑树(条件一)
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 回退链表结构阈值:当红黑树节点为6时,从红黑树变成链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 当数组的长度达到64的时候,可以将链表转为红黑树(条件二)
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 底层数组
*/
transient Node<K,V>[] table;
/**
* 键值对对象的个数
*/
transient int size;
/**
* 数组扩容阈值
* @serial
*/
int threshold;
/**
* 加载因子
* @serial
*/
final float loadFactor;
//当使用空参构造创建对象时,仅仅是准备了默认的加载因子0.75,【并没有准备数组】
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//调用put方法,重算key的哈希值,并且带上key和value
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)
//当数组没有创建的时候,创建一个长度为16的数组并且返回,赋值给tab
n = (tab = resize()).length;//n = 16
//经过一套算法,得到一个数组的索引i,并且从数组中获取i对应的元素,判断是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
//调用newNode方法,将传入的key和value创建成一个键值对对象并且添加到数组中
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e;
K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//当现在要添加的key和老元素的key一样,先临时把老元素的key存起来
else if (p instanceof TreeNode)//判断当前获取到的老键值对对象是不是红黑树的节点
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
/*
* 当我们要插入key,value时,但是数组选定的位置已经有了元素
* 就获取这个元素,看看这个元素有没有下一个节点
* 如果没有下一个链表节点,就利用尾插发形成单向链表挂在
* 如果有下一个链表节点,就通过循环,挪到下一个链表节点再判断这个节点有没有下一个节点,如果没有就挂 载,如果有继续向下移动
*
* 当移动数量达到8时,就尝试将链表转红黑树,但是还需要判断,数组容量是否足
够64
*
* */
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当链表长度大于等于7,就到
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)
e.value = value;//新的值覆盖旧的值
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//当节点个数超过数组扩容阈值的时候,对数组尝试进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
//当数组不存在的时候用于创建数组,或者容量不够的时候用于扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//先将老数组的地址值存储起来:null
int oldCap = (oldTab == null) ? 0 : oldTab.length;//老数组的长度0
int oldThr = threshold;//老阈值:0
int newCap, newThr = 0;
/*
* 数组必须存在且长度大于0
* 才能进入这个if
* */
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//老数组扩容为原来的2倍,并且大于默认的16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//原来的阈值扩大2倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//数组容量为16
newCap = DEFAULT_INITIAL_CAPACITY;
//扩容阈值大小为12
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;
@SuppressWarnings({"rawtypes","unchecked"})
//创建键值对对象数组:16
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将创建好的数组地址赋值给对象的数组声明
//到此为止,对象才有一个容量为16的Node数组,数组扩容阈值为12,加载因子为0.75
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/**
* 创建一个键值对对象并且返回
*/
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
总结:
过程一:从创建集合对象到添加第一个值
(1)使用空参构造创建HashMap的时候,底层仅仅是给集合对象赋值了默认的0.75的加载因子
(2)当我们调用put方法添加值的时候,帮助我们在底层创建一个容量为16的数组,并且赋值数组扩容
阈值为12
(3)将键值对对象Node创建出来,添加到数组中
过程二:
(1)调用put方法添加值
(2)判断要添加的这个目标位置上有没有元素存在,如果没有直接添加
(3)要添加的位置上有元素,判断键是否一样,如果一样,根据键修改值
(4)如果要添加位置上有元素,但是键不一样,就使用【尾插发】形成单向链表挂载
(5)如果在单向链表挂载的过程中,链表长度为8,尝试转为红黑树,但是,如果数组的长度也至少64,才能转换成功,否则失败
结论:JDK8的HashMap默认容量为16,默认加载因子为0.75,默认扩容阈值为12,当链表的长度大于8并且数组的长度大于64,才能将链表转为红黑树;当红黑树节点个数小于6,回退到链表。
注意:为什么链表转红黑树要求苛刻,因为转换树的过程,树实现自平衡的过程,都非常消耗内容,所
以不会轻易的将链表转为红黑树