HashMap
1. JDK1.7 HashMap底层数据结构分析
1.1. JDK1.7中,HashMap底层采用哈希表结构(数组+链表)实现,结合了数组和链表的各自优点,数组中的每个元素都是链表,HashMap通过 put&get 方法存储和获取
- 数组的优点
数组是顺序存储结构,通过数组下标可以快速实现对数组元素的访问,效率极高;
- 数组的缺点
插入或删除元素效率较低,因为可能需要数组扩容、移动元素;
- 链表的优点
链表是一种链式存储结构,插入或删除元素不需要移动元素,只需要修改指向下一个节点的指针域,效率较高;
- 链表的缺点
链表访问元素需要从头到尾逐个遍历,效率较低;
1.2. 单向链表由一个 Entry 内部类表示
Entry 包含四个属性:key,value,hash值和用于单向链表的 next(指向下一个entry节点)
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
1.3. capacity:当前数组容量,默认是16,始终保持 ,可以扩容,扩容后数组大小为当前的2倍;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
1.4. loadFactor:负载因子(或叫加载因子),配合下面的扩容阈值一起进行使用,默认值为0.75;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
1.5. threshold:扩容的阈值,意思就是当我们的数组中达到多少个元素的时候开始进行扩容,等于 capacity*loadFactor ;
1.6. 数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
2. JDK1.8中的HashMap
JDK1.8 对 JDK1.7 中的HashMap进行了一些修改,最大的不同就是新增了红黑树,所以JDK1.8中HashMap由 数组+链表+红黑树 构成;
在 JDK1.7 HashMap 中查找的时候,根据hash值能够快速定位到数组的具体下标,但如果是链表的话,则需要顺着链表一个个比较下去才能找到需要的元素,时间复杂度取决于链表的长度为 ;
为了降低这部分的开销,在 JDK1.8 中,当链表中的元素大于等于8个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为
2.1 HashMap中的红黑树
当单向链表中节点的个数大于等于8时,链表变为平衡二叉树,也就是红黑树,引入红黑树是为了解决单向链表深度过大的问题,优化链表的数据查找性能
红黑树的特点
- 树中每个节点最多有两个子树;
- 每个子树又是一个二叉树;
- 左子树的节点都小于等于根节点;
- 右子树的节点都大于等于根节点;
- HashMap根据节点的hash值生成二叉树;
JDK1.7 中使用 Entry 来代表每个 HashMap 中的数据节点,JDK1.8 中使用 Node 代表每个 HashMap 中的数据节点,只是换了个名字,没有其它区别
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
都是 key,value,hash和next这四个属性,不过 Node 只能用于链表的情况,红黑树的情况使用 TreeNode;
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
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);
}
根据数组元素中第一个节点的数据类型是 Node 还是 TreeNode 来判断该位置下是链表还是红黑树
3. HashMap中put操作
HashMap<String, String> map = new HashMap<>();
map.put("key1", "1");
map.put("key2", "2");
进入看 put 源码
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
我们先看 hash(key) 操作
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
将传入的 key 进行 hash 操作:
- 如果 key 为 null,则返回0;
- h = key.hashCode() :先取 key 的 hashCode 值赋值给变量h;
- h >>> 16 :然后将 h 右移16位
- ^ :最后进行 异或运算 hashCode()的高16位异或低16位计算,主要是从性能、hash碰撞来考虑的,减少系统的开销,降低hash碰撞的概率
然后我们再看调用的 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)
// resize() 创建数组
n = (tab = resize()).length;
// (n - 1) & hash 计算数组下表,并判断数组当前位置是否已经有值
if ((p = tab[i = (n - 1) & hash]) == null)
// 没有值的话,将值放入到上面计算的数组下标位置上
tab[i] = newNode(hash, key, value, null);
else {
// 如果有值的话,则进入到这里
Node<K, V> e;
K k;
// 先判断key的hash值是否与原来的相等,相等则说明当前传入的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) {
// 判断下一个节点是否为null
if ((e = p.next) == null) {
// 为null,则再创建一个节点(newNode)
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 将链表转换为红黑树,TREEIFY_THRESHOLD值默认为8
treeifyBin(tab, hash);
break;
}
// 这个判断和最开始的if判断一样
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// key 已经存在了,则直接将原来的值替换掉
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)
// 如果当前数组长度大于threshold(默认是12),则进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
简单总结一下 HashMap 的 put 操作
- 调用哈希函数获取key对应的hash值,再计算其数组下标;
- 如果没有出现哈希冲突,则直接放入数组,如果出现哈希冲突,则以链表的方式放在链表后面;
- 如果链表长度超过阈值(TREEIFY_THRESHOLD = 8),就把链表转换成红黑树;
- 如果节点的key已经存在,则替换掉value值即可;
- 如果长度大于threshold(12),则进行扩容。
4. HashMap中get操作
HashMap<String, String> map = new HashMap<>();
map.put("key1", "1");
String key1 = map.get("key1");
get方法源码
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
getNode()方法取值,如果为null,则返回null,否则返回value
getNode()方法源码
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 这里先判断数组不为空并且数组长度大于0
// (n - 1) & hash 这个还是计算下标,并且判断该数组下标位置的值不为null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
// 判断是否为红黑树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 使用 do while 循环去取值
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HashMap 的 get 操作
- 先根据key的hash值计算数组的下标;
- 根据计算得到的数组下标访问数组元素,如果数组元素为null则返回null;
- 如果数组元素不为null,则遍历该数组元素单向链表的每个节点,如果某个节点的key与当前key相等,则把该节点的值返回;
- 如果某个节点的key与当前所有key都不相等,则返回null;
- 如果长度大于threshold(12),则进行扩容。