个人理解
HashMap是一个用于存储key-value键值对的集合,每一个<key,value>也被叫做Entry。这些键值对(Entry)分散存储在一直数组当中,这个数据就是HashMap的主体。
HashMap数组的元素在初始化为都为null;
如下图:
对于HashMap,经常用到就是get和put方法。
1.PUT方法
比如调用Put方法插入<demo,0>的值,即HashMap.put(“demo”,0)。
首先需要使用哈希函数(Hash)来计算确认Entry的位置(index):
index = Hash(“demo”);
假定计算出来的index是3,如下图
但是呢,因为长度有限,当插入的元素无限多时,无可避免的就回出现index冲突,即Hash函数计算的值相同,也就是常说的哈希碰撞,哈希冲突。 如下图:
这个时候需要怎么解决呢,这里介绍利用链表来解决
HashMap数组中的每一个元素不止作为一个Entry对象,也可以是一个链表的头结点。每个Entry对象通过next指针(也可以叫做尾指针)指向下一个Entry对象,那么当新的Entry通过Hash运算映射到冲突的位置时,只需要插入到对应的链表中(注意这里使用的是“头插法”,至于原因后续会有介绍)。如下图
2.GET方法
使用Get方法就是根据Key值来查找对应的Value,首先把输入的Key值做一次Hash映射,得到对应的index:int index = Hash(“Key”)。由于之前说过的Hash冲突,那么同一位置可能存在多个Entry对象,这时需要顺着对应链表从头结点开始,挨个进行查询比对。如下图
步骤一:我们通过index,找到查看头结点Entry1,Entry1的key是张三,不是我们要的结果。
步骤二:查询next结点Entry3,Entry3的key是李四,正是我们要的结果。
之前的Put方法使用头插法,是因为HashMap的发明者认为,后插入的Entry被查询的可能性比较大。
3.源码简单分析(java8)
3.1 参数(DEFAULT_INITIAL_CAPACITY)介绍
static final int DEFAULT_INITIAL_CAPACITY = 16; //初始容量,且每次扩展或手动初始化时,长度必须为2的幂
解释下为什么初始容量为16?
初始容量为16是为了服务于从key值映射到index的Hash算法,上面描述过通过key值计算出index会用到一个Hash函数(index = Hash(“key”)),为了实现高效的Hash算法,HashMap的发明者采用了位运算的方法。
位运算的公式:index = HashCode(Key)& (length - 1)注:其中的length为HashMap的长度。下来模拟下整个过程:假定key值为“demo”
步骤一:计算“demo”的hashcode,假定结果十进制为888745,那么二进制位11011000111110101001。
步骤二:假定HashMap的长度为16,计算length-1的结果十进制为15,二进制为1111。
步骤三:把以上两个结果做与运算,11011000111110101001 & 1111=1001,十进制为9,所以index为9.
从上面可以看到,Hash算法得到的index结果,完全取决于Key的HashCode值的最后几位。至于为什么不用其他的长度,假设HashMap的长度为10,重复上面运算:
单独这个结果,表面上没有问题。我们在试下新的Hashcode 11011000111110101011:
再试一个新的Hashcode 11011000111110101111:
从上面可以看到Hashcode的值只是第二位第三位从0变成了1,但是运算结果都是1001,也就表明当HashMap的长度为10时,有些index出现的几率会更大,有些则不会出现(如:0111),很显然这不符合Hash算法均匀分布的原则。反之,当长度为16或2的幂,length-1的值所有二进制位都是1,这种情况下,index的结果等于Hashcode的后几位值。那么只要保证输入的Hashcode本身均匀分布的话,那么hash算法的结果也就均匀分布。
3.2 构造函数
A:HashMap() 无参构造函数
//构造一个初始容量为16,装载因子为0.75的空的HashMap
public HashMap()
{
loadFactor = 0.75F;
}
B: HashMap(int paramInt)
//构造一个初始容量为paramInt,装载因子为0.75的空的HashMap
public HashMap(int paramInt)
{
this(paramInt, 0.75F);
}
注意:HashMap开辟的最大容量为1GB,所以paramInt的最大值为1073741824,如果paramInt的值大于1073741824,会自动默认为1073741824。如下:
C:public HashMap(int paramInt, float paramFloat)
public HashMap(int paramInt, float paramFloat)
{
if (paramInt < 0) {
throw new IllegalArgumentException("Illegal initial capacity: " + paramInt);
}
if (paramInt > 1073741824) {//当长度大于1073741824,自动默认为1073741824
paramInt = 1073741824;
}
if ((paramFloat <= 0.0F) || (Float.isNaN(paramFloat))) {//负载因子小于0,或者不是数字时,抛出异常
throw new IllegalArgumentException("Illegal load factor: " + paramFloat);
}
loadFactor = paramFloat;
threshold = tableSizeFor(paramInt);
}
设定threshold,当HashMap的size到了 threshold时,就要进行resize,也就是扩容操作。
tableSizeFor() 方法的主要功能是返回一个大于给定整数且最接近2的幂次的整数,如给定整数位10,则返回2的4次方16。如下图:(移位运算就不介绍了)
static final int tableSizeFor(int paramInt)
{
int i = paramInt - 1; //这里减1,是为了方式paramInt 已经是2的幂,执行完位运算返回的值是paramInt 的2倍,因为paramInt 已经是2的幂次,已经满足条件了
i |= i >>> 1;
i |= i >>> 2;
i |= i >>> 4;
i |= i >>> 8;
i |= i >>> 16;
return i >= 1073741824 ? 1073741824 : i < 0 ? 1 : i + 1;
}
D:public HashMap(Map<? extends K, ? extends V> paramMap)
//构造一个和指定Map有相同mappings的HashMap,初始容量能充足的容下指定的Map,负载因子为0.75
public HashMap(Map<? extends K, ? extends V> paramMap)
{
loadFactor = 0.75F;
putMapEntries(paramMap, false);
}
final void putMapEntries(Map<? extends K, ? extends V> paramMap, boolean paramBoolean)
{
//得到paramMap中元素的个数
int i = paramMap.size();
//当paramMap中有元素时,将paramMap中的元素放入本HashMap中
if (i > 0)
{
//判断table有没初始化,没有的话先进行初始化,初始化是在put方法中进行
if (table == null)
{
float f = i / loadFactor + 1.0F;
int j = f < 1.07374182E9F ? (int)f : 1073741824;
if (j > threshold) {
threshold = tableSizeFor(j);
}
}
else if (i > threshold)
{
//扩容
resize();
}
Iterator localIterator = paramMap.entrySet().iterator();
//循环吧paramMap中的元素放入本HashMap中
while (localIterator.hasNext())
{
Map.Entry localEntry = (Map.Entry)localIterator.next();
Object localObject1 = localEntry.getKey();
Object localObject2 = localEntry.getValue();
putVal(hash(localObject1), localObject1, localObject2, false, paramBoolean);
}
}
}
本人目前只整理到这里,后续会继续添加或修改