[toc]
一、HashMap 简介
HashMap是java.util包中的一个集合框架,他是java.util.Map的实现类,具有方便、高效的基于键值对存取功能,其平均查询时间复杂度为O(1),非线程安全。
HashMap是一种哈希表+链表+红黑树数据结构组成的,基于key-value存取的工具类,在JDK1.8之前没用红黑树这一数据结构,在JDK1.8之后对其进行优化,考虑大量hash碰撞链表查询效率低,所以加入了红黑树这一数据结构以提高此种情况的查询效率,通过阈值控制,将链表和红黑树进行互相转换。同时JDK1.8还有一处优化,即hash扰动函数的优化,在JDK1.8之前hash()函数中key的hash值扰动了四次,目的是降低hash碰撞的可能性,但是JDK1.8之后只进行一次扰动,实现方式进行了简化。数据结构如下图:
JDK1.8的HashMap链表是尾插法,JDK1.7是链表是头插法
二、HashMap源码解读
类的定义
HashMap是Map的实现类,同时继承了AbstractMap类,Cloneable类、Serializable类,后来的两个标志性接口赋予了他可克隆、可序列化的能力。
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable{
}
注:HashMap的容量必须是2的n次方只有长度是2^n 次方,就可以通过&运算实现取模运算了,公式是X%(2^n) =X&(2^n-1),每次扩容也是原来的2倍,默认初始化是16,&运算的速度比取模运算(%)效率高的很多,主要因为位运算直接对内存进行操作,不需要转成十进制,因此处理速度快,并且除了性能外,很好的解决负数问题,因为hashcode结果是int范围是-2 ^ 31 ~ 2 ^ 31-1,包含负数,负数的取模比较麻烦,使用二进制就避免了这个问题,首先不管hashcode值是正数还是负数。length-1一定是正数,那么他的二进制第一位是0(有符号数用最高位表示符号位,0表示+,1表示-)这样两个数做位运算之后第一位一定是0,也就是结果一定是个正数。
常量的定义
//序列化ID,作为唯一识别标志,用于序列化和反序列化
private static final long serialVersionUID = 362498820763181265L;
//默认初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1<<4;// aka 16
//最大容量是2^30
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;
//哈希表最小树化容量,如果小于这个值,不会触发树化,会触发扩容
//如果链表开始树化的时候,发现数组太短小于这个值,直接扩容
//这个值不能小于4 * TREEIFY_THRESHOLD 避免大小调整和树化阈值之间的冲突
static final int MIN_TREEIFY_CAPACITY = 64;
内部的Node
HashMap用很多的内部类比如Node、TreeNode、KeySet、Values、EntrySet、HashIterator,本文只涉及增删改查,Node类和TreeNode类是主要类,由于红黑树比较复杂不是本文重点,所以暂时解读Node类。
Node类是链表中存储的节点类,用于存储节点的hash、key、value等信息,还有下一个节点的引用。
static class Node implements Map.Enty{
//key的hash
final int hash;
//键
final K key;
//值
V value;
//下一个节点引用
Node next;
//构造方法
Node(int hash,K key,V value,Node next){
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//获取key,value,重写toString
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//重写hashCode方法
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//设置节点的值,返回旧值
public final V setValue(V newValue){
V oldValue = value;
value = newValue;
return oldValue;
}
//重写equals方法
public boolean equals(Object o){
if(o == this){
return true;
}
// 先判断是否是Map.Entry类型的类
// 然后分别比较key和value是否相同
// 如果都相同则返回true,否则返回false
if(o instanceof Map.Entry){
Map.Entry e = (Map.Entry)o;
if(Objects.equals(key,e.getKey())&&
Objects.equals(value,e.value())){
return true;
}
}
return false;
}
}
静态工具方法
HashMap中提供了四个静态工具方法,分别是hash()、comparableClaassFor()、compareCompareables()、和tableSizeFor()。
hash()
hash():hash扰动函数,用于计算出key的hash值,其中进行了一次扰动,以减少hash碰撞的概率。
//扰动函数,获取key的hash值
// 该方法相比于jdk1.7的四次移动做出了优化
// 只需要1次位移即可实现,原理相同
static final int hash(Object K){
int h;
//如果key不为空,那么就对key的hash进行一次16位无符号右位移异或然后返回
//这样扰动一次的目的是在于减少hash碰撞的概率
//具体详解:https://www.zhihu.com/question/20733617/answer/111577937
return (key == null) ? 0 : (h = key.hashCoder()) ^ (h >>> 16);
}
comparableClassFor()
comparableClassFor:用于检查某个对象是否是可比较,在HashMap中多用于key的检查。其中对String,进行了特判,String实现了Comparable类,并重写了Object的hashcode()和equals()方法,所以平常都建议用String作为key。
//检查某个对象是否可比较,在HashMap多用于key的检查
static Class comparableClassFor(Object x){
//先判断是否是Comparable类型的,如果不是表明对象不可比较
if(x instanceof Comparable){
Class c; Type[] ts, as;Type t;ParameterizedType p;
//对String类型特判,如果是String类型直接返回对象的Class类,所以建议使用HashMap是key使用String类型
if((c = x.getClass()) == String.class){
return c;
}
//获取该类实现的接口集,包含泛型参数信息
//提前保证它实现了某些接口才有可能实现Comparable
if((ts = c.getGenericInterfaces()) != null){
//遍历循环这些接口
for (int i=0; i < ts.length; ++i){
//判断是否支持泛型
if(((t = ts[i]) instanceof ParameterizedType) &&
//判断承载该泛型信息的对象是否是Comparable类
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
//获取实际泛型列表,并且有且只有一个泛型,即c
// c是传入对象x的类型
(as = p.getActualTypeArguments()) != null &&
as.length ==1 && as[0] == c){
return c;
}
}
}
}
return null;
}
compareComparables()
compareComparables:比较k和x,如果x和k不是同一种类型就返回0,如果是同一种类型就返回compareTo得到的值:
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
tableSizeFor()
tableSizeFor:根据期望容量cap,计算出2的n次方形式的哈希桶的实际容量(容量是2 ^ n可以保证&运算和取模的值相等x%2 ^ n=x&(2 ^ n -1) )
/返会值一般会>=cap
static final int tableSizeFor(int cap) {
//经过下面的或和唯一元素,n最终各位都是1
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
//判断n是否越界,返会2的n次方作为table(哈希桶)的阈值
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
属性变量
HashMap中定义了六个属性变量,用于构建及管理hash表
// hash表是一个node类型的数组,在第一次被使用时初始化,同时扩容时对其进行数组迁移等操作
transient Node[] table;
//缓存node节点的集合,用于记录被使用过key和value集合
transient Set> entrySet;
//已经存在key-value对的数量,当size>threshold是触发扩容resize()
transient int size;
//结构修改记录,rehash时会记录
//因为hashmap是线程不安全的,所以要保存modCount,用于fail-fast策略
transient int modCount;
//调整容量的下一个大小值,其值等于容量*负载因子
int threshold;
//hash的负载因子,用于计算哈希表元素数量的阈值
//threshold=哈希桶.length * loadFactor
final float loadFactor;
构造函数
//传入初始化容量和负载因子
public HashMap(int initialCapacity,float loadFactor){
if(initialCapacity<0){
throw new IllegalArgumentException("Illegal inital 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;
//根据预期容量计算实际容量
//实际容量大小始终是大于预期容量的最小的2的次幂
//比如传入初始化容量是7,计算得到的时间容量是8,8是2的3次方
this.threshold = tableSizeFor(initialCapacity);
}
//传入初始化容量
public HashMap(int initialCapacity){
this(initialCapacity,DEFAULT_LOAD_FACTOR);
}
//无参的构造方法,默认初始化容量是16,负载因子是0.75
public HashMap(){
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(Map m){
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m,false);
}
其中批量注入元素的方法putMapEntries如下:
//将一个Map的所有元素加入表中,参数evict初始化时是false(如果是false表示为创建模式)
final void putMapEntries(Map m, boolean evict) {
//获得m元素的数量
int s = m.size();
//数量大于0才进行操作
if (s > 0) {
//hash表未初始化
if (table == null) { // pre-size
//根据m中的大小和负载因子计算出阈值
float ft = ((float)s / loadFactor) + 1.0F;
//修正阈值的边界,不能超过MAXIMUM_CAPACITY
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//如果新的阈值大于当前阈值,那么返回一个大于等于新的阈值的满足2的n次方的阈值
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果表格不为空,并且m的元素个数数量大于当前容量的而与之,则进行resize
else if (s > threshold)
resize();
//遍历m一次将元素加入当前表中
for (Map.Entry e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
几个常规方法
//经常使用,获取hash表中已存在的键值对数量
//注意这个size并非是hash表中的大小,而是实际存在的键值对的数量
public int size(){
return size;
}
//是否为空,,即是否实际存在键值对
public boolean isEmpty(){
return size==0;
}
//检测是否存在key
//逻辑与get相同,主要是调用getNode方法
public boolean containsKey(Object key){
return getNode(hash(key),key) != null;
}
//是否存在value
public boolean containsValue(Object value){
Node[] tab;V v;
//遍历哈希通上的每一个链表
if((tab = table) != null && size > 0){
for(int i = 0; i< tab.length; ++i){
for(Node e = tab[i]; e != null; e = e.next){
//找到value一致的返回true
if((v = e.value) == value || (value != null && value.equals(v))){
return true;
}
}
}
}
return false;
}
public void putAll(Map m){
putMapEntries(m,true);
}
get方法
//根据key取值
public V get(Object key){
Node e;
//根据key的值和扰动后key的hash值先得到Node节点,然后获取其中值
return (e = getNode(hash(key),key)) == null ? null : e.value;
}
//jdk8 新增的方法查询到返回value查询不到返回默认值
public V getOrDefault(Object key,V defaultValue){
Node e;
return (e = getNode(hash(key),key)) == null ? defaultValue : e.value;
}
// 根据扰动后的hash值和key的值获取节点
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
//基本逻辑:先找到相应节点,然后返回,如果不存在返回null
//table 不为null并且大小大于0才继续
if ((tab = table) != null && (n = tab.length) > 0 &&
//hash&n-1定位到桶的位置,然后获取头结点
// 只有容量是2的n次方就可以保证hash%(cap)= has &(cap-1) 公式 X%2^n =X &(2^n -1)
(first = tab[(n - 1) & hash]) != null) {
//如果头结点恰好是该节点则直接返回
//检查内容头结点的hash是否相同,key是否相同(检测内存地址或者检测值)
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//头结点不是要找的节点,接下来去的下一个节点进行寻找
if ((e = first.next) != null) {
//如果桶内元素是红黑树,那么就调用getTreeNode方法查找
if (first instanceof TreeNode)
return ((TreeNode)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;
}
put()插入/更新方法
过程主要如下:
- 检查hash表是否初始化,如果没有进行resize扩容
- 根据key的扰动hash值定位到桶的位置,如果桶内为空,直接创建新的Node放入桶中
- 如果桶不为空,则发生了hash碰撞,则进行下一步
- 如果桶内数据结构是红黑树,则以红黑树的形式遍历,如果key不存在则直接插入(JDK1.8插入链表方式是尾插法,JDK1.7插入链表方法是头插法),如果key存在先获取该节点
- 如果桶内数据结构是链表,则以链表形式循环遍历,如果遍历到尾结点仍无相同的key存在,则直接插入,并且判断是否超过阈值,决定是否树化,如果key存在,则先获取该节点。
- 如果允许覆盖,则将之前的找到的key对应的节点进行覆盖,否则什么都不做。
- 修改操作计数modCount,并检测是否需要扩容,如果需要则进行resize。
//插入新的值,主要调用putVal方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//插入新值核心函数
//如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value
//如果evict是false表示是在初始化时候调用的此函数
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//首先检查table是否为null并且容量是否大于0,即有没有初始化table
//如果没有初始化就进行resize
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//先定位到桶的位置,p为该桶的头结点
//如果p为null则说明该桶还没有节点,直接将新的键值对存入桶中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//p不为null,即发生了hash碰撞,进一步处理
else {
Node e; K k;
//比较头结点的扰动值,及key的值
//如果相同则说明存入接地昂key已经存在,而且就是头结点
//先获取该接地昂,是否覆盖其值进一步处理
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//头结点的key和插入的key不相同
//先判断桶内数据结构是否是红黑树,如果是则以红黑树的方式插入到树中
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
//桶内节点不是红黑树,即链表结构
else {
//循环遍历链表
//直到找到与插入节点key相同的节点,如果没有找到直接插入到尾结点
for (int binCount = 0; ; ++binCount) {
//已经遍历到尾结点,说明插入的key不存在,直接插入到尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果桶内节点数量达到了树型化阈值,则进行树型化,
//因为binCount从0开始所以TREEIFY_THRESHOLD-1
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//插入的key已存在,先获取该节点,是否覆盖其值进一步处理
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果获取到节点不为null则进行操作
if (e != null) { // existing mapping for key
V oldValue = e.value;
//方法出入的onlyIfAbsennt参数为false,获取旧值为null则直接替换掉旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//这是一个空实现函数,用作LinkedHashMap重写使用
afterNodeAccess(e);
return oldValue;
}
}
//以上操作及全部完成,并且已经成功插入或者更改一个节点的值
//修改modCount的值,记录修改次数
++modCount;
//更新size,并且判断是否超过阈值则进行扩容
if (++size > threshold)
resize();
//这是一个空实现的函数,用作LinkedHashMap重写使用
afterNodeInsertion(evict);
return null;
}
注:如果负载因子大于1,HashMap的桶由于填满了但是由于负载因子大于1不会进行扩容,之后当做普通的hash冲突一样生成链表,不进行扩容
链表的树化箱treeifyBin()
如果一个桶中元素超过TREEIFT_THRESHOLD(默认是8),就是用红黑树替换链表,提高查询效率。
如果当前hahs表为空或者哈希表长度(注意不是hashMap的size,而是数组的长度)小于进行树化的阈值(默认是64),就先扩容,而不会触发树化
//树化箱
//链表树形化,将链表节点替换成红黑树,
//除非给定的表太小,否则替换给定hash值对应索引的链接节点,如果是表太小则进行扩容
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
//如果当前哈希表为空,或者哈希表长度(注意不是hashMap的size)小于进行树化的阈值(默认是64)就去扩容,而不会触发树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//如果哈希表中的元素个数超过树形化阈值,进行树化
//e是哈希表中指定位置桶里的链表节点,从第一个开始
TreeNode hd = null, tl = null;
do {
//新建一个树形节点,内容和当前链表节点e一致
TreeNode p = replacementTreeNode(e, null);
//确认树头节点
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
//让桶中第一个元素指向新建的红黑树头结点你,以后这个桶里元素就是红黑树而不是链表了
if ((tab[index] = hd) != null)
//树化
hd.treeify(tab);
}
}
扩容
触发扩容的情况:
- 树化如果table大小小于树化最小阈值默认64则进行扩容
- 当size>threshold扩容(threshold=容量(数组长度)✖️负载因子)
resize是非常重要的一个函数,负责HashMap中动态扩容的核心逻辑,其主要逻辑如下:
- 1、备份旧表、旧表容量、旧表阈值,定义新表的容量、阈值;
- 2、如果旧表容量大于0
- 如果旧表容量已经达到上限,则设置阈值为最大整数,不在进行扩容
- 如果旧表容量未达到上限,设置新表容量为旧表容量的2倍,但前提是新表容量也得在上限范围内
- 3、如果旧表容量为空,但是阈值大于0,说明初始化时指定了容量和阈值,旧表的阈值作为新表的容量
- 4、如果旧表容量为空,并且阈值为0,说明初始化没有指定容量和阈值,则将默认的初始化容量和阈值作为新表的容量和阈值
- 5、如果以上操作之后新表的阈值为0,根据新表容量和负载因子求出新表的阈值
- 6、创建一个新的表,其数组长度为新表容量
- 7、如果旧表不为空,就进行数据迁移,迁移时,依次遍历每一个桶
- 如果桶中只有一个节点,则直接放入新表中对应位置的桶中。
- 如果桶中不止一个节点,并且结构是红黑树,则进行拆分红黑树然后迁移
- 如果桶中不止一个节点,并且结果是链表,则分为高位和低位分别迁移(高位=低位+原哈希桶容量),低位放入新表对应旧桶的索引中,高位放入新表对应的桶索引中
- 8、返回新表
下面是对应代码:
//hash表扩容核心函数
final Node[] resize() {
//先存一个旧的table
Node[] oldTab = table;
//旧的table的容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧的阈值
int oldThr = threshold;
//定义新表的容量和阈值
int newCap, newThr = 0;
//如果旧table的容量大于0
if (oldCap > 0) {
//判断旧表容量是否达到上限
if (oldCap >= MAXIMUM_CAPACITY) {
//旧表容量达到上限,设置阈值为最大整数,不在进行扩容重写
threshold = Integer.MAX_VALUE;
return oldTab;
}//未达到上限,新的table的容量是旧的table容量2倍,前提是在上限范围内
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新的阈值是旧阈值乘2
newThr = oldThr << 1; // double threshold
}
//表示table表为空,但是阈值大于0,说明初始化时候指定了容量和阈值
else if (oldThr > 0) // initial capacity was placed in threshold
//初始容量设置为老的阈值
newCap = oldThr;
else { //既没有初始容量又没有初始化阈值,那么进行初始化,使用默认的初始化容量和默认的负载因子计算阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//比如oldThr > 0的情况newThr为0,newCap=oldThr
if (newThr == 0) {
//根据table容量和负载因子求出新的阈值
float ft = (float)newCap * loadFactor;
//进行越界限定
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//更新阈值
threshold = newThr;
//创建一个新的table大小为newCap
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
//将新的table直接赋值给table,原来存放值的table内存被oldTab所指向
table = newTab;
//如果旧的table不为空,那么就进行节点迁移
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
//依次获取旧table中桶中的首节点
if ((e = oldTab[j]) != null) {
//清理旧表中概统的内存空间,防止内存泄漏
oldTab[j] = null;
//如果桶中只有一个节点,直接存入新的table中
if (e.next == null)
//定位在新table中的位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//桶中不止一个节点,并且结构是红黑树
//红黑树赋值方法几乎和链表一样,遍历链表
//形成高低位两个链表,
//根据两个新链表的长度来决定是否转换为数
//最后将两个链表放入新表中对应数组槽中
//设置旧表数组槽设置为ForwardingNode节点
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
//数据结构为链表
//因为扩容是容量范围,所以原链表上的每一个节点现在可能存在原来的下标,即低位
//或者扩容后的下标,即高位
//高位=低位+原哈希桶容量
//低位链表的头结点、尾结点
Node loHead = null, loTail = null;
//高位链表的头结点、尾结点
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
// 利用哈希值和旧的容量进行&运算,如果等于0应该存放在低位,否则存放在高位
//如果尾结点为空说明链表还没有头结点,将当前节点赋值到头结点,
//否则将当前节点赋值到尾结点的下一个
//最后更新尾结点变量
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;
}
//高位链表不为空,将高位链表的头放到原索引+oldCap的位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//返回新的table
return newTab;
}
注:JDK1.7扩容时候需要重新计算索引位置,1.8分为高位和低位分别迁移(高位=低位+原哈希桶容量),低位放入新表对应旧表桶索引中,高位放入新表对应新的桶的位置。
删除节点remove()
删除操作是根据key先找到对应Node节点,然后再删除,如果没用找到直接返回null,其操作和get十分相似。
//根据key删除一个节点,其主要是调用removeNode方法,如果key对应Node节点存在返回旧值,如果不存在返回null
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
//删除节点的核心方法
//如果参数matchValue是true,则必须key、value都相等才删除
//如果movable参数为false,在删除节点是,不移动其他节点(用于节点是树的情况)
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
//再删除之前先确定表是否为空,并且其容量大于0,并且通过key定位到桶位置中桶不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node node = null, e; K k; V v;
//如果头结点是要删除的节点,则直接赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//如果还存在后续节点就继续寻找要删除的节点
else if ((e = p.next) != null) {
//如果桶内数据结构是红黑树,则在红黑树中找出该节点
if (p instanceof TreeNode)
node = ((TreeNode)p).getTreeNode(hash, key);
else {
//如果是链表,则循环遍历查找
//注意此时p是删除节点的前驱节点,node是被删除的节点
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//如果要删除的节点找到了,就进行删除操作,否则返回null
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//根据数据结构不通同进行删除相应节点
if (node instanceof TreeNode)
//数据结构是树的情况
((TreeNode)node).removeTreeNode(this, tab, movable);
else if (node == p)
//数据结构是链表,并且删除的节点就是链表的头节点
tab[index] = node.next;
else
//数据结构是链表,头结点不是要删除的节点
p.next = node.next;
//记录修改次数
++modCount;
//键值对数量-1
--size;
//空实现函数,LinkedHashMap回调函数
afterNodeRemoval(node);
return node;
}
}
return null;
}
总结
HashMap包括:数组+链表+红黑树的组合,大量使用位运算提高效率,hash扰动减少碰撞,不过HashMap不支持多线程,在多线程情况下会发现数据丢失的情况(JDK1.7扩容时候会出现死循环)。HashTable虽然线程安全,但并发性能不是很好,而ConcurrentHashMap弥补了这一短板。