官方文档说明
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null值和 null键。除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
HashMap 的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
通常,默认加载因子 (.75) 在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。
数据结构图
如下图所示:
从上图可以看到,每个桶中的节点是按照链表的方式排列的。
模拟数据
假设映射结构为《Integer,String》,并且n=4,也就是hash桶的数量是4.我们模拟一下put方法的过程。
- 1:我们放入第一组映射<0,a>。对应桶的算法是(n-1)&hash(key),也就是3&hash(0) = 3&0 = 0.也就是说此映射会被创建成一个节点(Node)对象,然后放到图中0的位置。
- 2:我们放入第二组映射<1,b>。对应桶的算法是(n-1)&hash(key),也就是3&hash(1) = 3&1 = 1.也就是说此映射会被创建成一个节点(Node)对象,然后放到图中1的位置。
- 3:我们放入第三组映射<2,b>。对应桶的算法是(n-1)&hash(key),也就是3&hash(2) = 3&2 = 2.也就是说此映射会被创建成一个节点(Node)对象,然后放到图中2的位置。
- 4:我们放入第四组映射<3,b>。对应桶的算法是(n-1)&hash(key),也就是3&hash(3) = 3&3 = 3.也就是说此映射会被创建成一个节点(Node)对象,然后放到图中3的位置。
- 5:我们放入第五组映射<4,b>。对应桶的算法是(n-1)&hash(key),也就是3&hash(4) = 3&4 = 0.也就是说此映射会被创建成一个节点(Node)对象,然后放到图中0的位置,又因为0的位置不为null,然后调用equals方法判断两个key是否相同,如果相同则替换旧的节点,如果不相同则放到01的位置。那么很明显4和0不相同,会放到01的位置。
- 6:一次类推。
get和remove方法,同样是采用hash和equals方法来定位映射位置,然后采取相应的操作。
数据结构变化
上文说HashMap不保证该顺序恒久不变,但是我们看数据结构图,如果遍历此HashMap,就是从上到下,从左到右依次遍历,顺序更改是固定的啊!原因如下:
如果hash算法不能很好的把数据分散到各个桶中,那么其中某一个桶中的链表就可能会很大。这个时候,put,get,remove操作都要遍历整个链表,性能就会所以HashMap的实现对此进行了优化。如果桶的数量大于64,且某个桶的链表的长度大于8,那么就把这个桶中的数据结构就会重构为一颗红黑二叉树。数据结构就会变为如下所示:
所以如果桶2的数据太多了话,就会重构成一颗树,那么数据的顺序就变化了,所以说说HashMap不保证该顺序恒久不变。
重要源码查看
节点
链表节点
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;
}
}
树节点
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);
}
}
成员变量
transient Node<K,V>[] table;//hash结构
int threshold;//重构临界值,此值是根据容量和加载因子计算得到
final float loadFactor;//加载因子
构造函数
public HashMap(int initialCapacity, float loadFactor) {
//此处省略无关代码段
......
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
rehash
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//省略无关代码
......
if (++size > threshold)
resize();//rehash
afterNodeInsertion(evict);
return null;
}
rehash操作会使table的容量几乎翻倍。
结构变化
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//无关代码
......
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);//指定的hash桶,变为树结构
break;
}
}
//无关代码
......
return null;
}