Map使我们在开发过程中经常使用的一个数据结构,其主要有3个特点:键值对key-value、键key唯一且不可重复,每个键key都能且只能映射到一个value值。
HashMap又是Map中最常被使用的一个实现类,除了Map的3个基础特点外还具备无序性、key和value都可以为null、非线程安全等特点。
HashMap由数组组成,每个数组的元素都是一条链表,在jdk1.8中当链表超出一定长度会树化成一颗红黑树。为了保证HashMap的分布尽量均衡分散,HashMap采用了hash值与容量-1(即数组长度-1)进行按位与运算来实现取余的效果,从而确定元素在数组中的下标。而为了解决散列冲突,HashMap采用了拉链法进行处理,相同下标的元素组成链表放在数组中,当链表长度过长,又会树化成红黑树以提高存取效率。同时,当元素数量大于容量*负载因子时,HashMap会进行扩容,将容量变为原有的两倍。
接下来我们基于jdk1.8的源码对HashMap进行分析。
一. 主要的成员变量
- 静态成员变量
默认的初始容量:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16`
最大容量:static final int MAXIMUM_CAPACITY = 1 << 30;
默认的负载因子: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; - 普通成员变量
由链表的头结点组成的数组:transient Node<K,V>[] table;
key-value的元素集:transient Set<Map.Entry<K,V>> entrySet;
当前的元素数量:transient int size;
修改次数:transient int modCount;
扩容的阈值:int threshold;
负载因子:final float loadFactor;
二. 主要的成员类
- Node<K, V>类,实现了Map.Entry<K, V>接口
Node类表示HashMap中的每一个节点,由hash值、key值、value值和指向下一个Node的next组成。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//省略其它方法
}
- TreeNode<K, V>类,继承了LinkedHashMap.Entry<K, V>类
LinkedHashMap.Entry<K, V>类继承了Node<K, V>类,相比Node类多了指向前一个LinkedHashMap.Entry和后一个LinkedHashMap.Entry的before和after属性。
TreeNode<K, V>类又继承了LinkedHashMap.Entry<K, V>类,相比LinkedHashMap.Entry类多了指向父TreeNode、左儿子TreeNode、右儿子TreeNode和前一个TreeNode的parent、left、right、prev属性,以及表示是节点颜色的布尔型的red属性。
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;
//省略其它方法
}
三. 基础方法
在开始分析HashMap的主要方法之前先分析一下HashMap中被广泛使用的基础方法。
- tableSizeFor方法
HashMap采用了hash值与容量-1(即数组长度-1)进行按位与运算来实现取余的效果,从而确定元素在数组中的下标,因此要求容量值必须是2的次幂。tableSizeFor实现了根据参数获取到一个不小于参数的2的次幂的值的效果。
tableSizeFor采用了位运算来实现效果,位运算在CPU即可处理,运行速度更快。
对于n |= n >>> 1运算,由于n的最左一位必然是1,n>>>1的的最左一位也是1,且长度比n少一位,因此n | n >>> 1的最左边两位必然都是1,如假设n是一个6位数,设为1xxxxx, 1xxxxx | 1xxxx = 11xxxx,后续步骤同理,算到 n |= n >>> 16保证了32位数都由1组成。
由于MAXIMUM_CAPACITY = 1 << 30,因此保证了n每一位都是1,最后在进行n+1运算即保证了结果是一个2的次幂。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
- hash方法
hash方法用于获取key值的hash值,由key值的hashCode的高16位与低16位进行异或运算得到,以保证在HashMap中的元素分布尽量均衡。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
三. 构造方法
HashMap的构造方法由4种,分别如下:
- 无参构造器
无参构造器最简单,负载因子的值取默认值0.75。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 带容量和负载因子参数的构造方法
对初始容量做校验,小于零时抛出异常,大于最大容量时取最大容量,然后分别进行赋值。
注意,HashMap并没有存储容量capacity的成员变量,也不会立即新建一个对应长度的数组,此时传入的初始容量根据tableSizeFor方法取到合适的容量值后存储在扩容阈值threshold中。
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initi