继续挖掘 HashMap 背后的故事。
1. 特性
HashMap 是一个双列集合,存储的元素都是 key - value 形式的键值对,使用哈希表作为内部数据结构,不保证存入和取出元素的顺序,允许使用 null 键和 null 值,方法上没有使用 synchronized ,线程不安全,继承自 AbstractMap 类,实现了 Map 接口。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
// ...省略...
}
2. 内部数据结构
内部是一个 Node 数组,而数组的每一项又是一个单向链表,这个链表用来存储发生哈希冲突的元素,在 Java8 中,如果发生哈希冲突的次数超过了 8 次,就会将链表转化成一颗红黑树。
/**
* 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 的整数倍
* 在进行某些操作不需要该数组的时候,允许数组的长度为 0 ,以便于在虚拟机启动的时候节省内存
*/
transient Node<K,V>[] table;
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*
* HashMap 中所存储的元素的类型,可以看出我们存储的 key 和 value 都是作为这个 Node 类的两个属性
* 这个 Node 中由于有一个 next 属性用来指向发生哈希冲突的下一个节点,所以构成了一个单向链表
*/
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;
}
}
/**
* 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
*
* 一旦哈希冲突的次数达到了这个门限值(8),会将单向链表转化成一颗红黑树
* 遍历单链表的时间复杂度是O(n),而遍历一颗二叉树的时间复杂度是O(logn)
* 这就会将最坏的情况下 HashMap 的查找的时间复杂度从O(n)提高到O(logn)
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*
* 频繁的发生哈希冲突时所采用的红黑树的类型定义
*/
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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
3. 一些重要的属性
/**
* The default initial capacity - MUST be a power of two.
* HashMap 默认的容量,必须是 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.
* HashMap 中 Node 数组的大小最大是 2 的 30 次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 默认的装载因子,当 HashMap 中元素的个数已经达到了容量 * 0.75f 时,对哈希桶(形象的形容 Node 数组)进行扩容,容量是原来的 2 倍
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The load factor for the hash table.
*上面是默认的装载因子的大小,这个是实际值
* @serial
*/
final float loadFactor;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
* 存放的是键值对对象 Entry (其实 Node 节点实现了这个 Map.Entry 接口)
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 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.
*
* 将哈希桶的数据结构由红黑树装换成单向链表的条件:
* 哈希桶的中的节点树最大为6个的时候,将其装换成单向链表
*/
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.
* 如果哈希桶的数据结构要转换为红黑树,那么哈希桶最少的数量应达到这个值
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* The number of key-value mappings contained in this map.
*
* HashMap 中所存储的键值对的数量,并不是哈希桶的容量
*/
transient int size;
/**
* The next size value at which to resize (capacity * load factor).
* 当数组中下一个元素的个数达到这个门限值的时候,将会 rehash (重新调整数组的大小,并把原数组中的元素都存放到新数组中)
*
* 门限值 = 数组容量 * 装载因子的大小
* @serial
*/
int threshold;
4. HashTable 和 HashMap的区别
4.1 继承的父类不同
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
...
}
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
...
}
4.2 线程的安全性不同
Hashtable 中的方法是Synchronize的,而HashMap中的方法在默认的情况下是非Synchronize的。
例如:
/**
* Returns the number of keys in this hashtable.
*
* 这是 Hashtable 的 size 方法
* @return the number of keys in this hashtable.
*/
public synchronized int size() {
return count;
}
/**
* Returns the number of key-value mappings in this map.
*
* 这是 HashMap 的 size 方法
* @return the number of key-value mappings in this map
*/
public int size() {
return size;
}
4.3 是否提供 contains 方法
HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsValue 和 containsKey,因为 contains 方法容易让人引起误解。
Hashtable 则保留了 contains,containsValue 和 containsKey 三个方法,其中 contains 和 containsValue 功能相同。
/**
* 这是 HashTable 的 containsValue 方法,可以看出它调用的就是 contains 方法,但是这里并没有加 synchronized 同步,是因为...
*/
public boolean containsValue(Object value) {
return contains(value);
}
/**
* 是因为 contains 方法上已经加了同步
*
*/
public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
for (int i = tab.length ; i-- > 0 ;) {
for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
4.4 key 和 value 是否允许 null 值
其中 key 和 value 都是对象,并且不能包含重复 key,但可以包含重复的 value。
Hashtable 中,key 和 value 都不允许出现 null 值。
HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null 。当 get() 方法返回 null 值时,可能是 HashMap 中没有该键,也可能使该键所对应的值为 null 。因此,在 HashMap 中不能由 get() 方法来判断 HashMap 中是否存在某个键, 而应该用 containsKey() 方法来判断。
4.5 :哈希值的计算方法不同
Hashtable 直接使用的是对象的 hashCode ,而 HashMap 则是在对象的 hashCode 的基础上还进行了一些变化。
4.6 内部实现使用的数组初始化和扩容方式不同
HashTable 初始大小是11,而 HashMap 初始大小是16。
Hashtable 采用的是 2 * old + 1 ,而 HashMap 是 2 * old。
/**
* Constructs a new, empty hashtable with a default initial capacity (11)
* and load factor (0.75).
* 初始容量默认为 11 ,装载因子是 0.75
*/
public Hashtable() {
this(11, 0.75f);
}