1.HashMap
JDK1.7https://blog.csdn.net/xiaoyao2246
HashMap是基于哈希表的Map实现.
HashMap底层采用的是Entry数组和链表(1.7)实现.
HashMap是采用key-value形式存储,其中key是可以允许为null,但是只能有一个,并且key不能重复.
HashMap是线程不安全的.
HashMap存入数据的顺序和遍历的顺序有可能是不一样的.*
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{
//初始化桶大小,也就是数组的大小,默认大小为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//桶的最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//存放数据的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//存储key-value键值对的个数
transient int size;
//桶大小,在初始的时候可以显式指定(一定是2的次幂)
int threshold;
//负载因子,初始化时可以显式指定
final float loadFactor;
//修改次数,每次map集合变动一次,就加1
transient int modCount;
//真正存放数据的entry内部类
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
...省略其他
}
HashMap实际上是一个"链表"的数组table,每个数组中的元素存放链表的头结点,在每一个头结点的中,包含着下一个节点的地址,即数组和链表的结合体.
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
...省略其他
}
key其实就是写入数据的键
value是指写入的数据
next变量正是实现链表结构的关键
hash存放的是当前key的hashcode
到这里,我们应该有个概念:当我们创建了一个HashMap的时候,其实是创建了一个数组,此数组的默认大小是16,数组中的每一项都叫一个桶,数组中保存的数据正是Map.entry对象,它内部包含一个key-value键值对,还持有一个指向下一个元素的引用,还有他的key运算后的hashcode值.
再换句话说:
当系统开始初始化HashMap的时候,系统会创建一个长度为capacity 的Entry数组table,这个数组里可以存储元素的位置被称为"桶(bucket)",每个桶都有自己的索引,系统可以根据这个索引快速访问该bucket里的元素.
无论何时,HashMap的每个桶只存储一个元素(一个Entry),由于Entry对象可以包含一个引用,用于指向下一个Entry,所以会出现这种情况:HashMap的桶中只有一个Entry,但是这个Entry指向另一个Entry,这样就形成了一个Entry链.
put方法解析
- 首先判断HashMap中的table表是不是为空,如果为空,调用inflateTable(threshold)方法为table分配内存空间
- 然后判断key是否为空,如果key为空,则调用putForNullKey(value)方法,将value放在数组的第一个位置上.
- 若key不为空,则根据hash(key)方法计算出hash值,然后根据hash值,得到这个元素在table数组中的位置(下标),如果table在该位置已经存放了其他元素,则通过比较是否存在相同key,若存在则覆盖原来key的value,否则将该元素保存在链头(jdk1.8改成了尾插).
- 若table所在该处没有元素,那就直接将该元素放到此数组中的该位置上.
计算出hash值以后,会调用indexFor()方法来获得该数据在数组中的位置下标.我们知道对于HashMap的table而言,数据分布需要均匀(最好每一项都有元素),不能太紧也不能太松,太紧会导致查询慢,且hash碰撞的几率变大(不知道hash碰撞的同学自行百度),太松则浪费空间,所以怎么保证table元素分布均匀呢?那就是取模,在有些版本的源码中,indexFox方法是直接取模的,但是后来优化以后就变成了下面这样:
static int indexFor(int h, int length) {
return h & (length-1);
}
我们知道,HashMap的数组长度在roundUpToPowerOf2()方法的调用下,确保是2的次幂(在后面会详细讲为什么是2的n次方),当length为2的n次方时,h&(length-1)就相当于对length取模,也就是h%length,但是&比%运算要快的多,这是HashMap在效率上的优化.
以上就是对HashMap中put方法的源码分析,在这里,我们再来总结一下:
当程序试图将一对key-value放入HashMap中时,首先会根据key的hash值计算出该Entry对象的存储位置,如果两个Entry的key的hash值计算后的返回值相同,那么他们存储的位置也是相同的.如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但 key 不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部。
JDK1.8
https://blog.csdn.net/xiaoyao2246/article/details/88839212
https://www.zhihu.com/question/360072840/answer/1773772324?utm_source=qq&utm_medium=social&utm_oi=1019573850538819584
当Hash冲突严重时,在桶上形成的链表就会越来越长,这样在查询的时候效率就会越来越低;时间复杂度为O(N).因此jdk1.8对此进行了优化
在1.8以前,HashMap采用数组+链表来实现,同一个Hash值的节点都存储在一个链表中.而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值8的时候,就会将链表转换成红黑树,减少查询效率.
成员变量区别
在1.8中,HashMap的成员变量发生了变化:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;
}
区别有三点:
TREEIFY_THRESHOLD字段用于判断是否需要将链表转换为红黑树
UNTREEIFY_THRESHOLD字段用于判断红黑树何时转换为链表
HashEntry修改为Node
红黑树
//红黑树
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {
TreeNode<k,v> parent; // 父节点
TreeNode<k,v> left; //左子树
TreeNode<k,v> right;//右子树
TreeNode<k,v> prev; // needed to unlink next upon deletion
boolean red; //颜色属性
TreeNode(int hash, K key, V val, Node<k,v> next) {
super(hash, key, val, next);
}
//返回当前节点的根节点
final TreeNode<k,v> root() {
for (TreeNode<k,v> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
put过程
1.根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);
2.根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:
① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;
② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作;
③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样:
如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null;如果该链表已经有这个节点了,那么找到该节点并更新新数据,返回老数据。
为什么在JDK1.8中进行对HashMap优化的时候,把链表转化为红黑树的阈值是8,而不是7或者5呢?
根据注释中写到,理想情况下,在随机哈希码和默认大小调整阈值为 0.75 的情况下,存储桶中元素个数出现的频率遵循泊松分布,平均参数为 0.5,有关 k 值下,随机事件出现频率的计算公式为 (exp(-0.5) * pow(0.5, k) /factorial(k)))大体得到一个数值是8,那么退化树阀值为什么是6?如果退化树阀值也是8,则会陷入树化和退化的死循环中。如果退化阀值是7,假如对hash进行频繁的增删操作,同样会进入死循环中。如果退化树阀值小于5,我们知道红黑树在低元素查询效率并不比链表高,而且红黑树会存储很多索引,占有内存。所以退化阀值设为6比较合理。
LinkedHashMap
https://blog.csdn.net/xiaoyao2246/article/details/88836769
LinkedHashMap通过维护一个运行于所有条目的双向链表,保证了集合元素迭代的顺序,这个顺序可以是插入顺序或者访问顺序.
特点
- key和value都允许为空
- key重复会覆盖,value可以重复
- 有序的
- LinkedHashMap是非线程安全的
基本结构
LinkedHashMap可以认为是HashMap+LinkedList,也就是说,它使用HashMap操作数据结构,也用LinkedList维护插入元素的先后顺序.
LinkedHashMap的实现思想就是多态,理解LinkedHashMap能帮助我们加深对多态的理解.
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
多了以下两个属性
//链表的头结点
private transient Entry<K,V> header;
//该属性指取得键值对的方式,是个布尔值,false表示插入顺序,true表示访问顺序,也就是访问次数.
private final boolean accessOrder;
LinkedHashMap的Entry类继承了HashMap的Entry,并在此基础上进行了扩展,它拥有以下属性:
K key;
V value;
Entry<K, V> next;
int hash;
Entry<K, V> before;
NEtry<K, V> after;
前面的四个属性,是从HashMap中继承过来的,后面的两个是LinkedHashMap独有的,在这里需要明确next,before,after这三个属性的意思:
next是用于维护HashMap指定table位置上连接的Entry顺序的;before、after是用于维护Entry插入的先后顺序的.
正是因为before、after和header的存在,LinkedHashMap才形成了循环双向链表.
需要注意的是,header节点,是LinkedHashMap的一个属性,它并不保存key-value内容,它是双向链表的入口.
TreeMap
底层实现就是红黑树。中序遍历的结果即可得到有序的结果。
在TreeMap的put()的实现方法中主要分为两个步骤,第一:构建排序二叉树,第二:平衡二叉树
以根节点为初始节点进行检索。
与当前节点进行比对,若新增节点值较大,则以当前节点的右子节点作为新的当前节点。否则以当前节点的左子节点作为新的当前节点。
循环递归2步骤知道检索出合适的叶子节点为止。
将新增节点与3步骤中找到的节点进行比对,如果新增节点较大,则添加为右子节点;否则添加为左子节点。
按照这个步骤我们就可以将一个新增节点添加到排序二叉树中合适的位置
节点添加完成二叉搜索树可能会出现失衡,故需要进行调整,插入完成后会调用fixAfterInsertion(e); 调整的过程务必会涉及到红黑树的左旋、右旋、着色三个基本操作。
ConcurrentHashMap
https://blog.csdn.net/xiaoyao2246/article/details/88947767