Java源码分析之HashMap

Java源码分析之HashMap

本文基于Java 8

HashMap使我们在开发过程中经常用到的数据结构,在面试过程中也会经常问到,本篇博文就基于JDK1.8具体分析一下HashMap的实现。

首先看一下HashMap中的静态变量和一些类变量:

// 默认最大容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// HashMap的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转换为树型结构的临界值:
static final int TREEIFY_THRESHOLD = 8;
// Node数组,保存着链表或者树的头结点,每个Index位置称为一个桶
transient Node<K,V>[] table;
// Node的Set
transient Set<Map.Entry<K,V>> entrySet;
// HashMap的size
transient int size;
// 修改次数
transient int modCount;
// 扩容的阈值
int threshold;
// 加载因子
final float loadFactor;

接下来看一下table中的元素,也就是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;
    }
	//....
}

节点构成了HashMap的存储单元,每个节点都保存有该节点的key,key的hash值,value和next节点。

我们来回顾一下日常开发中经常用到的HashMap的方法:

  • put()
  • get(Object key)
  • entrySet()
  • remove(Object key)

首先从无参构造方法说起

public HashMap() {
	this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

默认构造方法很简单,只是初始化了一个loadFactor的值,这个值就是HashMap的负载因子,默认为0.75f

static final float DEFAULT_LOAD_FACTOR = 0.75f;

具体这个值怎么用,我们接下来再讲。

接下来看往HashMap里放数据的put()方法

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

put()方法又调用了putVal()方法,我们继续跟踪下去:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 
				boolean evict) {
    // tab: Node数组   i:根据hash值计算的要put的数据所在的桶的位置   p: talbe[i]桶上的头结点   n: table的长度  
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果table为null的话,调用resize()方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
         // 如果table[i]的值为空,直接new出来一个节点传入key和value并赋给table[i],put操作完成
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 如果p不为空并且p与要put的数据hash值和key值都相同,使用节点e来保存p,此时e不为空
            e = p;
        else if (p instanceof TreeNode)
            // 如果p是树节点,调用putTreeVal()方法向树中put数据
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 如果p不是树节点,则向链表中put数据
            for (int binCount = 0; ; ++binCount) {
                // 遍历至链表的尾结点,此时e为空
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果链表中node超过 TREEIFY_THRESHOLD 个,则将该链表转化为树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 遍历过程中遇到hash值和key均和要put的数据相同的情况,直接跳出循环,此时e不为空
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // e不为空时将e节点的value替换为新值,并返回旧值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
          // onlyIfAbsent参数为false或者e的当前value为空时进行替换
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //只有 e为空,也就是产生新的节点情况下size才会++,如果size > threshold 则对HashMap进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

通过分析putVal()方法我们可以得到如下结论:

  1. HashMap的数组创建是在我们put第一个元素时调用resize()方法完成的
  2. HashMap只有在put一个新key的情况下才会增加节点,否则的话只是替换key所在节点的value
  3. HashMap的数组中,一开始是链表结构,在大于某个临界值时,会转化为树结构

那么 node在table数组中的下标是怎么确定的呢?

我们可以想一个最简单的方案,也就是key的hash值对table的length取余 即 hash % n,而在HashMap的实现实际上更精妙,它的做法是(n - 1) & hash,这个值运算后的结果就是 hash % n。举个例子,假设n = 8 hash = 11,那么hash转化为二进制是1011,n - 1转化为二进制是0111 两者进行&操作后值为0011,就是十进制的3,而 11 % 8的值也是3,这里用位运算其实是为了更高的执行效率。

细心的同学会发现,在putVal()末尾执行了afterNodeInsertion()方法, 这个是干什么用的呢?这里先不讨论,我们后面讲LinkedHashMap时再讨论。

接下来看resize()方法:

final Node<K,V>[] resize() {
    // 旧table 
    Node<K,V>[] oldTab = table;
    // 旧tab的length
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 旧的 临界值
    int oldThr = threshold;
    // 新的最大容量,临界值
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 超出HashMap设置的最大容量,直接设置为int的最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 正常的扩容, 直接将最大容量和临界值扩大一倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
    // 有参构造时
        newCap = oldThr;
    else { 
        //无参构造HashMap时会走到这里,newCap的值为16  newThr的值为16 * 0.75
                  // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 以上几个判断条件都没有对newThr进行赋值时会走到这里
    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数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;// 将oldTab[j]的值赋给e 并将oldTab[j]位置置为null
                if (e.next == null)
                    //如果e只有单一一个节点,直接找到e在newTab的下标并赋值
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 遍历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;
                        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;
}

其他地方都好理解,但是我们看代码的54 和 55行,

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;

这里有点绕,它定义了两个head和两个tail,这是干什么用的呢,我们来分析一下,假设旧数组的size为8,而扩容都是双倍扩容,那么新的容量就是16,而从上面put()方法的分析可知,Node寻找下标位置都是通过hash对size取余,那么我们再假设旧数组下标在0位置上的几个Node他们的Hash值分别为0、8、16、24、32、40,那么对新的size 16取余后分别为0、0、8、0、8,因此可以得出如下结论:
数组扩容后,旧数组nidex上的链表只会出现在新数组的index位置和index+oldCap位置。所以定义两个头尾节点,分别管理低位和高位的链表。理解了这一部分之后,下面的这部分就好理解了。

if (loTail != null) {
    loTail.next = null;
    newTab[j] = loHead;
}
if (hiTail != null) {
    hiTail.next = null;
    newTab[j + oldCap] = hiHead;
}

接下来我们分析HashMap的遍历。

一般情况下我们遍历HashMap都是用这种方式:

Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
	Map.Entry<String, String> entry = it.next();
	System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

我们就一步步分析,首先看entrySet()方法:

public Set<Map.Entry<K,V>> entrySet() {
	Set<Map.Entry<K,V>> es;
	return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

这个就比较简单了,返回了一个EntrySet的对象,我们在生成迭代器时,就是调用的这个对象的迭代器方法,那么我们就看一下EntrySet这个类:

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size() { 
    	return size; 
    }
    public final void clear() { 
    	HashMap.this.clear(); 
    }
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
    //。。。
}

内容有点多,我们先把其他的忽略掉,看一下iterator()方法,也很简单,返回了一个EntryIterator对象,我们继续跟到EntryIterator类:

final class EntryIterator extends HashIterator
	implements Iterator<Map.Entry<K,V>> {
	public final Map.Entry<K,V> next() { return nextNode(); }
}

好吧,我们在遍历时是用的next()方法找到了,调用了父类HashIterator的nextNode()方法,继续跟下去:

final Node<K,V> nextNode() {
	Node<K,V>[] t;
	Node<K,V> e = next;
	if (modCount != expectedModCount)
		throw new ConcurrentModificationException();
	if (e == null)
		throw new NoSuchElementException();
	if ((next = (current = e).next) == null && (t = table) != null) {
		do {} while (index < t.length && (next = t[index++]) == null);
	}
	return e;
}

这里也比较简单,就是返回了next这个节点,那么这个节点在哪里初始化的呢?我们看一下HashIterator的构造方法:

HashIterator() {
  expectedModCount = modCount;
    Node<K,V>[] t = table;
    current = next = null;
    index = 0;
    if (t != null && size > 0) { // advance to first entry
        do {} while (index < t.length && (next = t[index++]) == null);
    }
}

我们可以看到,next这个节点的值,在初始化的时候,指向了HashMap的第一个不为空的节点,在外部调用next时,nextNode()方法会找寻下一个不为空的节点并返回。

接下来我们分析remove()方法:

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

remove()方法调用了removeNode()方法并返回要删除节点的vaule,那么就继续跟踪removeNode()方法:

final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
    //tab:  table  n: table长度  index:删除节点的数组下标 p:要删除的节点
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 找到了key 和 hash均相同的节点
            node = p;
        //否则继续往下一个节点找
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 找到的节点刚好是某个桶的头结点,则将头节点赋给下一个节点
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

这个方法逻辑不太复杂,注释已经说的比较清楚了,这里不多做说明。

get(Object key)方法:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

接下来看getNode(hash(key), key))方法:

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;
}

同样这个方法比较简单,只是找到要查找的key所在的桶,遍历并寻找元素即可。
至此,HashMap的CURD和遍历都已分析完毕,下面我们继续分析HashMap的树形结构和转换的实现。

树形桶的节点结构

static final class TreeNode<K,V> extends LinkedHashMapEntry<K,V> {
	TreeNode<K,V> parent;  // red-black tree links
	TreeNode<K,V> left;
	TreeNode<K,V> right;
	TreeNode<K,V> prev;    // needed to unlink next upon deletion
	boolean red;
	TreeNode(int hash, K key, V val, Node<K,V> next) {
	    super(hash, key, val, next);
	}
	// 代码省略
}

TreeNode是树形结构的基本节点,它继承自LinkedHashMapEntry类,我们看一下具体的实现

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

从以上代码我们可以看到,TreeNode是个红黑树的节点,Entry是个双向链表的节点,由此我们我们可以大胆猜测一下,如果一个桶的结构是树形结构,那么它应该是红黑树+双向链表的集合体,下面我们就来分析+验证这个猜测。

树形桶的生成

我们看一下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)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

我们从第19行开始看
这一句的判断条件的意思是 当某个桶内的元素大于等于TREEIFY_THRESHOLD这个值时,会把桶的链表结构转换为树形结构,我们继续分析treeifyBin()方法:

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // table的长度大于MIN_TREEIFY_CAPACITY时才会有执行树化操作,否则只是扩容
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> 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);
   }
}

我们看第3行代码,从这一行代码的判断条件可以得出结论:只有在桶的个数大于等于MIN_TREEIFY_CAPACITY个时,才会执行树化操作,否则只是扩容,由此我们可以得出如下结论:

HashMap将链表结构转化为树形结构有两个条件:

  1. 单个桶中节点个数大于等于TREEIFY_THRESHOLD(默认值为8)
  2. table长度大于等于MIN_TREEIFY_CAPACITY(默认值为64)

继续分析treeifyBin(Node<K,V>[] tab, int hash)方法,我们可以看到在接下来的do-while循环只是将目标桶内的单向链表结构转化为了双向链表结构,那么真正转为树形结构的操作在哪里呢?答案很明显,就是在hd.treeify(tab);这一行,我们继续跟下去。

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                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);

                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 = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

从代码可以看出,这其实就是把链表转化为红黑树的逻辑,关于红黑树我们后面再讲,现在只需要知道红黑树是一个平衡二叉查找树即可,而且综合之前的分析,我们可以看到其实TreeNode既是一个红黑树结构,又是一个双链表结构。
既然有treeify()方法,那么相对的就会有unTreeify()方法,我们来看一下:

final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

方法的实现很简单,就是遍历红黑树把TreeNode节点转化为Node节点,也就是把红黑树+双向链表的数据结构转化为单向链表结构。
那么这个unTreeify()方法是在哪里调用的呢?有两个地方,一个是removeTreeNode()方法,一个是split()方法,我们首先看removeTreeNode()方法:

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {
    int n;
    if (tab == null || (n = tab.length) == 0)
        return;
    int index = (n - 1) & hash;
    TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
    TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
    if (pred == null)
        tab[index] = first = succ;
    else
        pred.next = succ;
    if (succ != null)
        succ.prev = pred;
    if (first == null)
        return;
    if (root.parent != null)
        root = root.root();
    if (root == null || (movable && (root.right == null
                || (rl = root.left) == null || rl.left == null))) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    // 代码省略
}

代码很长,且大部分和红黑树的节点删除有关,我们把无关的代码省略,重点看一下untreeify()执行的条件

if (root == null || (movable && (root.right == null
            || (rl = root.left) == null || rl.left == null))) {
    tab[index] = first.untreeify(map);  // too small
    return;
}

我们来分析分析上面这个逻辑,进入这个untreeify()的要求是,root == null, root.right ==null, root.left==null, root.left.left==null四种情况,我们以7个节点的红黑树来分析,A为root节点。

七节点红黑树.png

所以进入这个方法主要有以下几种情况,我们一种一种的来分析当满足要求时,节点的个数。(这里默认认为知道红黑树的5个特点,主要是黑平衡)

当这四种情况都满足时,我们可以看出最多节点有如上图所示个数,可以为7个。(大于8个不考虑,因为大于8会变成红黑树)。

  1. 最多节点情况:当我们删除节点D时,只满足root.left.left==null这个条件,这棵树仍可以维持红黑树的特点,这时的最大节点数为6.

  2. 最少节点情况:当EFG不存在时,在A,B,C,D中删除任意一个节点,都会满足上述四种规则中的一种。则存在最少节点情况,有3个节点。

以上情况都是会将树转化成链表,此时的节点是 3<= nodes <=6,由此可以看出,当节点数在小于6时,是可能转化成链表,但不是绝对情况, 所以使用定义的变量(固定数量6)也不正确。只好通过判断去动态获取节点数。

节点数量原因分析

为什么在小于6的时候可能转换成链表,而在大于8的时候转化成红黑树?

主要通过时间查询节点分析,红黑树的平均查询时间为 log(n), 而链表是O(n),平均是O(n)/2。

当节点数为8时,红黑树查询时间3,链表查询时间是4, 可以看出来当红黑树查询效率大于了链表。(两个函数曲线问题,当节点更多是,比红黑树需要的时间更多)

当节点数为6时,为什么转换成链表,我认为主要时因为节点数太少,如果还是用红黑树,为了维持红黑树的特点,则需要翻转,左旋,右旋,等,更消耗性能。

为什么不是7是转化?

主要是为了给一个过渡,防止频繁转化。 也如上图,7个节点可能正好是一个满二叉树。

接下来看split()方法:

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }

    if (loHead != null) {
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

这个方法的调用是在resize()方法中,结合我们上一篇讲过的链表结构resize的操作,这里的逻辑就不难理解了,其实就是把一个桶的树形结构拆分成高低位两个桶,如果低位的桶不适用红黑树的结构了,就调用unTreeify()方法转化为链表结构。

以上分析了HashMap中的一些主要方法和它的一些实现方式,但是并没有分析为什么要用这种方式的实现,比如:为什么JDK8的HashMap扩容要使用双倍扩容?为什么最大容量要设置成2的整数幂?俗话说,知其然还要知其所以然,本篇就重点从设计思想上分析HashMap的实现,直面大师们的内心世界。

为什么hash算法得到的结果可以做到散列均匀分布?

首先看一下HashMap的putVal()方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // 代码省略
}

第一个参数hash是调用HashMap的hash()方法得到的,
我们看一下这个方法:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这段代码叫做“扰动函数

大家都知道上面代码里的key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。

理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648到2147483648。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。

但问题是一个40亿长度的数组,内存是放不下的。你想,HashMap扩容之前的数组初始大小才16。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。结合我们前两篇文章的分析,取模的运算是使用位运算h & (length-1)来实现的。

顺便说一下,这也正好解释了为什么HashMap的数组长度要取2的整数幂。因为2的整数幂用二级制表示一定是100···这种形式,因此2的整数幂减1一定是11111····这种形式, 这样(数组长度-1)正好相当于一个“低位掩码”, “与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。

    10100101 11000100 00100101
&   00000000 00000000 00001111
——————————————————————————————
    00000000 00000000 00000101    //高位全部归零,只保留末四位

但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。

这时候“扰动函数”的价值就体现出来了,说到这里大家应该猜出来了。看下面这个图

扰动函数示例
右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。

由此我们可以得出结论:
hash()方法是对hashcode又一次进行了散列,增加值的随机性。

但是读到这里,我们依然没有看到key.hashCode()方法是如何做到散列的。我们追踪key的hashCode()方法:

public native int hashCode();

这是个native的方法,我们看一下这个native方法的实现:

static inline intptr_t get_next_hash(Thread * Self, oop obj) {
  intptr_t value = 0 ;
  if (hashCode == 0) {
    // 根据Park-Miller伪随机数生成器生成的随机数
     value = os::random() ;
  } else
  if (hashCode == 1) {
     // 此类方案将对象的内存地址,做移位运算后与一个随机数进行异或得到结果
     intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
     value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
  } else
  if (hashCode == 2) {
     value = 1 ;            // 返回固定的1
} else
  if (hashCode == 3) {
     value = ++GVars.hcSequence ;  // 返回一个自增序列的当前值
  } else
  if (hashCode == 4) {
     value = cast_from_oop<intptr_t>(obj) ;  // 对象地址
  } else {
     // 通过和当前线程有关的一个随机数+三个确定值
     unsigned t = Self->_hashStateX ;
     t ^= (t << 11) ;
     Self->_hashStateX = Self->_hashStateY ;
     Self->_hashStateY = Self->_hashStateZ ;
     Self->_hashStateZ = Self->_hashStateW ;
     unsigned v = Self->_hashStateW ;
     v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
     Self->_hashStateW = v ;
     value = v ;
  }

  value &= markOopDesc::hash_mask;
  if (value == 0) value = 0xBAD ;
  assert (value != markOopDesc::no_hash, "invariant") ;
  TEVENT (hashCode: GENERATE) ;
  return value;
}

Java8默认是通过和当前线程有关的一个随机数+三个确定值,运用Marsaglia’s xorshift scheme随机数算法得到的一个随机数。

具体源码分析可以参考:

Java根类Object的方法说明

从以上的分析我们可以得出如下结论:
hashCode()方法通过随机数+内存地址进行一系列操作得到一个随机值作为hashcode,这个随机值是均匀的
HashMap()的hash()方法将hashcode和hashcode右移16位后进行异或运算,进一步增加随机性。

equals()方法和hashcode()的关系是什么?
这一点又是一个长篇大论,下面直接说结论:

  1. 若重写了equals(Object obj)方法,则有必要重写hashCode()方法。
  2. 若两个对象equals(Object obj)返回true,则hashCode()有必要也返回相同的int数。
  3. 若两个对象equals(Object obj)返回false,则hashCode()不一定返回不同的int数。
  4. 若两个对象hashCode()返回相同int数,则equals(Object obj)不一定返回true。
  5. 若两个对象hashCode()返回不同int数,则equals(Object obj)一定返回false。
  6. 同一对象在执行期间若已经存储在集合中,则不能修改影响hashCode值的相关信息,否则会导致内存泄露问题。

具体的分析可以参考下面的文章:

彻底搞懂hashCode与equals的作用与区别

HashMap 的容量不是2的整数幂有什么问题么?

我们再看看刚刚说的那个根据hash计算下标的方法:

tab[(n - 1) & hash];

其中 n 是数组的长度。其实该算法的结果和模运算的结果是相同的。但是,对于现代的处理器来说,除法和求余数(模运算)是最慢的动作。

上面情况下和模运算相同

a % b == (b-1) & a ,当b是2的指数时,等式成立。

我们说 & 与运算的定义:与运算 第一个操作数的的第n位于第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0;

当 n 为 16 时,与运算 101010100101001001101 时,也就是
1111 & 101010100101001001000 结果:1000 = 8
1111 & 101000101101001001001 结果:1001 = 9
1111 & 101010101101101001010 结果: 1010 = 10
1111 & 101100100111001101100 结果: 1100 = 12

可以看到,当 n 为 2 的幂次方的时候,减一之后就会得到 1111···· 的数字,这个数字正好可以掩码。并且得到的结果取决于 hash 值。因为 hash 值是1,那么最终的结果也是1 ,hash 值是0,最终的结果也是0。

我们说,hash 算法的目的是为了让hash值均匀的分布在桶中那么,如何做到呢?试想一下,如果不使用 2 的幂次方作为数组的长度会怎么样?

假设我们的数组长度是10,还是上面的公式:
1010 & 101010100101001001000 结果:1000 = 8
1010 & 101000101101001001001 结果:1000 = 8
1010 & 101010101101101001010 结果: 1010 = 10
1010 & 101100100111001101100 结果: 1000 = 8

看到结果我们惊呆了,这种散列结果,会导致这些不同的key值几乎全部进入到相同的插槽中,形成链表,性能急剧下降。

所以说,我们一定要保证 & 中的二进制位全为 1,才能最大限度的利用 hash 值,并更好的散列,只有全是1 ,才能有更多的散列结果。如果是 1010,有的散列结果是永远都不会出现的,比如 0111,0101,1111,1110…,只要 & 之前的数有 0, 对应的 1 肯定就不会出现(因为只有都是1才会为1)。大大限制了散列的范围。


Java源码分析之LinkedHashMap

前面主要分析了HashMap的实现,再来继续分析LinkedHashMap的实现,LinkedHashMap继承自HashMap,也就是它是HashMap功能的扩展,它和HashMap的区别在于内部维护了一个双向链表来维护数据的读取顺序。下面我们结合源码来看一下。

我们还是从构造方法看起

public LinkedHashMap() {
	super();
	accessOrder = false;
}

这里调用了父类也就是HashMap类的构造方法,并且将accessOrder值置为false,这个accessOrder值在LinkedHashMap中有很重要的作用,接下来我们会讲。

我们回顾一下HashMap的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)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

我们看这段代码的第7行和第18行,这个newNode()在LinkedHashMap类中被重写了,我们来看一下重写的实现:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMapEntry<K,V> p = new LinkedHashMapEntry<K,V>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

它和HashMap的newNode()方法实现有两个区别

  1. LinkedHashMap会生成LinkedHashMapEntry对象作为节点,而HashMap生成的是Node的对象
  2. LinkedHashMap相比HashMap执行了linkNodeLast()方法

我们看一下linkNodeLast()方法:

private void linkNodeLast(LinkedHashMapEntry<K,V> p) {
    LinkedHashMapEntry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

代码很简单,就是往双向链表的尾部插入元素的操作。

也就是说,只要是会产生新节点的地方,这个新产生的节点就会插入到LinkedHashMap维持的双向链表的尾部,这就是LinkedHashMap可以保持元素插入顺序的关键点所在了!

还记得之前分析HashMap的putVal()方法时,当时我提到在put()方法的最后执行了afterNodeInsertion(evict)方法,这个方法在HashMap类中并没有具体的实现,而是交给了LinkedHashMap去实现,HashMap中的afterNodeInsertion()如下所示:

// Callbacks to allow LinkedHashMp post-actions
void afterNodeAccess(Node<K, V> p){}
void afterNodeInsertion(boolean evict){}
void afterNodeRemoval(Node<K, V> p){}

它具体的实现在LinkedHashMap类中,我们跟踪进去看一下:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMapEntry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

这里我们看到,如果所有判断条件均成立的话,会删除首节点,也就是第一个进入的节点,这个特性在哪里会被用到呢?我想很多人都能猜得到,就我们日常开发中经常使用的LRUCache!

我们接着看putVal()方法的第33行,这里执行了afterNodeAccess(e)方法,通过前几篇的分析我们知道,这段代码的执行条件是当要put的节点的key值在HashMap中存在,那么这个方法的作用是什么呢,我们跟踪进去看一下:

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMapEntry<K,V> last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMapEntry<K,V> p =
            (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

我们看到,在afterNodeAccess()方法中,当accessOrder为true且尾结点不为目标节点时,会把目标节点放置到双向链表的队尾,我们思考一下,把最近访问的元素放到队尾,这个特性在哪里可以用到呢? 答案还是LRUCache!

我们看一下java中LRUCache类的实现:

public class LruCache<K, V> {
    private final LinkedHashMap<K, V> map;
    //代码省略
    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
    //代码省略
}

这里在构造LinkedHashMap对象时调用的LinkedHashMap的三参构造方法,我们看一下这个构造方法:

public LinkedHashMap(int initialCapacity,
                      float loadFactor,
                      boolean accessOrder) {
	super(initialCapacity, loadFactor);
	this.accessOrder = accessOrder;
}

我们看到,第三个参数LRUCache设置为true,结合我们之前的分析,我们可以得出结论:
只有当accessOrder这个参数为true时才会对节点按照访问顺序重新排序,也就是只有accessOrder为true时LinkedHashMap才能实现LRUCache的特性。

LinkedHashMap对元素的有序遍历

LinkedHashMap对节点的遍历和HashMap类似,也是调用entrySet()得到节点的集合并调用iterator()方法得到迭代器。我们来看一下LinedHashMap的遍历实现:

首先看entrySet()方法:

public Set<Map.Entry<K,V>> entrySet() {
	Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}

代码很简单,返回了一个LinkedEntrySet的对象,接着看LinedEntrySet类:

final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
	public final int size() { 
		return size; 
	}
	public final void clear() { 
		LinkedHashMap.this.clear(); 
	}
	public final Iterator<Map.Entry<K,V>> iterator() {
		return new LinkedEntryIterator();
	}
    // 代码省略
}

还是只看迭代器相关,看一下iterator()方法,返回一个LinkedEntryterator类,看下这个类:

final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {
   public final Map.Entry<K,V> next() { return nextNode(); }
}

看下nextNode()方法:

final LinkedHashMapEntry<K,V> nextNode() {
    LinkedHashMapEntry<K,V> e = next;
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
    current = e;
    next = e.after;
    return e;
}

这里很简单了,看逻辑就知道是取链表的下一个节点,那么这个next节点是在哪里初始化的呢?看一下LinkedHashIterator的构造方法:

LinkedHashIterator() {
  	next = head;
    expectedModCount = modCount;
    current = null;
}

在这里吧next指向了双向链表的头结点。

至此LinkedHashMap的遍历逻辑就很清晰了,其实就是对其内部持有和维护的双向链表的遍历。


Java源码分析之HashSet

我们先提出几个问题:

  1. HashSet怎么保证添加元素不重复?
  2. HashSet是否允许null元素?
  3. HashSet是有序的吗?
  4. HashSet是同步的吗?

我们都知道Set是一个内部元素不会重复的集合,那么HashSet是如何做到这一点的呢?我们结合源码看一下

先看一下HashSet类的成员变量:

private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();

很简单,就两个变量,map是一个HashMap的对象,map的key用来储存HashSet中的元素,PRESENT用来作为map的value值

再看一下构造方法:

public HashSet() {
    map = new HashMap<>();
}

public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}

public HashSet(int initialCapacity) {
    map = new HashMap<>(initialCapacity);
}

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

可以看到,HashSet所有的构造方法都执行了对map的初始化。

再看一下add()方法:

public boolean add(E e) {
	return map.put(e, PRESENT)==null;
}

代码也很简单,就是把要添加的元素作为key,PRESENT作为value往map中put数据。

再看一下iterator()方法:

public Iterator<E> iterator() {
	return map.keySet().iterator();
}

也很简单,就是返回map的keySet的迭代器。

至此,我们就可以回答本篇开头提出的问题:

  1. HashSet内部是用HashMap的key来保存元素,因为HashMap的特性,key是不可能重复的,故HashSet也不可能有重复数据。
  2. HashMap可以以null作为key,所以HashSet可以有null值
  3. HashMap不是有序的,故HashSet也不是有序的
  4. HashMap不是线程安全的,故HashSet也不是线程安全的。

因为HashSet其实就是对HashMap的封装,代码比较简单,源码暂时先分析到这里。

原文链接:

参考文章:

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值