简介
HashMap 一直是面试考察的重点,在JDK1.8中,HashMap的底层进行了优化,引入了红黑树的数据结构,并且对扩容机制进行了优化。本文将讲解 HashMap中的Node,resize(),以及put方法;
继承关系
HashMap继承自接口 java.util.Map,同样继承自该接口的还有Hashtable(遗留类,且线程安全)、LinkedHashMap(HashMap的子类,可以保存插入顺序)和TreeMap(可以根据存入的的键值进行排序)。
HashMap中的关键字段
int threshold;
final float loadFactor;
int modCount;
int size;
threshold:键值对的极限容量
size:HashMap中现存键值对的数量
loadFactor:负载因子,默认值为0.75
modCount:记录当前集合被修改的次数。
HashMap的默认初始化长度 Node[ ] = 16;
这几个值涉及到HashMap的扩容机制,即 threshold = LoadFactor * Node[].length;
也就是说,Node[ ]的长度越大,LoadFactor越大,那么所能容纳的键值对就越多。
如果存储的键值对超出了这个数据,就会调用resize()方法进行扩容。
Node与put
Node的jdk1.8源码:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;// 用来定位数组的位置
final K key;
V value;
Node<K,V> next; // 链表结构,指向下一个node
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;
}
}
Node[是HashMap的内部类,该内部类实现了Map.Entry接口。
运行put方法后系统执行的操作
map.put(“key”,“value”);
1.系统将调用”key“的hashcode方法,得到他的hashcode值(这也是每个类都必须定义hashcode方法的原因),然后通过Hash算法的高位运算与取模运算来得到该键值对存储的数组下标。
2.如果两个“key”定位到了相同的存储位置,此时就会判断二者的key值是否相同(equal()方法);
3.若相同,则覆盖”key“对应的value值。
若仍不相同,在jdk8中,就会判断这个map是否已经转化为红黑树结构,如果时,就直接插入数值
4.若还不是,就说明此时Node数组还是链表结构,那么就会遍历数组,判断链表的长度是否大于8,如果大于8,就将其转化为红黑树。小于8,则进行插入操作,如果在遍历过程中发现key已经存在,就直接将对应的value值覆盖。
5.插入成功后,还要判断map的size值是否超过了最大容量threshold,如果超过,就会调用resize()方法进行扩容,扩容的默认长度是初始值的两倍。
resize()扩容机制
当HashMap中的size超过threshold时,就会调用这个方法。
1 void resize(int newCapacity) { //新的容量
2 Entry[] oldTable = table; //存放之前的entry数组
3 int oldCapacity = oldTable.length;
4 if (oldCapacity == MAXIMUM_CAPACITY) {
5 threshold = Integer.MAX_VALUE;
6 return;
7 }
8
9 Entry[] newTable = new Entry[newCapacity]; //初始化新的 Entry 数组
10 transfer(newTable); //将数据转移到新的Entry数组里
11 table = newTable; //HashMap的table属性引用新的Entry数组
12 threshold = (int)(newCapacity * loadFactor);//修改threshold的阈值
13 }
此处源码为JDK1.7的源码,因为JDK8引入了新的红黑树过于复杂。此处红黑树暂且不谈。
其中的transfer()方法如下:
void transfer(Entry[] newTable) {
2 Entry[] src = table;
3 int newCapacity = newTable.length;
4 for (int j = 0; j < src.length; j++) {
5 Entry<K,V> e = src[j];
6 if (e != null) {
7 src[j] = null;
8 do {
9 Entry<K,V> next = e.next;
10 int i = indexFor(e.hash, newCapacity);
11 e.next = newTable[i];
12 newTable[i] = e;
13 e = next;
14 } while (e != null);
15 }
16 }
17 }
具体过程为
1.先把旧的table数组传入src之中,再遍历src中的每一个值,将其取出。
2.如果src[j]的值不为空,就将src[j]的引用置为空,释放空间。
3.indexfor()方法,用于重新计算每个值在数组中的位置。
4.将旧链表的元素转移到新的链表之中,此处使用的是头插法。jdk1.8中则采用了尾插法。
JDK1.7 和 1.8的不同
1.底层,JDK1.7的底层采用了数组+链表的形式,JDK1.8则采用了数组+链表+红黑树的形式。
2.效率,通过底层的改进,在Hash分布均衡的情况下相差不大,但是在Hash分布不平衡的情况下,越不平衡,JDK1.8的红黑树所带来的效率提升就越明显。