要点:
HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元。Entry是HashMap的一个内部类,实现了Map.Entry接口。
Entry有以下几个重要的属性:
简单来说,HashMap就是由数组和链表组成的,引用一张图片来说就是这样:
数组是Map的核心主体,而链表的存在只是为了解决Hash冲突。
当我们进行插入操作的时候,如果目标数组中不含链表,即该数组元素为null或者数组中Entry的next指向了null,则进行查找或者插入的操作的事件复杂度就为O(1)。
如果目标数组中包含链表,则在进行插入操作的时候,需要遍历链表。如果该元素存在,就会重新覆盖。如果不存在,则会在尾部新增。进行查询操作的时候,同样需要遍历链表,然后通过key的equals方法来判断是不是我们要找的元素。所以当数组中链表越少,则HaashMap的效率越高。
HashMap的几个重要属性:
transient Node<K,V>[] table //实际上存储key-value的数组,被封装成了Node
transient int size // table的大小
final float loadFactor //负载因子,代表了hashmap被填充的程度。默认为0.75,超过0.75之后会进行扩容操作。可以通过构造函数指定,减缓了哈希冲突的发生。
int threshold//存储阈值,未被初始化的时候,默认为16.被初始化后,值为初始容量*loadFactor
transient int modCount;//hashmap被改变的次数。如果在hashmap迭代的过程中其他线程对hashmap进行操作了,则会抛出异常。
HahMap有4个构造函数,分别是无参构造函数,含有1个参数的构造函数,含有两个参数的构造函数,用map去初始化的构造函数。
其中,前两个内部都调用了了第三个构造函数。我们直接分析第三个:
//构造一个空的初始容量为initialCapacity,负载因子为loadFactor的HashMap
public HashMap(int initialCapacity, float loadFactor) {
//如果初始容量小于0,抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始容量大于最大容量,将最大容量复制给初始的容量
if (initialCapacity >MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果负载因子小于0,或者负载因子不是一个数字,抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//赋值
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//最大容量
//static final int MAXIMUM_CAPACITY = 1 <<30;
看到这里我们有一个疑惑,我们将传入的参数赋值给了类变量,但在构造函数中并没有对我们的存储结构进行改动啊,Node数组没有变化啊。其实真正初始化table 这个变量的操作在put()方法中。
.我们来看put()方法:
//实现put和相关方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果table为空或者长度为0,则resize()
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//确定插入table的位置,算法是(n - 1)& hash,在n为2的幂时,相当于取摸操作。
////找到key值对应的槽并且是第一个,直接加入
if ((p = tab[i = (n - 1) &hash]) == null)
tab[i] = newNode(hash, key, value, null);
//在table的i位置发生碰撞,有两种情况:
//1.key值是一样的,替换value值,
//2.key值不一样的有两种处理方式:2.1.存储在i位置的链表;2.2.存储在红黑树中
else {
Node<K,V> e; K k;
//第一个node的hash值即为要加入元素的hash
if (p.hash == hash&&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//2.2
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//2.1
else{
//不是TreeNode,即为链表,遍历链表
for (int binCount = 0; ; ++binCount) {
///链表的尾端也没有找到key值相同的节点,则生成一个新的Node,
//并且判断链表的节点个数是不是到达转换成红黑树的上界达到,则转换成红黑树。
if ((e = p.next) == null) {
// 创建链表节点并插入尾部
p.next = newNode(hash, key, value, null);
////超过了链表的设置长度8就转换成红黑树
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;
}
}
//如果e不为空就替换旧的oldValue值
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;
}
为主干数组table在内存中分配存储空间的时候,会选取2的整数次幂,原因是:
要点:
由于HashTable中很多操作时与HashMap类似的,所以我们对比HashMap来看HashTable
1. 线程安全性: 我们看同样功能的方法,
// 判断Hashtable是否包含key
public synchronized boolean containsKey(Object key)
// HashMap是否包含key
public boolean containsKey(Object key)
我们可以看出,hashtable比hashmap多了synchronized 锁来保证线程安全
2.插入元素能否为空:
在上方我们提到,hashmap中是可以插入
3.遍历方式:
hashmap和hashtable都是用了Iterator,但除此以外hashtable还使用了Enumeration
4.索引的计算方式不同:
hashtable直接使用了key的hashcode()获取的值
hashmap先获取l了key的hashcode()值,叫做hash,然后再通过一个hash()方法得到了一个h,然后将h与length-1做了位运算。得到了索引的值。
5.内部数组的扩容方式不同
hashtable的默认容量为11,扩容方式为size = size2+1,且不要求数组容量为2的n次幂
hashmap的默认容量为16,扩容方式为size = size2,求数组容量为2的n次幂,
ConcurrentHashMap使用了一种与hashmap类似的结构,但却也不太相同.
核心属性:
其中Segment是ConcurrentHashMap的一个内部类,它的属性中有一个核心的真正填充数据的HashEntry
与hashtable不同的是,hashtable实现线程安全是一个全局的锁,线程A去put数据的时候,线程B去get数据会被阻塞。而ConcurrentHashMap采用了分段的可重入锁(ReentrantLock),根据key获取到segment的位置之后,然后通过key的hashcode来定位到HashEntry,然后开始尝试获取锁(自旋),然后后面的过程与hashmap类似,在完成放入元素的操作之后会释放锁。不同的Segment之间的锁不会互相影响。相比hashtable提高了效率。
在JDK1.8之后, ConcurrentHashMap的结构就与hashmap一致了,都是数组+链表+红黑树的方式。
而且放弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。
将原来的HashEntry改为了Node。
ConcurrentHashMap的put方法经历了以下的判断:
ConcurrentHashMap的get()方法也可以支持并发。也1.7中一致,操作顺序如下:
使用开放地址法进行建立散列表时,建表前须将表中所有单元中存储的数据置空
1.线性探测法:
如果当前hash值发生冲突,就在此hash值的基础上加一个单位,直到不发生hash冲突。
基本思想:假设散列表 T[0,m-1],从初始地址D开始探查,则最长的探查序列为:
D,D+1,D+2,…,m-1,0,1,2,…,D-1
探查结束的三种情况:
当前探查的单元为空,则代表查找失败,如果是插入的话则将key值写入
当前探查的单元有key值,则代表插入失败,但是查找成功。
若探查到T[D-1]时,未发现空单元,也未找到key值,则表示查找失败并且插入失败(此时表满)
缺点:
2. 再平方探测:如果当前hash值发生冲突,就在此hash值的基础上加一个单位的平方,如果还是冲突,就在原来的hash值减去一个单位的平方。如果还是冲突就操作两个单位的平方,三个单位的平方等。
3. 伪随机探测:如果当前hash值发生冲突。就用随机函数生成一个随机值,加在原来的hash值上,直到不发生hash冲突。
通过计算出来的hash值,如果发生冲突,则用链表或者红黑树进行存储
对于相同的hash值,使用链表进行连接,使用数组进行存储链表。这里找到了一个能动画演示的效果:
拉链法动画演示
与开放地址法相比的优点:
缺点:
指针需要额外的空间,所以当数据规模较小的时候,可以选择开放地址法。将节省的指针控件可以用来增加散列表的规模,是的hash冲突减少
对于冲突的哈希值再次进行通过哈希函数进行运算,直到没有哈希冲突
建立公共溢出区存储所有哈希冲突的数据
先就写这些,以后再慢慢查漏补缺