Java容器篇 HashMap
面试重灾区了快,还是自己好好梳理下
序章
- HashMap基本上已经是现在面试的重灾区了,基本上面试都会问一个底朝天,那么就用它来开容器篇的头吧。
- HashMap本质上一个K-V键值对的存储工具,但是和其他容器不同的地方在于,哈希表的这种结构它的增删改查操作的时间复杂度都是O(1)的
- HashMap对应在算法领域的模型叫做哈希表,那为啥它不叫HashTable咧,因为JDK1.0时代就已经把这种K-V哈希表给设计出来了,而且名字就叫HashTable,那么为啥现在我们都不用HashTable呢,很简单,因为它所有的方法都加上了synchronized!线程安全但是它比较慢呐。因此HashMap的诞生就是一个不保证线程安全但是性能好,查询快的这么一个工具容器
数据结构
- 本质上HashMap的设计中,就是一个数组+链表的结构,JDK1.8开始,在链表的基础上引入了红黑树是为了解决链表深度过深的时候,查询效率不高的问题
JDK1.7(数组+链表)
JDK1.8(数组+链表+红黑树)
重要参数
参数名称 | 作用 | 1.7默认 | 1.8默认 |
---|---|---|---|
DEFAULT_INITIAL_CAPACITY | 默认数组大小 | 24 | 24 |
MAXIMUM_CAPACITY | 数组容量上限 | 230 | 230 |
DEFAULT_LOAD_FACTOR | 负载因子,计算扩容阈值使用 | 0.75 | 0.75 |
TREEIFY_THRESHOLD | 链表转树的阈值 | 无此参数 | 8 |
UNTREEIFY_THRESHOLD | 树转链表的阈值 | 无此参数 | 6 |
MIN_TREEIFY_CAPACITY | 转树最小的容量 | 无此参数 | 64 |
table | 持有当前HashMap对象的数组引用 | ||
threshold | 当前数据大小*负载因子算出的真实扩容阈值 | ||
loadFactor | 指定的负载因子 |
重要内部类
键值的真正存储对象,在1.7和1.8里取了不一样的名字,一个叫Entry,一个叫Node,但是没有什么本质区别
Entry<K,V> (1.7版)和Node<K,V>(1.8版本)
属性 | 1.7 | 1.8 |
---|---|---|
键 | final K key | final K key |
值 | V value | V value |
后续节点 | Entry<K, V> next | Node<K,V> next |
哈希值 | int hash | final int hash |
Entry构造方法1.7
Entry (int h, K k, V v, Entry<K, V> n) {
value = v;
next = n;
key = k;
hash = h;
}
Node构造方法1.8
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
TreeNode(1.8独有)
因为1.8引入了红黑树,因此设计了TreeNode作为红黑树的节点,继承自 LinkedHashMap.Entry<K,V>,然后它又继承自HashMap.Node,也就是说hashMap的TreeNode继承的就是自己的Node
TreeNode持有的属性有
- TreeNode<K,V> parent
- TreeNode<K,V> left
- TreeNode<K,V> right
- TreeNode<K,V> prev
- boolean red
扩容算法
resize() 1.7
JDK1.7是直接创建一个两倍大小数组,然后一个个的头插到新数组的新索引内
JDK1.8因为涉及到了树的操作,因此要分两种情况看
- 如果当前节点下面挂的是链表,那么分拆之后,新索引下挂的也一定是链表,所以只要按链表结构组织即可
- 如果当前节点下面挂的是树,则在分装完成的,新索引下的树节点数据是否低于6,如果少于6,就触发变回链表的操作
- JDK1.7
void resize (int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
//直接不扩容了
return;
}
//创建一个两倍大小的Entry数组
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
/**
* Transfers all entries from current table to newTable.
*/
void transfer (Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//当前的table还持有着扩容前数组引用
for (Entry<K, V> e : table) {
while (null != e) {
Entry<K, V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//这里是标准的头插使用
//当前Entry的next指向当前的对应索引的Entry
e.next = newTable[i];
//当前对应索引的Entry改为当前循环的Entry
newTable[i] = e;
//指针下称
e = next;
}
}
}
- JDK1.8
final Node<K, V>[