HashMap数据结构
哈希表结构(链表散列:数组+链表+红黑树)实现,结合数组和链表的优点。当数组总容量超过64,链表长度超过8时,链表会转为红黑树。
transient Node<K,V>[] table;
// 初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 扩容因子
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;
HashMap工作原理
HashMap底层通过hash数组和单向链表实现,数组中每个元素都是链表,由Node内部类(实现Map.Entry接口)实现,HashMap通过put、get方法存储和获取。
存储
- 存储对象时,将K/V(key-value)键值对传给put()方法;
- 计算hash(K)方法计算k的hash值,然后结合数组长度,计算得到数组下标;
- 调整数组大小(当容量中元素个数大于capacity*loadfactor时,容量会进行扩容resize为2n);
如果K的hash值在HashMap中不存在,则插入,若存在,则发生碰撞;如果K的hash值在HashMap中存在,且两者equals()返回true,则更新键值对;如果K的hash值在HashMap中存在,且两者equals()返回false,则插入链表(jdk7头插法,jdk8尾插法)或者红黑树中(树的添加方式)。
put()方法
判断键值对数组table[i]是否为空或者null,否则执行resize()进行扩容;
- 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向6,如果table[i]不为空,转向3;
- 判断table[i]的首个元素是否和key一样,如果相同(这里的相同指hashcode相同,equals()相同)直接覆盖value,否则转向4;
- 判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是,则直接在书中叶子节点插入键值对,否则转向5;
- 遍历table[i],判断链表长度是否大于8,大于8且HashMap元素总量大于64时把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
- 插入成功后,判断实际存在的键值对size是否超过了最大容量threshold,如果超过,进行扩容。 ```java public V put(K key, V value) { // 对key的hashcode()做hash操作 return putVal(hash(key), key, value, false, true); }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node
<a name="xjYuY"></a>
#### 扩容机制
```java
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// oldCap表示为当前table的元素个数,默认长度16,扩容:2n
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 超过扩容最大值,就不在扩容,只好让它去碰撞static final int MAXIMUM_CAPACITY = 1 << 30;
// @Native public static final int MAX_VALUE = 0x7fffffff; 2^31-1
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//没超过最大值,扩充为原来两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//计算行的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每个bucket桶元素都移动到新的bucket桶元素上。
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order 链表优化重hash的代码块
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//原索引
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//原索引+oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//新索引= 原索引+oldCap放到bucket里
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
获取
- 获取对象时,将K传给get()方法;
- 调用hash(K)方法(计算K的hash值)从而获取该键值K所在的链表的数组下标;
- 顺序遍历链表,equals()方法查询相同Node链表中K对象的V值;
hashcode是定位的,存储位置,equals是定性的,比较两者是否相等。
两个对象的hashcode相同会发生什么
如果hashcode值相同,值不一定就是相同的,还需要使用equals()方法比较,所以两个对象所在数组的下标相同,就会发生“碰撞”,又因为HashMap采用链表存储对象,这个Node会存储到链表中。
红黑树
- 根节点是黑色
- 每个节点非红即黑
- 如果节点是红色,则子节点一定是黑色(反之不一定)
- 每个叶子节点都是黑色的空节点(NIL节点)
从根节点到叶子结点或空节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
HashMap、LinkedHashMap、TreeMap区别
HashMap使用数组+链表+红黑树保存数据;使用场景:在Map中插入、删除和定位元素的情况下;
LinkedHashMap保存记录的插入顺序,使用Iterator遍历时,先取到的记录肯定是先插入的,遍历比HashMap慢;使用场景:在需要输出的顺序和输入的顺序相同的情况下;
TreeMap实现SortMap接口,能够把它保存的记录根据键顺序(默认按键顺序升序排序,也可以指定排序的比较器);使用场景:需要按自然顺序或自定义顺序遍历键的情况下;HashMap和HashTable区别
HashMap线程不安全,HashTable线程安全;
- 由于线程不安全,HashMap效率比HashTable效率高;
- HashMap只允许一条记录的键为null,允许多条记录的值为null,而HashTable不允许;
- HashMap默认初始容量为16,扩容为原来两倍,HashTable默认初始容量为11,扩容为原来两倍+1;
- HashMap需要重新计算K的hash值,而HashTable直接使用K的hashcode值。
解决hash冲突的方法
- 开放地址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
- 再哈希法
- 链地址法(HashMap解决方法)
简单来说,就是数组+链表的结合,在每个数组元素上都加一个链表结构,当数据被hash后,得到数组下标,把数据放在对应下标元素的链表上。
- 建立一个公共溢出区