JDK1.8HashMap详解

[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之后只进行一次扰动,实现方式进行了简化。数据结构如下图:

image

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()插入/更新方法

image

过程主要如下:

  • 检查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弥补了这一短板。

你可能感兴趣的:(JDK1.8HashMap详解)