一、新的琢磨和旧的理解
之前大致写了一篇hashmap的源码分析,地址。
但总觉得理解有很多错误的理解,比如之前只理解数据存储在hashmap中开始是数组,后来是链表,再后来是红黑二叉树,但最近几周感觉理解有问题,重新理解了下,才觉得大错特错。
其实真实的结构却是这样的
二、hashmap新的源码解析
1、创建hashmap对象
HashMap map = new HashMap<>();
Map map2 = new HashMap();
Map map3 = new HashMap(15);
源码的实现(部分源码)
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Node[] table;
transient int modCount;
final float loadFactor;
int threshold;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
可以发现,当不指定大小创建HashMap集合时,他会默认指定一个大小为16的数组结构。当指定大小呢?
指定大小创建HashMap集合对象
public HashMap(int initialCapacity) { //指定大小创建hashmap集合对象
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//上面this调用的方式
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);
}
//如果给定的容量大小不是2的n次幂,则让他成为2^n
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;
}
2、向hashmap集合中增加数据(重要)
map.put(1, "香蕉");
map.put(1,"bunana");
首先别奇怪我添加了重复的key,下面会有说到,我们先看源码的实现,源码很长,我们分块分析
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[] tab; Node p; int n, i;
//判断hashmap是否存在
if ((tab = table) == null || (n = tab.length) == 0)
//如果不存在,则采用resize()构建新的node[],并赋值给tab
n = (tab = resize()).length;
//n=tab.length,所以此处(n-1)&hash是计算数据在数组中保存的位置下标
if ((p = tab[i = (n - 1) & hash]) == null)
//如果数组下标内无数据,则直接将数据保存至数组中
tab[i] = newNode(hash, key, value, null);
else {
//计算到这个数据保存到数组下标的位置存在别的数据时
Node e; K k;
//判断key是否一致(数组类型数据)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断当前数组这个下标内的数据类型是否为二叉树
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
//如果数组中指定下标中保存的数据类型是链表
else {
//遍历链表 获取各项节点信息
//注意一点:jdk1.7是链表头追加,1.8才是尾追加
for (int binCount = 0; ; ++binCount) {
//2、如果最后一个数据的next节点为null 则保存数据
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//3、保存数据后,判断保存后的链表的长度,度过大于等于8-1时,则将链表转化为二叉树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//1、判断每个节点中的key是否相同,相同则做值的覆盖操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//这里是为了在put操作存放相同key时,将旧的key的value返回出去
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//每次put都增加数组中保存的数据长度,如果大于 容量*0.75f 则进行扩容操作
++modCount;
if (++size > threshold)
//执行扩容
resize();
afterNodeInsertion(evict);
return null;
}
在最开始的put方式中,他的hash做了什么?
static final int hash(Object key) {
int h;
//将数据的key信息进行hashcode运算,并将高16位和低16位做异或运算,求取hash数据
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//这样操作的hash数据有什么好处?
1、下面的添加代码有分析,数据是如何保存至数组的指定位置的
2、如果不进行高、低16位的异或运算,则计算的数组下标位会有较高的几率出现一样
3、当数组中的数据填充一样时,他会使用链表或者红黑二叉树进行数据的保存操作
我们针对putVal的操作,分3次分析:
1)的分析:
当我们在创建一个新的HashMap时,作为类的成员属性,他会初始化一个table
transient Node[] table;
1中的操作,将hashmap类创建时的table属性赋值给局部变量 Node
final Node[] resize() {
//将全局成员变量赋值给局部变量
Node[] oldTab = table;
//如果全局的成员变量是null,则大小给定为0,否则大小则是它本身的大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//将成员的容量和容压系数乘积保存至局部变量
int oldThr = threshold;
int newCap, newThr = 0;
//如果这个容器的大小不为0(初始化创建的hashmap对象不会进行此项)
if (oldCap > 0) {
//如果容器大小超过最大上限,则采取最大上限作为容器容量
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果此时的容器扩容一倍后依旧小于最大容量 并且 高于初始的大小
//前面说到容量为2^n
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数组对象
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//-----------------------------------非初始化start
//初始不会进入此项判断
//如果大小将要超过最大容压大小(总大小和容压系数的乘积)
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
//原数组中的数据保存采取hash与运算容量大小,所以可能存在数组下标为空的数据,此处是为了过滤空数据
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果链表中的next参数为null
if (e.next == null)
//计算新的数组中,数据所在的下标,并存入数据(数组)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//二叉树类型交给二叉树的方式处理
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
//链表数据的处理
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
//如果链表中数据的hash成员数据值 与 容器大小为0
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;
}
}
}
}
}
//----------------------------------------非初始化end
//1、如果是初始化操作,则创建新的Node[] 对象后,将其直接返回出去
//2、如果是扩容的node[] ,则需要创建新的大小的容器,并将旧数组中的数据移入新的数组中并返回出去
return newTab;
}
resize方法也很长,我们只需要关注你是新创建的hashmap还是添加数据后,容量要上限了,进行的扩容操作。
2)的分析
n = tab.length,因为数组的下标是从0开始的,所以n-1表示数据在数组中的下标范围!
如果数组的容量和node中hash成员数据的与运算数据为null时(计算的下标,在Node
tab[i] = newNode(hash, key, value, null);
创建新的newNode对象保存新的数据,并存在Node
3)的分析
当hashmap对象存在,且采取key计算的hashcode的高、低16位计算的hash数据,与容器容量与运算后,对应的下标中,数组中有对应的数据
例如:
他可能会有以下几项操作:
所以我们继续拆分
3.1)如果数据key一样,则进行value的替换操作
3.2)如果数据是二叉树,则采取二叉树进行数据的分析
3.3)如果数据是链表
else {
//遍历链表
for (int binCount = 0; ; ++binCount) {
//如果发现指定数组中保存的Node数据next数据为null,则将新的数据保存至next属性下
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果次数的链表长度大于等于定义的上限参数(8-1),则将链表转化为红黑二叉树的方式,存入数组的指定下标区内
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//链表转换红黑二叉树
treeifyBin(tab, hash);
break;
}
//如果遍历发现node数组中的指定下标中存入的链表数据的hash值一样,切两者key信息一样,则进行值的覆盖操作
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//此项代码也不能忽视,因为当存在重复的key信息时,他是进行了值的覆盖,并将旧的数据返回出去
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
3.4)在putVal操作要完成之前,都判断次数的大小是否超过了最大容压大小,如果大于则重新定义数组的大小
++modCount;
if (++size > threshold)
resize();
3、最后做点总结和补充
3.1、我的二叉树学的不好,最近还在研究中,就没具体说明二叉树保存数据是如何实现了。
3.2、hashmap保存数据是最初的数组,当计算的数组下标存在数据后,会判断此时的数据是链表结构还是二叉树结构,在根据指定的结构分析每个元素的hash属性值和新加入的数据hash属性值是否一致,一致则采取equals方式对比内容是否一样,如哦一样则覆盖,不一样则找到链表最末尾的next,将值拼接上去。
3.3、最后说下数组中保存数据,下标的计算
首先,将数据的key采取object类的hashcode()计算真实的hashcode值,
其次,将计算后的hashcode值高16位和低16位进行 异或 算法,这样可以更好的保证低位的数据的随机性,
最后,将计算的hash属性值和容量减一的二进制数据进行与运算,因为是与容量减一进行与运算,所以最大的下标为容量大小减一!
3.4、二进制的集中运算方式
总结:写的有些杂乱,后期再慢慢整理优化吧,看明白了那个复杂的putVal,其实感觉还是蛮简单的。要有耐心吧。
2019.07.27 在网上找到个比我的写的清晰的博客:地址。