Java传家宝:微信公众号(Java传家宝)、Java传家宝-B站、Java传家宝-知乎、Java传家宝-CSND
双列集合
常用的实现了**Map**顶层接口为**双列**集合,常用的有**HashMap,LinkedHashMap,TreeMap,HashTable**,其他的在本文没有解析。Map接口没有继承Iterable接口,说明双列集合都不可以**进行迭代器迭代**。
HashMap
哈希表,**无序**,采用HashCode判断索引位置。用于存储**键值对**,通过**数组+链表+红黑树**实现。其中,**允许键为null,默认索引为0**,允许**存null值**。先看一下结构图:
通过结构图也能发现其他几个特点:
- 实现了Serializable接口,可序列化
- 实现了Cloneable接口,可复制
接下来研究一下JDK1.8的源码实现原理,先说结论:
- 空构造只设置加载因子为0.75,数组仍然为空
- 第一次添加元素时,设置默认容量DEFAULT_INITIAL_CAPACITY为16
- 扩容门槛threshold初始化为加载因子0.75*默认容量16
- 当容量使用大小size达到门槛值时,扩容为原来的2倍,最大扩容为1<<30=1073741824,是Integer.MAX_VALUE/2 + 1
- 如果链表长度大于8且总长度大于64。链表就转为红黑树
先看一下构造函数:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
// 加载因子设置为默认0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
再看一下添加put函数:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 求key的hash值
static final int hash(Object key) {
int h;
// key如果为null,索引为0
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 添加函数,删除了部分
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 数组为null或者长度为0.说明第一次添加,resize为默认长度10
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// n=最终长度,通过(n - 1) & hash得到key应该存放的索引
if ((p = tab[i = (n - 1) & hash]) == null)
// 该位置无元素,即无Hash冲突,直接放置即可
tab[i] = newNode(hash, key, value, null);
else {
// 发生Hash冲突
Node<K,V> 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<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);
//当链表节点大于等于8时,触发转红黑树的动作
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 在里面判断长度是否大于等于64,是就转为红黑树
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;
}
}
// 使用容量大小达到门槛值,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashTable
与HashMap类似,**存储键值对**,**无序**。不同的是底层采用**数组+链表**结构,通过Synchronized关键字实现**线程安全**。先看一下结构图:
![Hashtable](https://img-blog.csdnimg.cn/img_convert/63c9bb2efcbab453d65cd2b7ec91686c.png)
通过结构图也能发现其他几个特点:
- 实现了Serializable接口,可序列化
- 实现了Cloneable接口,可复制
接下来研究一下JDK1.8的源码实现原理,先说结论:
- 空构造默认长度为11,加载因子为0.75,扩容门槛threshold初始化为新容量*加载因子
- 扩容时,扩容为原容量的2倍+1
- 键值均不可以为null
接下来看一下源码实现,先看一下空构造:
public Hashtable() {
// 调用另一个有参构造
this(11, 0.75f);
}
public Hashtable(int initialCapacity, float loadFactor) {
// 设置加载因子0.75
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
// 初始化门槛值(int) 11*0.75
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
再看一下添加方法:
public synchronized V put(K key, V value) {
// 值不可为空
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
// 键不可为空 获得键的hash值
int hash = key.hashCode();
// 计算得到索引
int index = (hash & 0x7FFFFFFF) % tab.length;
// 拿到索引位置的元素
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
// 不为空,判断是否与key相同
if ((entry.hash == hash) && entry.key.equals(key)) {
// 相同 替换值返回即可
V old = entry.value;
entry.value = value;
return old;
}
// 不同 网下查找下一个链表节点
}
// 添加新节点即可
addEntry(hash, key, value, index);
return null;
}
TreeMap
树映射,存储**键值对**,可以通过元素对象内部实现的**Comparable接口**规则实现**自然排序**,也可以通过传入比较器实现**自定义排序**。先看一下结构图:
![TreeMap](https://img-blog.csdnimg.cn/img_convert/771efe341d13c7571d987439912eee90.png)
通过结构图也能发现其他几个特点:
- 实现了Serializable接口,可序列化
- 实现了Cloneable接口,可复制
- 实现了SortedMap,可排序
接下来研究一下JDK1.8的源码实现原理,先说结论:
- 空构造只做一件事,设置比较器为null
- 没有传入比较器时,元素对象必须实现了Comparable接口
- 键不能为null
首先看一下空构造的源码:
private final Comparator<? super K> comparator; // 比较器
private transient Entry<K,V> root; // 树形结构,保存根节点即可
public TreeMap() {
// 设置比较器为null
comparator = null;
}
在看一下put添加方法:
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
// 第一个元素检查是否实现了Comparable接口,或者是否传入了比较器
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
return null;
}
int cmp;
Entry<K,V> parent;
// 拿到比较器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
...传入比较器
}
// 未传入比较器
else {
// 键不能为null
if (key == null)
throw new NullPointerException();
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
// 与key相比
if (cmp < 0)
// 小于网左子树查找
t = t.left;
else if (cmp > 0)
// 大于网右子树查找
t = t.right;
else
// key相同,替换旧值即可
return t.setValue(value);
} while (t != null);
}
// 没找到相同的key 就放在叶子节点
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
// 小于父节点放在左叶子节点
parent.left = e;
else
// 大于父节点放在右叶子节点
parent.right = e;
// 根据红黑树的规则调整
fixAfterInsertion(e);
size++;
return null;
}
LinkedHashMap
**链表+HashMap**的结合,属于双向链表,但是每一个节点都是一个Entry。维护了**存储顺序**,存储**键值对**。先看一下结构图:
通过结构图也能发现其他几个特点:
- 实现了Serializable接口,可序列化
- 实现了Cloneable接口,可复制
- 继承了HashMap,拥有HashMap的属性
接下来研究一下JDK1.8的源码实现原理,先说结论:
- 维护了插入顺序,空构造调用了HashMap的空构造函数,accessOrder设置为false表示插入顺序
- 修改了Node,添加了每个节点保存了before, after前后节点地址
先看一下空构造器:
// false 表示维护插入顺序 true 表示维护访问顺序,即LRU
final boolean accessOrder;
public LinkedHashMap() {
// 调用了父类的空构造
super();
accessOrder = false;
}
添加方法就不看了,继承的HashMap的,再看一下LinkedHasHMap在HashMap上增加了哪些属性:
// head tail用于维护访问顺序
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
// false 表示维护插入顺序 true 表示维护访问顺序,即LRU
final boolean accessOrder;
static class Entry<K,V> extends HashMap.Node<K,V> {
// 每个节点保存了before, after前后节点地址,实现双向链表
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}