JDK1.8之HashMap源码分析一

一. 插入
1.1 主流程解析
public static void main(String[] args) {
    HashMap<Object, Object> map = new HashMap<Object, Object>();
    // 重写Entity的hashCode方法,返回传入的数值
    map.put(new Entity(97), "2");
    // 插入a,hashCode值同样为97,模拟相同hashCode的值插入,形成链表
    map.put("a", "1");
    // 再次插入a,模拟相同hashCode且满足==,或equals,替换原值并返回
    map.put("a", "1");
    // 增加多个值,模拟扩容
    for(int i = 'b'; i < 'q'; i++){
        map.put(String.valueOf((char)i), i);
    }


}

调用put方法

public V put(K key, V value) {
	// 计算hash值,调用key.hashCode() ^ (key.hashCode() >>> 16)
	// hashCode和hashCode右移16位进行位运算,增加离散性
    return putVal(hash(key), key, value, false, true);
}

利用算好的hash值,开始插入数据

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果未初始化table,那么先初始化,默认hashMap数组大小是16【DEFAULT_INITIAL_CAPACITY】
    // 默认的扩容因子是0.75【DEFAULT_LOAD_FACTOR】,即16 * 0.75
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 根据容量(n - 1) & hash,算出一个小于n的数组下标,判断当前下标对应的tab是不是为空,
   	// 如果为空,那么直接新建一个Node放到该数组位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    	// 如果当前下标已经有一个Node了
        Node<K,V> e; K k;
        // 判断hash值知否一样,并且key是不是同一个对象或这个equals相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 如果相等,就把当前下标的Node赋给临时变量e
            e = p;
        else if (p instanceof TreeNode)
        	// 如果当前下标的Node是一个TreeNode,说明是一个红黑树的Node,然后往红黑树中插入数据,插入逻辑就移到TeeNode中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        	// 否则当前下标的Node是一个链表结构
        	// 从头部遍历链表
            for (int binCount = 0; ; ++binCount) {
            	// 如果没有下一个节点了,表示前面的遍历都没有遇到key相等或equal的,说明该节点已经是尾节点了,那么就把尾节点的下一个节点设置为当前需要插入的节点
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 判断节点数有8个了,那么就将链表转换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果有相同或equals的key,那么就break掉,此时e就是该相同key的Node
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 如果e不为null,说明链表中有相同的key,直接替换该值并返回,不会对modCount或者size执行++操作
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 成功插入数据,除开相等或equals的key,modeCount都要++
    ++modCount;
    //size也执行++操作,并且比较是否大于了扩容阈值,然后进行自动扩容操作
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

上面是HashMap插入的大致插入流程,下面我们来具体分析每一步

1.2 初始化table

初始化的过程其实分为两步,第一步是在实例化HashMap的时候,HashMap的构造方法分为无参构造和有参构造

1.2.1 构造器初始化
// 无参构造方法默认会初始化扩容阈值因子,loadFactor=0.75,后续初始化时默认使用16作为数组初始化容量
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

// 有参构造方法,可以传入一个容量大小,扩容因子
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;
    // tableSizeFor方法中会进行一些列的位运算,最终返回一个小于等于initalCapacity的2的幂次方的容量值,后续resize的时候会用到
    this.threshold = tableSizeFor(initialCapacity);
}


1.2.1 插入过程中的初始化

数组的初始化是在put的时候进行的,当发现当前table为空会长度为0时,会进行重新计算数组长度。

if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

下面来看resize方法

final Node<K,V>[] resize() {
   Node<K,V>[] oldTab = table;
   // 获取到原数组的长度
   int oldCap = (oldTab == null) ? 0 : oldTab.length;
   // 获取到原阈值,如果是通过有参构造器创建的hashMap,这个值会是一个2的幂次方的数值,当第一次进这个方法的时候就是将要初始化的数组的长度,而不是乘以因子后的阈值
   int oldThr = threshold;
   int newCap, newThr = 0;
   //  如果原容量大于0,表示初始化过了
   if (oldCap > 0) {
   		// 长度不能超过2^32次方个
       if (oldCap >= MAXIMUM_CAPACITY) {
           threshold = Integer.MAX_VALUE;
           return oldTab;
       }
       // oldCap左移以为表示 乘以 2,原来的扩容阈值也翻倍
       else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
           newThr = oldThr << 1; // double threshold
   }
   // 如果没初始过,并且原来的阈值大于0,那应该就是通过有参构造函数实例化的HashMap,直接将扩容阈值给新数组的容量
   else if (oldThr > 0) // initial capacity was placed in threshold
       newCap = oldThr;
   // 如果上面的都不成立,使用默认的容量和阈值因子
   else {               // zero initial threshold signifies using defaults
       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"})
       Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
   table = newTab;
   if (oldTab != null) {
   		// 遍历原来长度的数组,决定该下标的Node应该放到新的数组的哪个下标
       for (int j = 0; j < oldCap; ++j) {
           Node<K,V> e;
           if ((e = oldTab[j]) != null) {
           	   // 先把原下标置为null
               oldTab[j] = null;
               // 如果当前下标只有一个Node,没有链表也没有红黑树,那么就用hash值和新数组长度-1做与运算,
               // 得到一个小于新数组长度的下标,并放进去
               if (e.next == null)
                   newTab[e.hash & (newCap - 1)] = e;
               // 如果当前下标的Node是红黑树结构,就拆分红黑树,这里我们等到面说了红黑树再来分析
               else if (e instanceof TreeNode)
                   ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
               // 如果是链表结构
               else { // preserve order
                   Node<K,V> loHead = null, loTail = null;
                   Node<K,V> hiHead = null, hiTail = null;
                   Node<K,V> next;
                   do {
                       next = e.next;
                       // hash值和原数组长度做与运算,长度是一个2的幂次方,因此只有可能有一个位上是1
                       // 因此这个与运算,要么是等于0,要么是等于原数组长度
                       // 如果等于0,就放到低位,如果不等于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;
                   }
                   if (hiTail != null) {
                       hiTail.next = null;
                       newTab[j + oldCap] = hiHead;
                   }
               }
           }
       }
   }
   return newTab;
}
1.2.2 链表转红黑树

前面我们提到当同一个下标上的链表的Node个数大于等于了8个,那么就会将链表转换成红黑树,但实际上,这么回答不对,大于等于8只是会进入下面的treeifyBin方法,但这个方法还有一个判断条件必须是数组的长度要大于等于64【MIN_TREEIFY_CAPACITY】才会转换红黑树,否则hashMap会认为当前数组还太小,可以通过扩容解决。

final void treeifyBin(Node<K,V>[] tab, int hash) {
   int n, index; Node<K,V> e;
   // 判断数组长度是否小于64,如果小于进行扩容操作
   if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
       resize();
   // (n - 1) & hash就是当前链表上Node数等于8的数组下标,获取到这个下标的node
   else if ((e = tab[index = (n - 1) & hash]) != null) {
   		// 头节点,尾节点
       TreeNode<K,V> hd = null, tl = null;
       do {
       		// 利用原Node的hash,value,key, next创建一个新的TreeNode
           TreeNode<K,V> p = replacementTreeNode(e, null);
           // 第一次循环把原链表的作为head节点产生的TreeNode赋值给hd,尾节点也指向它
           if (tl == null)
               hd = p;
           // 后面的循环将当前遍历的节点的prev指向上一次循环种的尾节点,上一次循环的尾节点指向当前节点
           else {
               p.prev = tl;
               tl.next = p;
           }
           // 并把尾节点指向当前节点
           tl = p;
       } while ((e = e.next) != null);
       // 上面循环完成后形成了另一个TreeNode的双向链表
       // 还是按原来的格式,将头节点放入数组中,即头插法
       if ((tab[index] = hd) != null)
       	   // 产生红黑树
           hd.treeify(tab);
   }
}

看生成红黑树之前,我们先看下TreeNode类

TreeNode<K,V> parent;  // red-black tree links 父节点
TreeNode<K,V> left; // 左边子节点
TreeNode<K,V> right;// 右边子节点
TreeNode<K,V> prev;    // 双向链表用的上一个子节点
boolean red; // 是否为红节点

红黑树的定义或者说是规则如下:

  1. 每个结点或是红的,或是黑的
  2. 根节点是黑的
  3. 每个叶结点是黑的,null leaves
  4. 如果一个结点是红的,则它的两个儿子都是黑的
  5. 对每个结点,从该结点到其子孙节点的所有路径上包含相同数目的黑结点

下面来看TreeNode是怎么利用头节点的treeify方法创建出这个树的

 final void treeify(Node<K,V>[] tab) {
 	// 根节点临时变量
    TreeNode<K,V> root = null;
    // 遍历treenode链表,一个一个插入到红黑树种
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        // x为当前节点,next为下一个节点
        x.left = x.right = null;
        // 当遍历的x为头节点时root==null
        // 设置它的父节点为null,不为红节点,root定位头节点
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
        	// 当后续进入该节点时
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            // 从根节点开始比较hash值的大小
            for (TreeNode<K,V> p = root;;) {
            	// 临时变量标识方向和当前遍历到的节点的hash值
                int dir, ph;
                K pk = p.key;
                // 如果遍历到的树节点的hash值大于当前待插入的节点,标识需要继续和左边子节点进行比较
                // 如果相反,则和右边子节点继续比较
                // 如果相等,看待插入节点是否实现了Comparable,如果没有实现或者实现了但是调用compareTo比较结果还是等于0,那么就调用tieBreadOrder方法继续比较
                // 如果比较的其中一个有一个为null或者类名称相同,就调用System.identityHashCode方法进行比较,反正最后得出一个比较值,如果还是等于0,就让它为0,默认走比较左子节点的逻辑
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

				// xp临时变量,标识待插入节点的父节点
				// 根据上面判断出来的方向,看它的左右子节点是否存在,如果存在则继续比较,如果不存在
				// 将当前遍历到的树节点作为待插入节点的父节点
				// 并且根据方向,将当前遍历的树节点根据方向将左或右子节点置为待插入节点
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    // 插入后平衡当前树,返回数的root节点
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    // 得到的root对应的node放回数组的对应下标上
    moveRootToFront(tab, root);
}

我们继续看如何平衡插入节点后的树

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
    // 默认插入的新的节点都是红节点
    x.red = true;
    // 定义循环变量
    // xp:当前节点的父节点
    // xpp:当前节点的祖父节点
    // xppl:当前节点的祖父节点的左子节点
    // xppr:当前节点的祖父节点的右子节点
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
    	// 如果当前节点的父节点为null,说明当前节点就是根节点,将自己置为黑色返回
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        // 如果当前节点的父节点为黑色,或者祖父节点为null,说明新插入的节点的为第二层节点,上层就是root节点,直接返回父节点root节点
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        // 如果父节点的左子节点就是新插入的节点的父节点
        if (xp == (xppl = xpp.left)) {
        	// 如果叔叔节点已经存在,并且是红节点,那么就将祖父节点的黑向下压,即父节点和叔叔节点都置为黑节点,祖父节点置为红节点,当前需要平衡的节点设置为祖父节点递归
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            // 叔叔节点为空或者为黑色
            else {
            	// 如果当前节点是父节点的右子节点,先左旋,再右旋
            	// 如果是左子节点,那么直接右旋
            	// 得到新的root节点后,继续递归平衡
                if (x == xp.right) {
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // 如果父节点是右孩子节点
        else {
            // 同上面判断叔叔节点的逻辑一样,如果叔叔节点也是红色,直接将黑色的祖父节点颜色向下压,祖父节点变红,祖父节点的两个子节点变黑,然后递归判断祖父节点
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            // 同上面的逻辑,只不过是反过来的
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

左旋,其中参数说明一下:
root:当前的根节点
p:需要进行左旋的节点

static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    // 要满足左旋必须是当前要左旋的节点的右节点不为空
    if (p != null && (r = p.right) != null) {
    	// 将需要左旋的节点的右子节点指向原右节点的左节点,如果不为空,再将这个右子节点的父节点指向当前节点
        if ((rl = p.right = r.left) != null)
            rl.parent = p;
        // 如果当前节点的父节点为null,说明p节点是root节点,那么就将它的右节点置为root节点并置为黑色
        if ((pp = r.parent = p.parent) == null)
            (root = r).red = false;
        // 如果p不是root节点,p是它父节点的左节点,那么直接将pp的左节点指向p的右节点r
        else if (pp.left == p)
            pp.left = r;
        else
            pp.right = r;
        // 把最终得到的r的左节点指向当前节点完成左旋
        r.left = p;
        p.parent = r;
    }
    return root;
}

右旋的操作逻辑自己体会一下,这里就不概述了。
最后得到一颗完整的红黑树结构,返回了他的root节点,
然后把root节点对应的node设置到对应的数组下标上。

1.2.3 插入到已生成的红黑树

前面主流程中会判断当前下标的节点是否是instanceof TreeNode,如果是说明这个下标上已经是红黑树,那么本次插入会直接将节点往红黑树中插。
调用TreeNode【root节点】的putTreeVal方法插入。

/**
 * Tree version of putVal.
 */
 // map:hashMap实例
 // tab:数组
 // h: hash值
 // k:键
 // v:值
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    // 拿到root节点,一般就是自己
    TreeNode<K,V> root = (parent != null) ? root() : this;
    // 这里的逻辑基本和treeify方法一样,判断出本次插入的这个节点的hash值,是该往左边查还是往右边查,如果遍历到的节点刚好有这个方向的子节点,就递归比较,循环中主要做插入位置的判断,其他操作还是一样交给balanceInsertion,然后再设置root节点对应的Node到数组上
    // **另外,这里还有一个很大的不同是**:判断遍历到的节点hash值如果和当前节点的hash值相等,并且两个key的对象是同一个对象或者equals,那么直接返回该节点值,不会做任何插入操作,而是直接在外层方法中进行值替换
    for (TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            // 平衡调整及设置数组下标对应的root节点
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

好了,插入的部分基本上讲到这里,下面我们来看查询的部分,如果能理解插入的逻辑,查询就比插入简单多了。

二. 查询
2.1 主流程

查询的基本思路:

  1. 通过hash值,根据容量做与运算,获取到这个hash值对应的数组下标,
  2. 比较判断节点类型
  3. 遍历比较key是否是同一对象或是否equals,拿出对应的Node节点或者TreeNode节点
  4. 获取value
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 校验及获取到对应下标上的
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 直接比较头节点上的数据,如果头节点上满足条件了,就不用再判断是不是有后续的链表或树了
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
            	// 如果是红黑树,调用红黑树的获取树节点方法
                if (first instanceof TreeNode)
                    return ((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;
    }

其实基本上查询的代码就差不多结束了,只有treeNode需要再次进getTreeNode获取节点,稍微想一下就应该知道里面的逻辑是什么,无非就是递归判断树节点和当前要查询的节点的hash值大小,选择方向,继续递归,一旦查询到有满足条件的key,就返回该节点。

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        if ((ph = p.hash) > h)
            p = pl;
        else if (ph < h)
            p = pr;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        // 如果既不大于也不小于,还不等于,那么判断是否有左子节点是否为kong,取右节点继续向下遍历
        else if (pl == null)
            p = pr;
       // 取左节点继续往下遍历
        else if (pr == null)
            p = pl;
       // 比较compareTo,能比较出大小,继续向下遍历
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;
        // 所有上述条件都得不到一个,默认使用右节点查找,再找不到就返回null
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
            p = pl;
    } while (p != null);
    return null;
}

今天就先写到这里,后面还涉及到HashMap的删除操作,也是一个比较复杂的过程,下次慢慢写。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值