1、什么是HashMap?
HashMap是Map集合的一个常用实现类,它是非线程安全的。它存储的是键值对(key-value),key必须唯一,不可重复,value可重复。它的底层实现是数组+链表。
2、HashMap的模型
了解HashMap的基本模型后,下面从源码解析HashMap。
3、HashMap类的继承关系
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {...}
其中,实现Serializable接口是为了使类可持久化,不是本文关注的重点,这里不多加赘述,想了解详细可参考这位大佬的文章:java类中serialVersionUID的作用;
Cloneable接口不包含任何方法,实现Cloneable接口仅仅是用来指示Object类中的clone()方法可以用来合法的进行克隆,想了解详细可参考:Cloneable接口及clone()。
4.Map接口
然后接下来是我们关注的重点——Map接口,它包含的方法如下:
public interface Map<K,V> {
int size();//返回Map容器的数据长度
boolean isEmpty();//返回容器是否为空
boolean containsKey(Object key);//验证Key是否存在
boolean containsValue(Object value);//验证Value是否存在
V get(Object key);//按Key获取键值对
V put(K key, V value);//向容器添加键值对
V remove(Object key);//按Key删除键值对
void putAll(Map<? extends K, ? extends V> m);//复制其他容器的数据到此
void clear();//清空容器
Set<K> keySet();//返回所有key的集合
Collection<V> values(); //返回所有values的集合
Set<Map.Entry<K, V>> entrySet();//返回所有键值对
interface Entry<K,V> {//内部接口
......
}
boolean equals(Object o);//对象相等判断
int hashCode();//hashCode值
default V replace(K key, V value) {//键值对替换
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
//链表节点
static class Node<K,V> implements Map.Entry<K,V> {...}
//红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {...}
......
}
Map接口大致地定义了一个Map容器该有的基本方法及实现规范:它存储键值对(Key-Value)类型的数据,put()添加数据,get()获取数据,size()查看长度,remove()删除元素,clear()清空容器。
5.HashMap的默认常量
HashMap类实现了Map接口,在它的基础上添加了一些自己独有的特性。先来看看源码中写在最前面的几个常量(源码中在每一个常量前面都会用一大段的英文解释这个常量的作用,并不难理解):
private static final long serialVersionUID = 362498820763181265L;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
第一个数据是:serialVersionUID = 362498820763181265L。
serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。
第二个数据是:DEFAULT_INITIAL_CAPACITY = 1 << 4(默认初始容量)。
当一个HashMap对象被创建时,如果没有在参数中指定它的大小,那它将被初始化为默认长度16。
第三个数据是:MAXIMUM_CAPACITY = 1 << 30(最大容量)。
一个HashMap容器的最大容量不能超过2^30(1,073,741,824)。
第四个数据是:DEFAULT_LOAD_FACTOR = 0.75f(默认负载因子)。
负载因子关乎到容器的扩容,HashMap的默认负载因子为0.75,即当容器中的数据数量达到容器容量的3/4时,容器将会进行扩容。
第五个数据是:TREEIFY_THRESHOLD = 8(树阈值)。
当相同位置的键值对数量增加到这个值后,它们的存储结构将由链表转化成红黑树,默认该值为8。源码中注释“该值必须大于2,并且至少应为8,以符合树木移除中关于在收缩时转换回普通垃圾箱的假设”。
第六个数据是:UNTREEIFY_THRESHOLD = 6(非树阈值)。
当相同位置的键值对数量减少到这个值后,它们的存储结构将由红黑树重新转为链表,默认该值为6。
第七个数据是:MIN_TREEIFY_CAPACITY = 64(最小树容量)。
当HashMap容器的大小增加到这个值时,不管最大相同位置的键值对是否达到TREEIFY_THRESHOLD(树阈值),它的存储结构都将转化成红黑树。
6.HashMap的构造方法
HashMap有四个构造方法:
第一个,参数为空,系统将默认负载因子传入。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
第二个,容器大小(initialCapacity)+负载因子(loadFactor),系统根据用户自定义的容器大小和负载因子创建HashMap对象。
public HashMap(int initialCapacity, float loadFactor) {
//判断用户自定义的参数是否符合创建规范
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
第三个,容器大小(initialCapacity),系统调用第二个构造方法,根据用户定义的容器大小,再将默认负载因子传入。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
第四个,Map对象,根据传入的Map对象复制一个HashMap对象。
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
选择上面的构造方法创建好容器后,就需要往容器中添加数据了,那么这些添加进去的数据是什么样的呢?
7.HashMap存储的数据
前面已经说了,HashMap存储键值对的数据,并且这些数据在默认情况下以链表的形式存储,当链表长度大于8,则转成红黑树。
OK,我们先来看第一种,当数据以链表的形式存储时,它的每个节点是怎样的。
//链表数组,存储HashMap元素的地方
transient Node<K,V>[] table;//transient关键字:在序列化时,跳过该字段
从源码中可以看到,它将数据存储在一个Node<>数组中,这个Node其实就是一个链表。
//HashMap的内部类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;
}
}
我们从上往下来捋一捋这部分源码,它实现了Map接口的一个内部接口Entry(),然后每个链表节点对象都存储了四个值,分别是hash值(后面会讲),key,value和指向下一个节点的next。
剩下的就是用来访问这个节点对象的get和set方法了,它还重写了hashCode()方法,用来给equals()方法做判断:当两个节点对象的key和value相等时,视为两个节点对象相等。
当节点数量达到阈值时,它将转成红黑树,以下是红黑树节点在HashMap中的定义。
//HashMap的内部类TreeNode
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);
}
//返回根节点
final TreeNode<K,V> root(){...}
//将红黑树的根节点移至链表首节点
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {...}
//根据给的hash值和key在一颗红黑树中寻找节点
final TreeNode<K,V> find(int h, Object k, Class<?> kc){...}
//根据给的hash值和key在一颗红黑树中寻找节点
final TreeNode<K,V> getTreeNode(int h, Object k) {...}
//
static int tieBreakOrder(Object a, Object b) {}
//
final void treeify(Node<K,V>[] tab) {...}
//
final Node<K,V> untreeify(HashMap<K,V> map) {...}
//
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {...}
//
final void removeTreeNode(HashMap<K,V> map,Node<K,V>[] tab, boolean movable) {...}
//
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {...}
首先,我们梳理一下TreeNode的继承关系。
TreeNode继承了Node,所以TreeNode不止是一个红黑树,也是一个链表,而在TreeNode 源码中还可以看到“TreeNode<K,V> prev;”用于指向前置节点,所以:TreeNode不只是一棵红黑树 ,也是一个双向链表。当容器中的节点数据从Node转成TreeNode后,它依然存储在Node<K,V>[] table中,其结构模型如下:
现在容器中的数据是怎样的已经确定下来了,接下来就应该确定数据在容器中的存放位置了。
HashMap通过 hash值+indexFor()方法 来确定数据的插入位置。
8.hash值的计算
关于hash值的计算,源码中如此写道:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这段代码并不难理解,稍稍简化一下就变成了
int hash(Object key){
if(key==null)return 0;
int h=key.hashCode();
return h^(h>>>16);
}
所以hash值只跟对象的hashCode()有关,其中最最最重要的一个参数key.hashCode(),HashMap重写了hashCode()方法:
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
而hashCode(key)和hashCode(value)的来源为:
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
public native int hashCode();
看到了吧,最终这个值还是由Object类的本地方法提供的,并不需要我们自己计算(网上有hashCode的计算公式)。每一个对象被创建出来,它们的hashCode()都是不一样的。
//既然hashCode()不用我们计算,那我们随便举个例子来计算一下上面的hash值,如 key.hashCode()=156.
0000 0000 1001 1100 //156的二进制
0000 0000 0000 0000 //>>>16:无符号右移16位,高位补0
0000 0000 1001 1100 //h^(h>>>16):^ 位异或运算,相同为0,不同为1
这样,我们就计算出了当key的hashCode为156时,它的hash值
hash=156
8.indexFor()方法计算插入位置
先来看看indeFor()方法的源码。
static int indexFor(int h, int length) {//h为hash值,length为容器大小
return h & (length-1);//& 位与
}
//假设现在容器的大小为16,一个插入数据的hash值为156,则它的插入位置为:
0000 0000 1001 1100 //156的二进制
0000 0000 0000 1111 //16-1=15的二进制
0000 0000 0000 1100 //156&15=12,则该数据的插入位置为数组下标12的位置
从上面这个length-1我们也可以看出一些问题:为什么HashMap的初始大小最好设置成2n(默认16)?
因为当这个length为2n时,length-1的二进制会变成 (0)n(1)m,这样它按位与多个不同的数时,可以极大地减少碰撞率。
至此,HashMap的数据插入应该算分析完了,但当我在我的HashMap源代码文件中查找它时,却发现没有这个方法。
经过我的一番查找,我发现,原来indexFor()的计算被直接嵌入到调用它的方法去了。
//将链表转成红黑树
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index;//index变量即为要保存indexFor()的位置
...
if (...)
...
else if ((e = tab[index = (n - 1) & hash]) != null) {//indexFor()
...
}
}
//删除链表节点
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
int n, index;//index为保存indexFor()的位置
if (... &&
(p = tab[index = (n - 1) & hash]) != null) {//indexFor()
......
}
return null;
}
//删除红黑树节点
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
...
int index = (n - 1) & hash;//indexFor()
......
}
9.HashMap的扩容机制
HashMap采用的是懒扩容,即每次在进行put操作时,它会检查容器中的数据量,当HashMap容器中的数据量达到当前容量*负载因子(0.75)时,进行扩容。
//插入操作put(),它调用了putVal()
transient Node<K,V>[] table;
//put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//putVal实现数据插入
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//如果表为空或者表的大小为0
n = (tab = resize()).length;//调用resize()为表初始化大小
......
if (++size > threshold)//如果数据量超标
resize();//扩容
...
return null;
}
//实现初始化表或者扩容表
final Node<K,V>[] resize() {
......
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 新表扩容为原来的两倍
}
......
}
......
return newTab;
}
总结,当HashMap在put()需要初始化容量或进行扩容时,会调用resize(),讲原表初始化或进行2倍扩容,根据数据的hash值计算它在新表中的位置,重新存放。
10.总结
对HashMap的源码分析暂时就这么多了,只讲了类和方法的一部分关键代码,并没有对整个的代码进行全部分析,可能以后会补全吧…可能。我还还只是个即将毕业的新人,文中有什么疏漏的地方,还望各位大佬不吝指教,在下方评论区指出。