此篇文章基于java jdk8,作者在学习过程中总结的知识点,尽量严谨表述,如有错误,望指正
一、散列表的引入
首先看两种简单的数据结构
数组:查询快;增删慢
链表:查询慢,但是增删快
hash表(散列表的引入):
是数组+链表,综合了上述两种数据结构的优点,
什么是hash?什么是hash碰撞?
hash也称为散列,其原理就是把任意范围的输入经过映射转换成固定范围的输出,而这个映射规则就是hash算法,映射后的值的二进制串就是hash值;不同的输入值,得到的hash值一样,就是hash碰撞
hash的特点:
1、从hash值不能反向推导出原数据
2、相同的输入,对应同一个hash
3、hash算法的效率要高,对于大的数据也能很快的计算出hash值
4、hash算法的冲突概率要尽可能的小,即使数据尽可能均匀的分布。
二、HashMap的原理
先弄清楚了原理,最后分析源码会简单很多
HashMap里面存放的key-value 对,其中key唯一,即不能有两个key是同一个对象,但是存放key的时候,还应该关注key的每一个属性(所以即使两个不同的对象,它们的所有属性都一样,我们都需要认为它是两个同样的对象,不能使它存放进来,所以需要重写存放的key对象的类的equals方法和hashcode方法(使它们和自身属性关联起来))
HashMap的底层结构图:
HashMap的结构图中,可以得知HashMap是由数组+链表+红黑树构成(当每个桶位的元素达到8个以上时,则有可能转为红黑树,实际转化红黑树中,还要看HashMap的容量是否达到64)
什么是链化:
数组的每一个索引位置,我们称为一个桶位,同一个桶位,已经存了一个元素后,后面的元素链接到第一个元素,这样就形成一个链表,这就是链化;
为什么要引入红黑树
当一个桶位,链化严重,(即形成的链表过长),链表的缺点是查询慢,链表过长,会使得查询效率变低,这时链表会转化成红黑树(本篇文章不分析具体是如何转变成红黑树,只需知道什么时候需要转换成红黑树即可)
Node结构分析:
为什么要分析Node,因为HashMap中有一个table属性用来存放我们put进去的元素,它是一个Node数组,我们的元素就是以Node形式存放的
transient Node<K,V>[] table;
Node是定义在HashMap中的一个内部类,主要关注以下几个属性,可知
1、其中除了存放key 和value,还有一个hash,这个hash值是key的hashcode返回值经过hashmap的hash方法得到的一个值,(注意这个hash方法不是之前提到的hash算法的体现,hash方法的作用在后面源码分析再解释,hash算法的体现实际是写在put方法内部)
2、其中hash和key都是final修饰,证明一旦创建Node,存的key对象和hash值都是不可变的(也就是说,一个元素存放到HashMap后,即使后期修改key这个对象里面的属性值,使得hashcode值发生改变,也不影响这个Node里面存放的hash值)
3、next属性也是一个Node类型的,用于存放下一个Node元素,这表示每个桶位中存放多个元素时是以单向链表的形式存放;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...//省略后面代码
}
put()方法原理分析:
1、首先,对key的hash值(这个hash值是已经调用hash()方法处理过的)进行处理,得到一个[0 ,数组长度-1]的值,这就是hash算法的体现,得到的这个值就是将要存放元素的桶位位置索引
2、找到桶位后,如果桶位没有存元素,则把元素放到桶位,如果桶位有元素,则将要存放的元素和桶位里及其后面链接的元素的key逐个对比,如果没有相同的,则将待存放元素链接到最后面,有key相同的元素,则将待存放元素的value值进行替换
扩容的原理:
当我们存放的元素过多时,这时hash碰撞的概率增加,桶位下面链接的元素也会增加,查询效率变低,这时就需要我们将数组大小扩容为之前的两倍;
什么时候扩容呢?
存放的元素大于数组的长度*负载因子,负载因子的默认值是0.75,也可以调用构造器时,自己指定
三、HashMap的源码分析:
1、HashMap里面常用的属性,方法说明;
//--------------------方法说明----------------------- static final int hash(Object key) //用于处理key的hashcode值得到一个hash值 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); //put方法内部调用putval方法,我们真正需要分析的也是putval的源码 }
final Node<K,V>[] resize() //扩容方法,当数组需要扩容时,调用此方法
// ----------------------常量说明--------------------------
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 //默认的初始化数组容量
static final int MAXIMUM_CAPACITY = 1 << 30; //系统允许的最大数组容量
static final float DEFAULT_LOAD_FACTOR = 0.75f //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //树化阈值 和 最小树化容量阈值,一起控制链表转换成红黑树的值
static final int UNTREEIFY_THRESHOLD = 6 //解树化阈值,当桶位下的元素低于这个值时,由红黑树变成链表
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量阈值,
//------------------------属性说明-----------------------
transient Node<K,V>[] table; //元素存放的数组
transient int size; //已经存放的元素个数
transient int modCount; //对hashmap进行修改的次数总和,增加,删除元素,都会增加这个值,修改不会
final float loadFactor; //加载因子,乘以数组长度得到扩容的阈值。
int threshold; //需要扩容的阈值,默认是数组长度*加载因子
2、构造器说明:
2.1、new HashMap (initialCapacity , loadFactor) ;
// 传入需要new的数组容量大小,指定加载因子
public HashMap(int initialCapacity, float loadFactor){
...
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
...
}
这个构造方法里面没有初始化table,只是调用一个tableSizeFor方法得到容量值赋值给threshold(threshold本来是存放扩容阈值的,但是这里存放的是table的长度,因为,第一次调用put方法时,如果table没有初始化,会调用resize方法初始化table,这里面会根据threshold值创建数组大小)
2.2、public HashMap(int initialCapacity)
(嵌套调用上面的构造器)
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
2.3、public HashMap()
(啥也没做,只是给负载因子赋上默认值)
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
3、tableSizeFor方法源码分析:
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;
}
这个方法,经过一些列的位运算,得到一个大于或等于cap的最小2的次幂值,比如,如果cap是8,则得到8,如果是9,则得到16;所以当调用一个构造器指定创造数组容量为9时,实际这个数组在第一次扩容时会创建一个大小为16的数组;
4、put()方法源码分析(重点)
put方法里面调用putval方法,我们实际分析的是putval方法:
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)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
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;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
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;
}
5、resize()方法源码分析(重点)
6、get方法分析
7、remove方法分析