HashMap底层原理(含面试题)
HashMap
以下是基于JDK1.8
1. HashMap结构图
2. HashMap中定义的一些成员变量
// hashMap数组的初始容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 负载因子 0.75f;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树形化阈值 8
static final int TREEIFY_THRESHOLD = 8;
// 解除树形化阈值 6
static final int UNTREEIFY_THRESHOLD = 6;
// 树形化的另一条件 Map数组的长度阈值 64
static final int MIN_TREEIFY_CAPACITY = 64
// 这个就是hashMap的内部数组了,而Node则是链表节点对象。
transient Node<K,V>[] table;
// 数组扩容阈值。
int threshold;
- DEFAULT_INITIAL_CAPACITY:数组的初始容量为16。可以在构造方法中指定。必须是2的幂次方。(16->32->64…)
- DEFAULT_LOAD_FACTOR:加载因子0.75f。所谓的加载因子就是HashMap的容量达到0.75时的时候会试试扩容resize(),(例:假设有一个HashMap的初始容量为16,那么扩容的阈值就是0.75*16=12。也就是说,在你打算存入第13个值的时候,HashMap会先执行扩容)。加载因子也能通过构造方法进行指定,如果指定大于1,则数组不会扩容,牺牲了性能不过提升了内存。
- TREEIFY_THRESHOLD:树形化阈值。当链表的节点个数大于等于这个值时,会将链表转化为红黑树。
- UNTREEIFY_THRESHOLD:解除树形化阈值。当链表的节点个数小于等于这个值时,会将红黑树转换成普通的链表。
- MIN_TREEIFY_CAPACITY:树形化阈值的第二条件。当数组的长度小于这个值时,就算树形化阈达标,链表也不会转化为红黑树,而是优先扩容数组resize()。
- threshold:数组扩容阈值。即:HashMap数组总容量乘以加载因子。当前容量大于或等于该值时会执行扩容resize()。扩容的容量为当前HashMap总容量的两倍。比如,当前HashMap的总容量为16,那么扩容之后为32。
3. 继承关系
// table 内部数组是节点类型
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; // 下一个节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() {
return key; }
public final V getValue() {
return value; }
public final String toString() {
return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
拉链法的散列表是通过链表解决碰撞问题的,所以HashMap的内部数组是节点类型。
hash值是经过hash()方法处理过的hashCode,也就是数组的索引bucket,为了使hashCode分布更加随机。
java.util.HashMap<K, V>.Node<K, V>
java.util.LinkedMap<K, V>.Entry<K, V>
java.util.HashMap<K, V>.TreeNOde<K, V>
TreeNode是Node的子类,继承关系如下:Node是单向链表节点,Entry是双向链表节点,TreeNode是红黑树节点。TreeNode的代码400多行都是写的红黑树。
4. 简单总结
HashMap是基于拉链法实现的一个散列表,内部由数组和链表和红黑树实现。
- 数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模运算(据说提升了5~8倍)。
- 数组是否需要扩充通过负载因子来判断,如果当前元素个数为数组容量的0.75,就会扩充数组。这个0.75就是默认的负载因子,可由构造方法传入。也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
- 为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能,这里是一个平衡点。
- 对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组。所以上面也说了链表长度的阈值是7或8,因为会有一次放弃转换的操作。
5. 深入了解源码
5.1 构造方法
// 默认数组初始容量为16,负载因子为0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 指定数组的初始容量
public HashMap(int initalCapacity) {
this(initalCapacity, D