简介
本篇介绍基于JDK1.8的HashMap的数据结构以及主要方法的源码。
HashMap基于散列表,继承了AbstractMap,实现了Map接口,存储的是无序的键值对,键唯一值可以相同,可以存储null键和null值。其内部是通过维护一个链表红黑树数组来实现全部功能,链表长度大于8时会转变为红黑树。
成员变量
下面是HashMap的几个主要成员变量。
//HashMap即由此可以存储键值对的链表的数组实现
transient Node<K,V>[] table;
//返回键值对的Set集合
transient Set<Map.Entry<K,V>> entrySet;
//键值对的数量
transient int size;
//用于快速失败机制
transient int modCount;
//阀值,size大于此阀值时扩容
int threshold;
//负载系数,默认为0.75
final float loadFactor;
在这几个成员变量中threshold和loadFactor较重要因此需要详细介绍一下。
- loadFactor:加载因子,表示散列表空间的使用度。加载因子越大,空间使用度越高,但查找效率越低,性能也就越低;加载因子越小,空间使用度越低,空间浪费越多,扩容次数越多性能也越低。默认值为0.75,这也是一个折中值,更大或更小都会影响性能。
- threshold:阀值,值=HashMap容量 x 加载因子。
HashMap的数据结构
一个一般的HashMap的数据结构可能类似下图:
HashMap中的链表和红黑树实现
HashMap的链表和红黑树分别是通过其内部类Node类和TreeNode类实现,Node类实现如下:
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;
}
}
TreeNode类的方法较多,此处我们只看类声明和成员变量。
//LinkedHashMap.Entry继承了HashMap.Node类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//当前节点的父节点
TreeNode<K,V> parent;
//当前节点的左节点
TreeNode<K,V> left;
//当前节点的右节点
TreeNode<K,V> right;
//前一个节点
TreeNode<K,V> prev;
//是否是红色节点
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
主要方法源码
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) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
//table为null或其长度为0则执行扩容方法
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算键值对要存储在哪个桶中,如果桶为null则将键值对直接存储在数组中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//上一步计算的如果桶不为null,说明可能存在hash冲突。
//如果key已经存在,则得到该Node对象的引用赋值给e;
//如果key不存在,则直接根据要存储的键值对创建Node对象并加到对应数据结构中。
else {
HashMap.Node<K,V> e; K k;
//如果(桶中第一个元素的hash值与要存储的键的hash值相等且键也相等)
// 或(存储的键不为null且键与桶中第一个元素的键值相同),则此Node对象就是我们想要的e
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断桶结构如果是红黑树则遍历红黑树
else if (p instanceof HashMap.TreeNode)
e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果不是红黑树那这个桶结构就是链表了
else {
//遍历链表
for (int binCount = 0; ; ++binCount) {
//key不存在则直接创建此键值对的Node对象并添加到链表中
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
//此方法中判断桶数量如果小于64则执行扩容方法,将桶转换为红黑树结构
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e不为空表示key是存在的,直接替换value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//键值对数量大于阀值时执行扩容方法
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
注释写的较乱,可以结合下面的流程图。
get()
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final HashMap.Node<K,V> getNode(int hash, Object key) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
//table为null或数组长度小于等于0或根据key计算的桶的第一个元素为null则返回null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//桶的第一个元素即我们想要的,直接返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//桶是红黑树结构则遍历红黑树
if (first instanceof HashMap.TreeNode)
return ((HashMap.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;
}
get方法较简单,其流程图如下。
resize()
final HashMap.Node<K,V>[] resize() {
HashMap.Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//桶数量达到最大值将阀值设置为int最大值,这样以后不会执行扩容操作
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容为原来的两倍,新的桶数量小于最大桶数量且旧的桶数量大于等于16则阀值也变为原来的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
table = newTab;
if (oldTab != null) {
//遍历旧table
for (int j = 0; j < oldCap; ++j) {
HashMap.Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//桶中只有一个元素则直接计算桶下标放入新table
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof HashMap.TreeNode)
((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
HashMap.Node<K,V> loHead = null, loTail = null;
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.Node<K,V> next;
//遍历链表,重新构造两个链表
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//把两个链表放到对应桶中
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
以上代码主要做了两件事:1.将数组扩容为原来的两倍。2.将旧数组的元素重新计算桶下标后放到新数组中。我们重点关注第二点,以上代码中遍历旧的table数组的流程图如下:
流程图包含了一个for循环和一个do while循环。可以看到结构为链表的桶扩容后在新数组的桶的下标的重点在于上面流程图的右下角,取决于e.hash&oldTab.length是否等于0。
HashMap的长度一定是2的N次幂,如果有个HashMap的长度为64,二进制为1000000。对于表达式e.hash&oldTab.length来说,其结果取决于e.hash的二进制的第7位是否为1。为1时,表达式结果为64;为0时,表达式结果为0。因此,在扩容后,桶中的元素在新数组中或者处于原位置,或者在原位置的基础上再移动旧数组长度的位置。
HashMap的遍历
HashMap的遍历有以下多种方式,可以根据需要自行选择。
- 使用HashMap的entrySet()方法获得Map.Entry的Set集合
public static void main(String[] args) {
Map<String,String> map = new HashMap<>();
map.put("name","charles");
map.put("age","18");
map.put("gender","1");
Set<Map.Entry<String,String>> entrySet = map.entrySet();
for (Map.Entry<String,String> entry: entrySet){
System.out.println("key="+entry.getKey()+",value="+entry.getValue());
}
}
- 使用keySet()方法获得key的Set集合,再get到value
Set<String> keySet = map.keySet();
for (String key:keySet){
System.out.println("key="+key+",value="+map.get(key));
}
- 使用values()方法获得value的的集合
Collection<String> valueColl = map.values();
for(String value:valueColl){
System.out.println("value="+value);
}
结尾
本文到这里就结束了,感谢看到最后的朋友,都看到最后了,点个赞再走啊,如有不对之处还请多多指正。
网友评论