参考文献:
Java 8系列之重新认识HashMap(美团技术团队)
HashMap源码解析JDK1.8
(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
对于上述四种Map类型的类,要求映射中的key是不可变对象。不可变对象是该对象在创建后它的哈希值不会被改变。如果对象的哈希值发生变化,Map对象很可能就定位不到映射的位置了。
通过上面的比较,我们知道了HashMap是Java的Map家族中一个普通成员,鉴于它可以满足大多数场景的使用条件,所以是使用频度最高的一个。
HashMap底层数据结构
- JDK1.8:数组+链表+红黑树
HashMap部分源码分析
部分成员变量
/**
* The default initial capacity - MUST be a power of two.
* 默认的初始容量2^4-必须是2的幂。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
* 最大容量,如果更高的值是由任何一个带有参数的构造函数隐式指定的,则使用该值。
* 必须是2的幂<= 1<<30(2^30)。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 在构造函数中没有指定时使用的负载因子。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 使用树而不是列表的容器计数阈值。当向至少有这么多节点的bin中添加元素时,bin将被转换为树。
* 该值必须大于2,并且应该至少为8,以便与树木移除时关于收缩后转换回普通桶的假设相吻合。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
* 可以对容器进行treeified的最小table容量。(否则,如果一个bin中有太多节点,就会重新调整表的大小。)
* 至少4 * TREEIFY_THRESHOLD,以避免调整大小和treeification阀值之间的冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
* 第一次使用时初始化,并根据需要调整大小。当分配时,长度总是2的幂。
*/
transient Node<K,V>[] table;
/**
* The next size value at which to resize (capacity * load factor).
* 要调整大小的下一个大小值(容量*负载因子)。
*/
int threshold;
静态内部类
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;
}
}
1、put()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//1、如果table数组还没有初始化,先进行初始化扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//i = (n - 1) & hash:根据key获取到当前元素在数组中的位置
//2、如果该位置的元素为null,用当前元素创建一个新的节点,并放在当前位置
if ((p = tab[i = (n - 1) & hash]) == null)
//tab[2] = newNode(hash, "杨过", "小龙女", null)
tab[i] = newNode(hash, key, value, null);
//3、如果该位置已有元素存在,上面最近的if判断已经让p指向当前元素
else {
//这个节点e,会在下面的判断中进行赋值
Node<K,V> e;
//这里声明的k会被赋值为当前位置元素的key
K k;
//3.1key存在,即:hash值相等,同时满足==或者equals为true
//key相等就将当前元素赋值给e
if (p.hash == hash &&
//当前位置元素的key == 添加元素的key || key != null && key.equals(k)
//即:== 或者 equals
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//e被赋值为当前位置的元素
//3.2如果当前位置的元素是TreeNode的实例,即是一个红黑树节点
else if (p instanceof TreeNode)
//将添加元素添加到红黑树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//3.3当前元素时链表节点,添加元素到链表中
else {
//从当前位置遍历链表,直到链表末尾,或者key相同
for (int binCount = 0; ; ++binCount) {
//3.3.1 e指向p.next,判断是否为null(这里将p、e可以看作双指针)
if ((e = p.next) == null) {
//创建新的节点,将添加元素添加到当前位置元素的末尾
p.next = newNode(hash, key, value, null);
//如果链表的长度大于等于8,则链表转为红黑树, -1 是 1st
if (binCount >= TREEIFY_THRESHOLD - 1){
treeifyBin(tab, hash);
}
break;
}
//3.3.2如果当前位置的next不为null(e不为null),判断next位置元素和添加元素的key
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
//如果key相同就break,跳出循环
break;
}
//将p指向e,继续执行循环
p = e;
}
}
//key存在(e不为null,在上面已经进行了赋值),用新的value替换旧的value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}
}
/**
* 成员变量 modCount 的注释解释:
* 表示此HashMap在结构上修改的次数。
* 结构修改是指改变HashMap中映射的数量,或者修改其内部结构(例如,重新散列)。
* 此字段用于使HashMap的集合视图上的迭代器快速失效。(见ConcurrentModificationException)。
*/
++modCount;
//当前map中元素的个数是否大于扩容的阈值,进行扩容
if (++size > threshold){
resize();
}
afterNodeInsertion(evict);
return null;
}
2、hash()
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
根据key计算元素要添加到数组中的哪个位置上
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("杨过", "小龙女");
int hashCode = "杨过".hashCode();
System.out.println("key的hashCode值:" + hashCode);
int hash = hashCode ^ (hashCode >>> 16);
System.out.println("hashCode位运算后:" + hash);
System.out.println("hash % 16:" + hash % 16);
System.out.println("hash & 15:" + (hash & 15));
}
当数组的长度是2的幂的时候,hash % length
的值和 hash & length-1
的结果是相同的,但是&运算更快,为了能够采用后者的运算方式,所以table数组的长度必须要是2的幂
3、resize()
final Node<K,V>[] resize() {
// 旧table数组
Node<K,V>[] oldTab = table;
// 旧数组的长度(调用无参构造器初始化时,oldTab为null,旧长度为0)
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧扩容阈值(临界值),初始值为0
int oldThr = threshold;
// 新的长度,新的阈值
int newCap, newThr = 0;
// 1、旧数组长度大于 0
if (oldCap > 0) {
// 1.1、如果旧数组长度大于等于最大容量(1 << 30)
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
// 返回旧数组长度,此时并没有改变数组的长度,只是修改了扩容的阈值
return oldTab;
}
// 1.2、新长度为旧长度左移1位,即:扩大2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY){
// 如果此时新长度小于最大容量,并且旧长度大于等于初始化容量,则新阈值等于旧阈值的2倍
newThr = oldThr << 1; // double threshold
}
}
// 2、如果旧数组长度为 0,看就阈值是否大于 0
// 当调用两个参数的构造函数的时候会执行:this.threshold = tableSizeFor(initialCapacity),此时table数组还未初始化
else if (oldThr > 0){ // initial capacity was placed in threshold
// 如果旧阈值大于 0,则新的长度为旧阈值
newCap = oldThr;
}
// 3、当旧数组长度和旧阈值都为 0 时,新长度和新阈值使用默认值
else {
// 新长度为默认值16
newCap = DEFAULT_INITIAL_CAPACITY;
// 新的阈值为默认长度 * 扩容因子(16 * 0.75 = 12)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的阈值为 0,(经过上面的判断后,为什么会是 0呢?)
if (newThr == 0) {
// 根据新数组长度计算阈值
float ft = (float)newCap * loadFactor;
// 如果数组的新长度和计算阈值都小于最大容量,则新阈值就为计算出来的阈值,否则为Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
}
// 成员变量阈值赋值为新的阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 根据新的数组长度创建Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 成员变量table数组指向新的数组
table = newTab;
//初始化的时候(oldTab == null),直接返回长度为 16的table数组
//不是初始化的时候(oldTab != null),扩容后需要将就数组里的元素移动到新数组中
if (oldTab != null) {
// 循环旧数组
for (int j = 0; j < oldCap; ++j) {
// 声明一个局部变量来保存当前循环到的节点
Node<K,V> e;
// 当前位置元素节点不为null时
if ((e = oldTab[j]) != null) {
// 将当前位置的元素赋值给e之后,将当前位置设置为null
oldTab[j] = null;
// 当前位置只有一个节点
if (e.next == null){
// 确定当前元素在新数组中的位置:e.hash & (newCap - 1)
newTab[e.hash & (newCap - 1)] = e;
}
// 如果当前位置节点是红黑树的根节点
else if (e instanceof TreeNode) {
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
}
// 当前位置节点是链表的头结点,且链表至少有两个节点
else { // preserve order
// 不需要移动位置的节点构成的链表的头,尾节点
Node<K,V> loHead = null, loTail = null;
// 需要移动位置的节点构成的链表的头,尾节点
Node<K,V> hiHead = null, hiTail = null;
// 遍历辅助节点
Node<K,V> next;
// do,while循环当前链表
do {
// 存放当前节点在原数组中的next节点,用来循环链表
next = e.next;
// 当前元素的hash值与上原数组长度为 0时,不需要移动位置
if ((e.hash & oldCap) == 0) {
// 尾节点为null时,头结点为当前节点
if (loTail == null) {
loHead = e;
}
// 尾节点不为null的时候,当前节点放到尾节点后面
else {
loTail.next = e;
}
// 尾节点更新为当前节点
loTail = e;
}
// 当前元素的hash值与上原数组长度不为0时,需要移动位置
else {
// 操作同上
if (hiTail == null) {
hiHead = e;
}
else {
hiTail.next = e;
}
hiTail = e;
}
} while ((e = next) != null); // 当e.next = null的时候退出循环
// 跳出循环之后,形成了两条链表,这一条是头节点在新数组中下标位置不变的
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 这一条链表的头节点在新链表中下标为当前下标加上旧数组长度的
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
对于原数组中当前位置为链表的桶来说:当前桶的链表将被分成两个链表,一个链表的头结点在新数组中的下标和在原数组中的下标相同,另一个链表的头节点在新数组中的下标则等于原数组下标加上原数组的长度。区分当前桶链表中节点应该被分在哪个链表是通过e.hash & oldCap) == 0
来判断的,等于0的位置就不变,其他的就加上原数组的长度
关于红黑树节点的复制操作后续再研究
4、get()
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n;
K k;
//根据key获取hash值找到桶的位置
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 {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
HashMap 和 Hashtable 的区别
- 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的;Hashtable 内部的方法基本都经过synchronized 修饰
- 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它
- 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 Hashtable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
- 初始容量和扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16,之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小
- 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。